#!/usr/bin/env python
#
# Copyright 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
import struct
FAT_TABLE_START =
0x200
DEL_MARKER =
0xe5
ESCAPE_DEL_MARKER =
0x05
ATTRIBUTE_READ_ONLY =
0x1
ATTRIBUTE_HIDDEN =
0x2
ATTRIBUTE_SYSTEM =
0x4
ATTRIBUTE_VOLUME_LABEL =
0x8
ATTRIBUTE_SUBDIRECTORY =
0x10
ATTRIBUTE_ARCHIVE =
0x20
ATTRIBUTE_DEVICE =
0x40
LFN_ATTRIBUTES = \
ATTRIBUTE_VOLUME_LABEL | \
ATTRIBUTE_SYSTEM | \
ATTRIBUTE_HIDDEN | \
ATTRIBUTE_READ_ONLY
LFN_ATTRIBUTES_BYTE = struct.pack(
"B", LFN_ATTRIBUTES)
MAX_CLUSTER_ID =
0x7FFF
def read_le_short(f):
"Read a little-endian 2-byte integer from the given file-like object"
return struct.unpack(
"<H", f.read(
2))[
0]
def read_le_long(f):
"Read a little-endian 4-byte integer from the given file-like object"
return struct.unpack(
"<L", f.read(
4))[
0]
def read_byte(f):
"Read a 1-byte integer from the given file-like object"
return struct.unpack(
"B", f.read(
1))[
0]
def skip_bytes(f, n):
"Fast-forward the given file-like object by n bytes"
f.seek(n, os.SEEK_CUR)
def skip_short(f):
"Fast-forward the given file-like object 2 bytes"
skip_bytes(f,
2)
def skip_byte(f):
"Fast-forward the given file-like object 1 byte"
skip_bytes(f,
1)
def rewind_bytes(f, n):
"Rewind the given file-like object n bytes"
skip_bytes(f, -n)
def rewind_short(f):
"Rewind the given file-like object 2 bytes"
rewind_bytes(f,
2)
class fake_file(object):
"""
Interface
for python file-like objects that we use to manipulate the image.
Inheritors must have an idx member which indicates the file pointer,
and a
size member which indicates the total file size.
"""
def seek(self, amount, direction=
0):
"Implementation of seek from python's file-like object interface."
if direction == os.SEEK_CUR:
self.idx += amount
elif direction == os.SEEK_END:
self.idx = self.size - amount
else:
self.idx = amount
if self.idx <
0:
self.idx =
0
if self.idx > self.size:
self.idx = self.size
class fat_file(fake_file):
"""
A file inside of our fat image. The file may
or may
not have a dentry,
and
if it does this object knows nothing about it. All we see
is a valid cluster
chain.
"""
def __init__(self, fs, cluster, size=
None):
"""
fs: The fat() object
for the image this file resides
in.
cluster: The first cluster of data
for this file.
size: The size of this file.
If not given, we use the total length of the
cluster chain that starts
from the cluster argument.
"""
self.fs = fs
self.start_cluster = cluster
self.size = size
if self.size
is None:
self.size = fs.get_chain_size(cluster)
self.idx =
0
def read(self, size):
"Read method for pythonic file-like interface."
if self.idx + size > self.size:
size = self.size - self.idx
got = self.fs.read_file(self.start_cluster, self.idx, size)
self.idx += len(got)
return got
def write(self, data):
"Write method for pythonic file-like interface."
self.fs.write_file(self.start_cluster, self.idx, data)
self.idx += len(data)
if self.idx > self.size:
self.size = self.idx
def shorten(name, index):
"""
Create a file short name
from the given long name (
with the extension already
removed). The index argument gives a disambiguating integer to work into the
name to avoid collisions.
"""
name =
"".join(name.split(
'.')).upper()
postfix =
"~" + str(index)
return name[:
8 - len(postfix)] + postfix
class fat_dir(object):
"A directory in our fat filesystem."
def __init__(self, backing):
"""
backing: A file-like object
from which we can read dentry info. Should have
an fs member allowing us to get to the underlying image.
"""
self.backing = backing
self.dentries = []
to_read = self.backing.size /
32
self.backing.seek(
0)
while to_read >
0:
(dent, consumed) = self.backing.fs.read_dentry(self.backing)
to_read -= consumed
if dent:
self.dentries.append(dent)
def __str__(self):
return "\n".join([str(x)
for x
in self.dentries]) +
"\n"
def add_dentry(self, attributes, shortname, ext, longname, first_cluster,
size):
"""
Add a new dentry to this directory.
attributes: Attribute flags
for this dentry. See the ATTRIBUTE_ constants
above.
shortname: Short name of this file. Up to
8 characters, no dots.
ext: Extension
for this file. Up to
3 characters, no dots.
longname: The long name
for this file,
with extension. Largely unrestricted.
first_cluster: The first cluster
in the cluster chain holding the contents
of this file.
size: The size of this file. Set to
0 for subdirectories.
"""
new_dentry = dentry(self.backing.fs, attributes, shortname, ext,
longname, first_cluster, size)
new_dentry.commit(self.backing)
self.dentries.append(new_dentry)
return new_dentry
def make_short_name(self, name):
"""
Given a long file name,
return an
8.
3 short name
as a tuple. Name will be
engineered
not to collide
with other such names
in this folder.
"""
parts = name.rsplit(
'.',
1)
if len(parts) ==
1:
parts.append(
'')
name = parts[
0]
ext = parts[
1].upper()
index =
1
shortened = shorten(name, index)
for dent
in self.dentries:
assert dent.longname != name,
"File must not exist"
if dent.shortname == shortened:
index +=
1
shortened = shorten(name, index)
if len(name) <=
8 and len(ext) <=
3 and not '.' in name:
return (name.upper().ljust(
8), ext.ljust(
3))
return (shortened.ljust(
8), ext[:
3].ljust(
3))
def new_file(self, name, data=
None):
"""
Add a new regular file to this directory.
name: The name of the new file.
data: The contents of the new file. Given
as a file-like object.
"""
size =
0
if data:
data.seek(
0, os.SEEK_END)
size = data.tell()
# Empty files shouldn't have any clusters assigned.
chunk = self.backing.fs.allocate(size)
if size >
0 else 0
(shortname, ext) = self.make_short_name(name)
self.add_dentry(
0, shortname, ext, name, chunk, size)
if data
is None:
return
data_file = fat_file(self.backing.fs, chunk, size)
data.seek(
0)
data_file.write(data.read())
def open_subdirectory(self, name):
"""
Open a subdirectory of this directory
with the given name.
If the
subdirectory doesn
't exist, a new one is created instead.
Returns a fat_dir().
"""
for dent
in self.dentries:
if dent.longname == name:
return dent.open_directory()
chunk = self.backing.fs.allocate(
1)
(shortname, ext) = self.make_short_name(name)
new_dentry = self.add_dentry(ATTRIBUTE_SUBDIRECTORY, shortname,
ext, name, chunk,
0)
result = new_dentry.open_directory()
parent_cluster =
0
if hasattr(self.backing,
'start_cluster'):
parent_cluster = self.backing.start_cluster
result.add_dentry(ATTRIBUTE_SUBDIRECTORY,
'.',
'',
'', chunk,
0)
result.add_dentry(ATTRIBUTE_SUBDIRECTORY,
'..',
'',
'', parent_cluster,
0)
return result
def lfn_checksum(name_data):
"""
Given the characters of an
8.
3 file name (concatenated *without* the dot),
Compute a one-byte checksum which needs to appear
in corresponding long file
name entries.
"""
assert len(name_data) ==
11,
"Name data should be exactly 11 characters"
name_data = struct.unpack(
"B" *
11, name_data)
result =
0
for char
in name_data:
last_bit = (result &
1) <<
7
result = (result >>
1) | last_bit
result += char
result = result &
0xFF
return struct.pack(
"B", result)
class dentry(object):
"A directory entry"
def __init__(self, fs, attributes, shortname, ext, longname,
first_cluster, size):
"""
fs: The fat() object
for the image we
're stored in.
attributes: The attribute flags
for this dentry. See the ATTRIBUTE_ flags
above.
shortname: The short name stored
in this dentry. Up to
8 characters, no
dots.
ext: The file extension stored
in this dentry. Up to
3 characters, no
dots.
longname: The long file name stored
in this dentry.
first_cluster: The first cluster
in the cluster chain backing the file
this dentry points to.
size: Size of the file this dentry points to.
0 for subdirectories.
"""
self.fs = fs
self.attributes = attributes
self.shortname = shortname
self.ext = ext
self.longname = longname
self.first_cluster = first_cluster
self.size = size
def name(self):
"A friendly text file name for this dentry."
if self.longname:
return self.longname
if not self.ext
or len(self.ext) ==
0:
return self.shortname
return self.shortname +
"." + self.ext
def __str__(self):
return self.name() +
" (" + str(self.size) + \
" bytes @ " + str(self.first_cluster) +
")"
def is_directory(self):
"Return whether this dentry points to a directory."
return (self.attributes & ATTRIBUTE_SUBDIRECTORY) !=
0
def open_file(self):
"Open the target of this dentry if it is a regular file."
assert not self.is_directory(),
"Cannot open directory as file"
return fat_file(self.fs, self.first_cluster, self.size)
def open_directory(self):
"Open the target of this dentry if it is a directory."
assert self.is_directory(),
"Cannot open file as directory"
return fat_dir(fat_file(self.fs, self.first_cluster))
def longname_records(self, checksum):
"""
Get the longname records necessary to store this dentry
's long name,
packed
as a series of
32-byte strings.
"""
if self.longname
is None:
return []
if len(self.longname) ==
0:
return []
encoded_long_name = self.longname.encode(
'utf-16-le')
long_name_padding =
"\0" * (
26 - (len(encoded_long_name) %
26))
padded_long_name = encoded_long_name + long_name_padding
chunks = [padded_long_name[i:i+
26]
for i
in range(
0,
len(padded_long_name),
26)]
records = []
sequence_number =
1
for c
in chunks:
sequence_byte = struct.pack(
"B", sequence_number)
sequence_number +=
1
record = sequence_byte + c[:
10] + LFN_ATTRIBUTES_BYTE +
"\0" + \
checksum + c[
10:
22] +
"\0\0" + c[
22:]
records.append(record)
last = records.pop()
last_seq = struct.unpack(
"B", last[
0])[
0]
last_seq = last_seq |
0x40
last = struct.pack(
"B", last_seq) + last[
1:]
records.append(last)
records.reverse()
return records
def commit(self, f):
"""
Write this dentry into the given file-like object,
which
is assumed to contain a FAT directory.
"""
f.seek(
0)
padded_short_name = self.shortname.ljust(
8)
padded_ext = self.ext.ljust(
3)
name_data = padded_short_name + padded_ext
longname_record_data = self.longname_records(lfn_checksum(name_data))
record = struct.pack(
"<11sBBBHHHHHHHL",
name_data,
self.attributes,
0,
0,
0,
0,
0,
0,
0,
0,
self.first_cluster,
self.size)
entry =
"".join(longname_record_data + [record])
record_count = len(longname_record_data) +
1
found_count =
0
while found_count < record_count:
record = f.read(
32)
if record
is None or len(record) !=
32:
# We reached the EOF, so we need to extend the file with a new cluster.
f.write(
"\0" * self.fs.bytes_per_cluster)
f.seek(-self.fs.bytes_per_cluster, os.SEEK_CUR)
record = f.read(
32)
marker = struct.unpack(
"B", record[
0])[
0]
if marker == DEL_MARKER
or marker ==
0:
found_count +=
1
else:
found_count =
0
f.seek(-(record_count *
32), os.SEEK_CUR)
f.write(entry)
class root_dentry_file(fake_file):
"""
File-like object
for the root directory. The root directory isn
't stored in a
normal file, so we can
't use a normal fat_file object to create a view of it.
"""
def __init__(self, fs):
self.fs = fs
self.idx =
0
self.size = fs.root_entries *
32
def read(self, count):
f = self.fs.f
f.seek(self.fs.data_start() + self.idx)
if self.idx + count > self.size:
count = self.size - self.idx
ret = f.read(count)
self.idx += len(ret)
return ret
def write(self, data):
f = self.fs.f
f.seek(self.fs.data_start() + self.idx)
if self.idx + len(data) > self.size:
data = data[:self.size - self.idx]
f.write(data)
self.idx += len(data)
if self.idx > self.size:
self.size = self.idx
class fat(object):
"A FAT image"
def __init__(self, path):
"""
path: Path to an image file containing a FAT file system.
"""
f = open(path,
"r+b")
self.f = f
f.seek(
0xb)
bytes_per_sector = read_le_short(f)
sectors_per_cluster = read_byte(f)
self.bytes_per_cluster = bytes_per_sector * sectors_per_cluster
reserved_sectors = read_le_short(f)
assert reserved_sectors ==
1, \
"Can only handle FAT with 1 reserved sector"
fat_count = read_byte(f)
assert fat_count ==
2,
"Can only handle FAT with 2 tables"
self.root_entries = read_le_short(f)
skip_short(f)
# Image size. Sort of. Useless field.
skip_byte(f)
# Media type. We don't care.
self.fat_size = read_le_short(f) * bytes_per_sector
self.root = fat_dir(root_dentry_file(self))
def data_start(self):
"""
Index of the first byte after the FAT tables.
"""
return FAT_TABLE_START + self.fat_size *
2
def get_chain_size(self, head_cluster):
"""
Return how many total bytes are
in the cluster chain rooted at the given
cluster.
"""
if head_cluster ==
0:
return 0
f = self.f
f.seek(FAT_TABLE_START + head_cluster *
2)
cluster_count =
0
while head_cluster <= MAX_CLUSTER_ID:
cluster_count +=
1
head_cluster = read_le_short(f)
f.seek(FAT_TABLE_START + head_cluster *
2)
return cluster_count * self.bytes_per_cluster
def read_dentry(self, f=
None):
"""
Read
and decode a dentry
from the given file-like object at its current
seek position.
"""
f = f
or self.f
attributes =
None
consumed =
1
lfn_entries = {}
while True:
skip_bytes(f,
11)
attributes = read_byte(f)
rewind_bytes(f,
12)
if attributes & LFN_ATTRIBUTES != LFN_ATTRIBUTES:
break
consumed +=
1
seq = read_byte(f)
chars = f.read(
10)
skip_bytes(f,
3)
# Various hackish nonsense
chars += f.read(
12)
skip_short(f)
# Lots more nonsense
chars += f.read(
4)
chars = unicode(chars,
"utf-16-le").encode(
"utf-8")
lfn_entries[seq] = chars
ind = read_byte(f)
if ind ==
0 or ind == DEL_MARKER:
skip_bytes(f,
31)
return (
None, consumed)
if ind == ESCAPE_DEL_MARKER:
ind = DEL_MARKER
ind = str(unichr(ind))
if ind ==
'.':
skip_bytes(f,
31)
return (
None, consumed)
shortname = ind + f.read(
7).rstrip()
ext = f.read(
3).rstrip()
skip_bytes(f,
15)
# Assorted flags, ctime/atime/mtime, etc.
first_cluster = read_le_short(f)
size = read_le_long(f)
lfn = lfn_entries.items()
lfn.sort(key=
lambda x: x[
0])
lfn = reduce(
lambda x, y: x + y[
1], lfn,
"")
if len(lfn) ==
0:
lfn =
None
else:
lfn = lfn.split(
'\0',
1)[
0]
return (dentry(self, attributes, shortname, ext, lfn, first_cluster,
size), consumed)
def read_file(self, head_cluster, start_byte, size):
"""
Read
from a given FAT file.
head_cluster: The first cluster
in the file.
start_byte: How many bytes
in to the file to begin the read.
size: How many bytes to read.
"""
f = self.f
assert size >=
0,
"Can't read a negative amount"
if size ==
0:
return ""
got_data =
""
while True:
size_now = size
if start_byte + size > self.bytes_per_cluster:
size_now = self.bytes_per_cluster - start_byte
if start_byte < self.bytes_per_cluster:
size -= size_now
cluster_bytes_from_root = (head_cluster -
2) * \
self.bytes_per_cluster
bytes_from_root = cluster_bytes_from_root + start_byte
bytes_from_data_start = bytes_from_root + self.root_entries *
32
f.seek(self.data_start() + bytes_from_data_start)
line = f.read(size_now)
got_data += line
if size ==
0:
return got_data
start_byte -= self.bytes_per_cluster
if start_byte <
0:
start_byte =
0
f.seek(FAT_TABLE_START + head_cluster *
2)
assert head_cluster <= MAX_CLUSTER_ID,
"Out-of-bounds read"
head_cluster = read_le_short(f)
assert head_cluster >
0,
"Read free cluster"
return got_data
def write_cluster_entry(self, entry):
"""
Write a cluster entry to the FAT table. Assumes our backing file
is already
seeked to the correct entry
in the first FAT table.
"""
f = self.f
f.write(struct.pack(
"<H", entry))
skip_bytes(f, self.fat_size -
2)
f.write(struct.pack(
"<H", entry))
rewind_bytes(f, self.fat_size)
def allocate(self, amount):
"""
Allocate a new cluster chain big enough to hold at least the given amount
of bytes.
"""
assert amount >
0,
"Must allocate a non-zero amount."
f = self.f
f.seek(FAT_TABLE_START +
4)
current =
None
current_size =
0
free_zones = {}
pos =
2
while pos < self.fat_size /
2:
data = read_le_short(f)
if data ==
0 and current
is not None:
current_size +=
1
elif data ==
0:
current = pos
current_size =
1
elif current
is not None:
free_zones[current] = current_size
current =
None
pos +=
1
if current
is not None:
free_zones[current] = current_size
free_zones = free_zones.items()
free_zones.sort(key=
lambda x: x[
1])
grabbed_zones = []
grabbed =
0
while grabbed < amount
and len(free_zones) >
0:
zone = free_zones.pop()
grabbed += zone[
1] * self.bytes_per_cluster
grabbed_zones.append(zone)
if grabbed < amount:
return None
excess = (grabbed - amount) / self.bytes_per_cluster
grabbed_zones[-
1] = (grabbed_zones[-
1][
0],
grabbed_zones[-
1][
1] - excess)
out =
None
grabbed_zones.reverse()
for cluster, size
in grabbed_zones:
entries = range(cluster +
1, cluster + size)
entries.append(out
or 0xFFFF)
out = cluster
f.seek(FAT_TABLE_START + cluster *
2)
for entry
in entries:
self.write_cluster_entry(entry)
return out
def extend_cluster(self, cluster, amount):
"""
Given a cluster which
is the *last* cluster
in a chain, extend it to hold
at least `amount` more bytes.
"""
if amount ==
0:
return
f = self.f
entry_offset = FAT_TABLE_START + cluster *
2
f.seek(entry_offset)
assert read_le_short(f) ==
0xFFFF,
"Extending from middle of chain"
return_cluster = self.allocate(amount)
f.seek(entry_offset)
self.write_cluster_entry(return_cluster)
return return_cluster
def write_file(self, head_cluster, start_byte, data):
"""
Write to a given FAT file.
head_cluster: The first cluster
in the file.
start_byte: How many bytes
in to the file to begin the write.
data: The data to write.
"""
f = self.f
last_offset = start_byte + len(data)
current_offset =
0
current_cluster = head_cluster
while current_offset < last_offset:
# Write everything that falls in the cluster starting at current_offset.
data_begin = max(
0, current_offset - start_byte)
data_end = min(len(data),
current_offset + self.bytes_per_cluster - start_byte)
if data_end > data_begin:
cluster_file_offset = (self.data_start() + self.root_entries *
32 +
(current_cluster -
2) * self.bytes_per_cluster)
f.seek(cluster_file_offset + max(
0, start_byte - current_offset))
f.write(data[data_begin:data_end])
# Advance to the next cluster in the chain or get a new cluster if needed.
current_offset += self.bytes_per_cluster
if last_offset > current_offset:
f.seek(FAT_TABLE_START + current_cluster *
2)
next_cluster = read_le_short(f)
if next_cluster > MAX_CLUSTER_ID:
next_cluster = self.extend_cluster(current_cluster, len(data))
current_cluster = next_cluster
assert current_cluster >
0,
"Cannot write free cluster"
def add_item(directory, item):
"""
Copy a file into the given FAT directory.
If the path given
is a directory,
copy recursively.
directory: fat_dir to copy the file
in to
item: Path of local file to copy
"""
if os.path.isdir(item):
base = os.path.basename(item)
if len(base) ==
0:
base = os.path.basename(item[:-
1])
sub = directory.open_subdirectory(base)
for next_item
in sorted(os.listdir(item)):
add_item(sub, os.path.join(item, next_item))
else:
with open(item,
'rb')
as f:
directory.new_file(os.path.basename(item), f)
if __name__ ==
"__main__":
if len(sys.argv) <
3:
print(
"Usage: fat16copy.py <image> <file> [<file> ...]")
print(
"Files are copied into the root of the image.")
print(
"Directories are copied recursively")
sys.exit(
1)
root = fat(sys.argv[
1]).root
for p
in sys.argv[
2:]:
add_item(root, p)