Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2016 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 import os
     18 import sys
     19 import struct
     20 
     21 FAT_TABLE_START = 0x200
     22 DEL_MARKER = 0xe5
     23 ESCAPE_DEL_MARKER = 0x05
     24 
     25 ATTRIBUTE_READ_ONLY = 0x1
     26 ATTRIBUTE_HIDDEN = 0x2
     27 ATTRIBUTE_SYSTEM = 0x4
     28 ATTRIBUTE_VOLUME_LABEL = 0x8
     29 ATTRIBUTE_SUBDIRECTORY = 0x10
     30 ATTRIBUTE_ARCHIVE = 0x20
     31 ATTRIBUTE_DEVICE = 0x40
     32 
     33 LFN_ATTRIBUTES = \
     34     ATTRIBUTE_VOLUME_LABEL | \
     35     ATTRIBUTE_SYSTEM | \
     36     ATTRIBUTE_HIDDEN | \
     37     ATTRIBUTE_READ_ONLY
     38 LFN_ATTRIBUTES_BYTE = struct.pack("B", LFN_ATTRIBUTES)
     39 
     40 MAX_CLUSTER_ID = 0x7FFF
     41 
     42 def read_le_short(f):
     43   "Read a little-endian 2-byte integer from the given file-like object"
     44   return struct.unpack("<H", f.read(2))[0]
     45 
     46 def read_le_long(f):
     47   "Read a little-endian 4-byte integer from the given file-like object"
     48   return struct.unpack("<L", f.read(4))[0]
     49 
     50 def read_byte(f):
     51   "Read a 1-byte integer from the given file-like object"
     52   return struct.unpack("B", f.read(1))[0]
     53 
     54 def skip_bytes(f, n):
     55   "Fast-forward the given file-like object by n bytes"
     56   f.seek(n, os.SEEK_CUR)
     57 
     58 def skip_short(f):
     59   "Fast-forward the given file-like object 2 bytes"
     60   skip_bytes(f, 2)
     61 
     62 def skip_byte(f):
     63   "Fast-forward the given file-like object 1 byte"
     64   skip_bytes(f, 1)
     65 
     66 def rewind_bytes(f, n):
     67   "Rewind the given file-like object n bytes"
     68   skip_bytes(f, -n)
     69 
     70 def rewind_short(f):
     71   "Rewind the given file-like object 2 bytes"
     72   rewind_bytes(f, 2)
     73 
     74 class fake_file(object):
     75   """
     76   Interface for python file-like objects that we use to manipulate the image.
     77   Inheritors must have an idx member which indicates the file pointer, and a
     78   size member which indicates the total file size.
     79   """
     80 
     81   def seek(self, amount, direction=0):
     82     "Implementation of seek from python's file-like object interface."
     83     if direction == os.SEEK_CUR:
     84       self.idx += amount
     85     elif direction == os.SEEK_END:
     86       self.idx = self.size - amount
     87     else:
     88       self.idx = amount
     89 
     90     if self.idx < 0:
     91       self.idx = 0
     92     if self.idx > self.size:
     93       self.idx = self.size
     94 
     95 class fat_file(fake_file):
     96   """
     97   A file inside of our fat image. The file may or may not have a dentry, and
     98   if it does this object knows nothing about it. All we see is a valid cluster
     99   chain.
    100   """
    101 
    102   def __init__(self, fs, cluster, size=None):
    103     """
    104     fs: The fat() object for the image this file resides in.
    105     cluster: The first cluster of data for this file.
    106     size: The size of this file. If not given, we use the total length of the
    107           cluster chain that starts from the cluster argument.
    108     """
    109     self.fs = fs
    110     self.start_cluster = cluster
    111     self.size = size
    112 
    113     if self.size is None:
    114       self.size = fs.get_chain_size(cluster)
    115 
    116     self.idx = 0
    117 
    118   def read(self, size):
    119     "Read method for pythonic file-like interface."
    120     if self.idx + size > self.size:
    121       size = self.size - self.idx
    122     got = self.fs.read_file(self.start_cluster, self.idx, size)
    123     self.idx += len(got)
    124     return got
    125 
    126   def write(self, data):
    127     "Write method for pythonic file-like interface."
    128     self.fs.write_file(self.start_cluster, self.idx, data)
    129     self.idx += len(data)
    130 
    131     if self.idx > self.size:
    132       self.size = self.idx
    133 
    134 def shorten(name, index):
    135   """
    136   Create a file short name from the given long name (with the extension already
    137   removed). The index argument gives a disambiguating integer to work into the
    138   name to avoid collisions.
    139   """
    140   name = "".join(name.split('.')).upper()
    141   postfix = "~" + str(index)
    142   return name[:8 - len(postfix)] + postfix
    143 
    144 class fat_dir(object):
    145   "A directory in our fat filesystem."
    146 
    147   def __init__(self, backing):
    148     """
    149     backing: A file-like object from which we can read dentry info. Should have
    150     an fs member allowing us to get to the underlying image.
    151     """
    152     self.backing = backing
    153     self.dentries = []
    154     to_read = self.backing.size / 32
    155 
    156     self.backing.seek(0)
    157 
    158     while to_read > 0:
    159       (dent, consumed) = self.backing.fs.read_dentry(self.backing)
    160       to_read -= consumed
    161 
    162       if dent:
    163         self.dentries.append(dent)
    164 
    165   def __str__(self):
    166     return "\n".join([str(x) for x in self.dentries]) + "\n"
    167 
    168   def add_dentry(self, attributes, shortname, ext, longname, first_cluster,
    169       size):
    170     """
    171     Add a new dentry to this directory.
    172     attributes: Attribute flags for this dentry. See the ATTRIBUTE_ constants
    173                 above.
    174     shortname: Short name of this file. Up to 8 characters, no dots.
    175     ext: Extension for this file. Up to 3 characters, no dots.
    176     longname: The long name for this file, with extension. Largely unrestricted.
    177     first_cluster: The first cluster in the cluster chain holding the contents
    178                    of this file.
    179     size: The size of this file. Set to 0 for subdirectories.
    180     """
    181     new_dentry = dentry(self.backing.fs, attributes, shortname, ext,
    182         longname, first_cluster, size)
    183     new_dentry.commit(self.backing)
    184     self.dentries.append(new_dentry)
    185     return new_dentry
    186 
    187   def make_short_name(self, name):
    188     """
    189     Given a long file name, return an 8.3 short name as a tuple. Name will be
    190     engineered not to collide with other such names in this folder.
    191     """
    192     parts = name.rsplit('.', 1)
    193 
    194     if len(parts) == 1:
    195       parts.append('')
    196 
    197     name = parts[0]
    198     ext = parts[1].upper()
    199 
    200     index = 1
    201     shortened = shorten(name, index)
    202 
    203     for dent in self.dentries:
    204       assert dent.longname != name, "File must not exist"
    205       if dent.shortname == shortened:
    206         index += 1
    207         shortened = shorten(name, index)
    208 
    209     if len(name) <= 8 and len(ext) <= 3 and not '.' in name:
    210       return (name.upper().ljust(8), ext.ljust(3))
    211 
    212     return (shortened.ljust(8), ext[:3].ljust(3))
    213 
    214   def new_file(self, name, data=None):
    215     """
    216     Add a new regular file to this directory.
    217     name: The name of the new file.
    218     data: The contents of the new file. Given as a file-like object.
    219     """
    220     size = 0
    221     if data:
    222       data.seek(0, os.SEEK_END)
    223       size = data.tell()
    224 
    225     # Empty files shouldn't have any clusters assigned.
    226     chunk = self.backing.fs.allocate(size) if size > 0 else 0
    227     (shortname, ext) = self.make_short_name(name)
    228     self.add_dentry(0, shortname, ext, name, chunk, size)
    229 
    230     if data is None:
    231       return
    232 
    233     data_file = fat_file(self.backing.fs, chunk, size)
    234     data.seek(0)
    235     data_file.write(data.read())
    236 
    237   def open_subdirectory(self, name):
    238     """
    239     Open a subdirectory of this directory with the given name. If the
    240     subdirectory doesn't exist, a new one is created instead.
    241     Returns a fat_dir().
    242     """
    243     for dent in self.dentries:
    244       if dent.longname == name:
    245         return dent.open_directory()
    246 
    247     chunk = self.backing.fs.allocate(1)
    248     (shortname, ext) = self.make_short_name(name)
    249     new_dentry = self.add_dentry(ATTRIBUTE_SUBDIRECTORY, shortname,
    250             ext, name, chunk, 0)
    251     result = new_dentry.open_directory()
    252 
    253     parent_cluster = 0
    254 
    255     if hasattr(self.backing, 'start_cluster'):
    256       parent_cluster = self.backing.start_cluster
    257 
    258     result.add_dentry(ATTRIBUTE_SUBDIRECTORY, '.', '', '', chunk, 0)
    259     result.add_dentry(ATTRIBUTE_SUBDIRECTORY, '..', '', '', parent_cluster, 0)
    260 
    261     return result
    262 
    263 def lfn_checksum(name_data):
    264   """
    265   Given the characters of an 8.3 file name (concatenated *without* the dot),
    266   Compute a one-byte checksum which needs to appear in corresponding long file
    267   name entries.
    268   """
    269   assert len(name_data) == 11, "Name data should be exactly 11 characters"
    270   name_data = struct.unpack("B" * 11, name_data)
    271 
    272   result = 0
    273 
    274   for char in name_data:
    275     last_bit = (result & 1) << 7
    276     result = (result >> 1) | last_bit
    277     result += char
    278     result = result & 0xFF
    279 
    280   return struct.pack("B", result)
    281 
    282 class dentry(object):
    283   "A directory entry"
    284   def __init__(self, fs, attributes, shortname, ext, longname,
    285       first_cluster, size):
    286     """
    287     fs: The fat() object for the image we're stored in.
    288     attributes: The attribute flags for this dentry. See the ATTRIBUTE_ flags
    289                 above.
    290     shortname: The short name stored in this dentry. Up to 8 characters, no
    291                dots.
    292     ext: The file extension stored in this dentry. Up to 3 characters, no
    293          dots.
    294     longname: The long file name stored in this dentry.
    295     first_cluster: The first cluster in the cluster chain backing the file
    296                    this dentry points to.
    297     size: Size of the file this dentry points to. 0 for subdirectories.
    298     """
    299     self.fs = fs
    300     self.attributes = attributes
    301     self.shortname = shortname
    302     self.ext = ext
    303     self.longname = longname
    304     self.first_cluster = first_cluster
    305     self.size = size
    306 
    307   def name(self):
    308     "A friendly text file name for this dentry."
    309     if self.longname:
    310       return self.longname
    311 
    312     if not self.ext or len(self.ext) == 0:
    313       return self.shortname
    314 
    315     return self.shortname + "." + self.ext
    316 
    317   def __str__(self):
    318     return self.name() + " (" + str(self.size) + \
    319       " bytes @ " + str(self.first_cluster) + ")"
    320 
    321   def is_directory(self):
    322     "Return whether this dentry points to a directory."
    323     return (self.attributes & ATTRIBUTE_SUBDIRECTORY) != 0
    324 
    325   def open_file(self):
    326     "Open the target of this dentry if it is a regular file."
    327     assert not self.is_directory(), "Cannot open directory as file"
    328     return fat_file(self.fs, self.first_cluster, self.size)
    329 
    330   def open_directory(self):
    331     "Open the target of this dentry if it is a directory."
    332     assert self.is_directory(), "Cannot open file as directory"
    333     return fat_dir(fat_file(self.fs, self.first_cluster))
    334 
    335   def longname_records(self, checksum):
    336     """
    337     Get the longname records necessary to store this dentry's long name,
    338     packed as a series of 32-byte strings.
    339     """
    340     if self.longname is None:
    341       return []
    342     if len(self.longname) == 0:
    343       return []
    344 
    345     encoded_long_name = self.longname.encode('utf-16-le')
    346     long_name_padding = "\0" * (26 - (len(encoded_long_name) % 26))
    347     padded_long_name = encoded_long_name + long_name_padding
    348 
    349     chunks = [padded_long_name[i:i+26] for i in range(0,
    350       len(padded_long_name), 26)]
    351     records = []
    352     sequence_number = 1
    353 
    354     for c in chunks:
    355       sequence_byte = struct.pack("B", sequence_number)
    356       sequence_number += 1
    357       record = sequence_byte + c[:10] + LFN_ATTRIBUTES_BYTE + "\0" + \
    358           checksum + c[10:22] + "\0\0" + c[22:]
    359       records.append(record)
    360 
    361     last = records.pop()
    362     last_seq = struct.unpack("B", last[0])[0]
    363     last_seq = last_seq | 0x40
    364     last = struct.pack("B", last_seq) + last[1:]
    365     records.append(last)
    366     records.reverse()
    367 
    368     return records
    369 
    370   def commit(self, f):
    371     """
    372     Write this dentry into the given file-like object,
    373     which is assumed to contain a FAT directory.
    374     """
    375     f.seek(0)
    376     padded_short_name = self.shortname.ljust(8)
    377     padded_ext = self.ext.ljust(3)
    378     name_data = padded_short_name + padded_ext
    379     longname_record_data = self.longname_records(lfn_checksum(name_data))
    380     record = struct.pack("<11sBBBHHHHHHHL",
    381         name_data,
    382         self.attributes,
    383         0,
    384         0,
    385         0,
    386         0,
    387         0,
    388         0,
    389         0,
    390         0,
    391         self.first_cluster,
    392         self.size)
    393     entry = "".join(longname_record_data + [record])
    394 
    395     record_count = len(longname_record_data) + 1
    396 
    397     found_count = 0
    398     while found_count < record_count:
    399       record = f.read(32)
    400 
    401       if record is None or len(record) != 32:
    402         # We reached the EOF, so we need to extend the file with a new cluster.
    403         f.write("\0" * self.fs.bytes_per_cluster)
    404         f.seek(-self.fs.bytes_per_cluster, os.SEEK_CUR)
    405         record = f.read(32)
    406 
    407       marker = struct.unpack("B", record[0])[0]
    408 
    409       if marker == DEL_MARKER or marker == 0:
    410         found_count += 1
    411       else:
    412         found_count = 0
    413 
    414     f.seek(-(record_count * 32), os.SEEK_CUR)
    415     f.write(entry)
    416 
    417 class root_dentry_file(fake_file):
    418   """
    419   File-like object for the root directory. The root directory isn't stored in a
    420   normal file, so we can't use a normal fat_file object to create a view of it.
    421   """
    422   def __init__(self, fs):
    423     self.fs = fs
    424     self.idx = 0
    425     self.size = fs.root_entries * 32
    426 
    427   def read(self, count):
    428     f = self.fs.f
    429     f.seek(self.fs.data_start() + self.idx)
    430 
    431     if self.idx + count > self.size:
    432       count = self.size - self.idx
    433 
    434     ret = f.read(count)
    435     self.idx += len(ret)
    436     return ret
    437 
    438   def write(self, data):
    439     f = self.fs.f
    440     f.seek(self.fs.data_start() + self.idx)
    441 
    442     if self.idx + len(data) > self.size:
    443       data = data[:self.size - self.idx]
    444 
    445     f.write(data)
    446     self.idx += len(data)
    447     if self.idx > self.size:
    448       self.size = self.idx
    449 
    450 class fat(object):
    451   "A FAT image"
    452 
    453   def __init__(self, path):
    454     """
    455     path: Path to an image file containing a FAT file system.
    456     """
    457     f = open(path, "r+b")
    458 
    459     self.f = f
    460 
    461     f.seek(0xb)
    462     bytes_per_sector = read_le_short(f)
    463     sectors_per_cluster = read_byte(f)
    464 
    465     self.bytes_per_cluster = bytes_per_sector * sectors_per_cluster
    466 
    467     reserved_sectors = read_le_short(f)
    468     assert reserved_sectors == 1, \
    469         "Can only handle FAT with 1 reserved sector"
    470 
    471     fat_count = read_byte(f)
    472     assert fat_count == 2, "Can only handle FAT with 2 tables"
    473 
    474     self.root_entries = read_le_short(f)
    475 
    476     skip_short(f) # Image size. Sort of. Useless field.
    477     skip_byte(f) # Media type. We don't care.
    478 
    479     self.fat_size = read_le_short(f) * bytes_per_sector
    480     self.root = fat_dir(root_dentry_file(self))
    481 
    482   def data_start(self):
    483     """
    484     Index of the first byte after the FAT tables.
    485     """
    486     return FAT_TABLE_START + self.fat_size * 2
    487 
    488   def get_chain_size(self, head_cluster):
    489     """
    490     Return how many total bytes are in the cluster chain rooted at the given
    491     cluster.
    492     """
    493     if head_cluster == 0:
    494       return 0
    495 
    496     f = self.f
    497     f.seek(FAT_TABLE_START + head_cluster * 2)
    498 
    499     cluster_count = 0
    500 
    501     while head_cluster <= MAX_CLUSTER_ID:
    502       cluster_count += 1
    503       head_cluster = read_le_short(f)
    504       f.seek(FAT_TABLE_START + head_cluster * 2)
    505 
    506     return cluster_count * self.bytes_per_cluster
    507 
    508   def read_dentry(self, f=None):
    509     """
    510     Read and decode a dentry from the given file-like object at its current
    511     seek position.
    512     """
    513     f = f or self.f
    514     attributes = None
    515 
    516     consumed = 1
    517 
    518     lfn_entries = {}
    519 
    520     while True:
    521       skip_bytes(f, 11)
    522       attributes = read_byte(f)
    523       rewind_bytes(f, 12)
    524 
    525       if attributes & LFN_ATTRIBUTES != LFN_ATTRIBUTES:
    526         break
    527 
    528       consumed += 1
    529 
    530       seq = read_byte(f)
    531       chars = f.read(10)
    532       skip_bytes(f, 3) # Various hackish nonsense
    533       chars += f.read(12)
    534       skip_short(f) # Lots more nonsense
    535       chars += f.read(4)
    536 
    537       chars = unicode(chars, "utf-16-le").encode("utf-8")
    538 
    539       lfn_entries[seq] = chars
    540 
    541     ind = read_byte(f)
    542 
    543     if ind == 0 or ind == DEL_MARKER:
    544       skip_bytes(f, 31)
    545       return (None, consumed)
    546 
    547     if ind == ESCAPE_DEL_MARKER:
    548       ind = DEL_MARKER
    549 
    550     ind = str(unichr(ind))
    551 
    552     if ind == '.':
    553       skip_bytes(f, 31)
    554       return (None, consumed)
    555 
    556     shortname = ind + f.read(7).rstrip()
    557     ext = f.read(3).rstrip()
    558     skip_bytes(f, 15) # Assorted flags, ctime/atime/mtime, etc.
    559     first_cluster = read_le_short(f)
    560     size = read_le_long(f)
    561 
    562     lfn = lfn_entries.items()
    563     lfn.sort(key=lambda x: x[0])
    564     lfn = reduce(lambda x, y: x + y[1], lfn, "")
    565 
    566     if len(lfn) == 0:
    567       lfn = None
    568     else:
    569       lfn = lfn.split('\0', 1)[0]
    570 
    571     return (dentry(self, attributes, shortname, ext, lfn, first_cluster,
    572       size), consumed)
    573 
    574   def read_file(self, head_cluster, start_byte, size):
    575     """
    576     Read from a given FAT file.
    577     head_cluster: The first cluster in the file.
    578     start_byte: How many bytes in to the file to begin the read.
    579     size: How many bytes to read.
    580     """
    581     f = self.f
    582 
    583     assert size >= 0, "Can't read a negative amount"
    584     if size == 0:
    585       return ""
    586 
    587     got_data = ""
    588 
    589     while True:
    590       size_now = size
    591       if start_byte + size > self.bytes_per_cluster:
    592         size_now = self.bytes_per_cluster - start_byte
    593 
    594       if start_byte < self.bytes_per_cluster:
    595         size -= size_now
    596 
    597         cluster_bytes_from_root = (head_cluster - 2) * \
    598             self.bytes_per_cluster
    599         bytes_from_root = cluster_bytes_from_root + start_byte
    600         bytes_from_data_start = bytes_from_root + self.root_entries * 32
    601 
    602         f.seek(self.data_start() + bytes_from_data_start)
    603         line = f.read(size_now)
    604         got_data += line
    605 
    606         if size == 0:
    607           return got_data
    608 
    609       start_byte -= self.bytes_per_cluster
    610 
    611       if start_byte < 0:
    612         start_byte = 0
    613 
    614       f.seek(FAT_TABLE_START + head_cluster * 2)
    615       assert head_cluster <= MAX_CLUSTER_ID, "Out-of-bounds read"
    616       head_cluster = read_le_short(f)
    617       assert head_cluster > 0, "Read free cluster"
    618 
    619     return got_data
    620 
    621   def write_cluster_entry(self, entry):
    622     """
    623     Write a cluster entry to the FAT table. Assumes our backing file is already
    624     seeked to the correct entry in the first FAT table.
    625     """
    626     f = self.f
    627     f.write(struct.pack("<H", entry))
    628     skip_bytes(f, self.fat_size - 2)
    629     f.write(struct.pack("<H", entry))
    630     rewind_bytes(f, self.fat_size)
    631 
    632   def allocate(self, amount):
    633     """
    634     Allocate a new cluster chain big enough to hold at least the given amount
    635     of bytes.
    636     """
    637     assert amount > 0, "Must allocate a non-zero amount."
    638 
    639     f = self.f
    640     f.seek(FAT_TABLE_START + 4)
    641 
    642     current = None
    643     current_size = 0
    644     free_zones = {}
    645 
    646     pos = 2
    647     while pos < self.fat_size / 2:
    648       data = read_le_short(f)
    649 
    650       if data == 0 and current is not None:
    651         current_size += 1
    652       elif data == 0:
    653         current = pos
    654         current_size = 1
    655       elif current is not None:
    656         free_zones[current] = current_size
    657         current = None
    658 
    659       pos += 1
    660 
    661     if current is not None:
    662       free_zones[current] = current_size
    663 
    664     free_zones = free_zones.items()
    665     free_zones.sort(key=lambda x: x[1])
    666 
    667     grabbed_zones = []
    668     grabbed = 0
    669 
    670     while grabbed < amount and len(free_zones) > 0:
    671       zone = free_zones.pop()
    672       grabbed += zone[1] * self.bytes_per_cluster
    673       grabbed_zones.append(zone)
    674 
    675     if grabbed < amount:
    676       return None
    677 
    678     excess = (grabbed - amount) / self.bytes_per_cluster
    679 
    680     grabbed_zones[-1] = (grabbed_zones[-1][0],
    681         grabbed_zones[-1][1] - excess)
    682 
    683     out = None
    684     grabbed_zones.reverse()
    685 
    686     for cluster, size in grabbed_zones:
    687       entries = range(cluster + 1, cluster + size)
    688       entries.append(out or 0xFFFF)
    689       out = cluster
    690       f.seek(FAT_TABLE_START + cluster * 2)
    691       for entry in entries:
    692         self.write_cluster_entry(entry)
    693 
    694     return out
    695 
    696   def extend_cluster(self, cluster, amount):
    697     """
    698     Given a cluster which is the *last* cluster in a chain, extend it to hold
    699     at least `amount` more bytes.
    700     """
    701     if amount == 0:
    702       return
    703     f = self.f
    704     entry_offset = FAT_TABLE_START + cluster * 2
    705     f.seek(entry_offset)
    706     assert read_le_short(f) == 0xFFFF, "Extending from middle of chain"
    707 
    708     return_cluster = self.allocate(amount)
    709     f.seek(entry_offset)
    710     self.write_cluster_entry(return_cluster)
    711     return return_cluster
    712 
    713   def write_file(self, head_cluster, start_byte, data):
    714     """
    715     Write to a given FAT file.
    716 
    717     head_cluster: The first cluster in the file.
    718     start_byte: How many bytes in to the file to begin the write.
    719     data: The data to write.
    720     """
    721     f = self.f
    722     last_offset = start_byte + len(data)
    723     current_offset = 0
    724     current_cluster = head_cluster
    725 
    726     while current_offset < last_offset:
    727       # Write everything that falls in the cluster starting at current_offset.
    728       data_begin = max(0, current_offset - start_byte)
    729       data_end = min(len(data),
    730                      current_offset + self.bytes_per_cluster - start_byte)
    731       if data_end > data_begin:
    732         cluster_file_offset = (self.data_start() + self.root_entries * 32 +
    733                                (current_cluster - 2) * self.bytes_per_cluster)
    734         f.seek(cluster_file_offset + max(0, start_byte - current_offset))
    735         f.write(data[data_begin:data_end])
    736 
    737       # Advance to the next cluster in the chain or get a new cluster if needed.
    738       current_offset += self.bytes_per_cluster
    739       if last_offset > current_offset:
    740         f.seek(FAT_TABLE_START + current_cluster * 2)
    741         next_cluster = read_le_short(f)
    742         if next_cluster > MAX_CLUSTER_ID:
    743           next_cluster = self.extend_cluster(current_cluster, len(data))
    744         current_cluster = next_cluster
    745         assert current_cluster > 0, "Cannot write free cluster"
    746 
    747 
    748 def add_item(directory, item):
    749   """
    750   Copy a file into the given FAT directory. If the path given is a directory,
    751   copy recursively.
    752   directory: fat_dir to copy the file in to
    753   item: Path of local file to copy
    754   """
    755   if os.path.isdir(item):
    756     base = os.path.basename(item)
    757     if len(base) == 0:
    758       base = os.path.basename(item[:-1])
    759     sub = directory.open_subdirectory(base)
    760     for next_item in sorted(os.listdir(item)):
    761       add_item(sub, os.path.join(item, next_item))
    762   else:
    763     with open(item, 'rb') as f:
    764       directory.new_file(os.path.basename(item), f)
    765 
    766 if __name__ == "__main__":
    767   if len(sys.argv) < 3:
    768     print("Usage: fat16copy.py <image> <file> [<file> ...]")
    769     print("Files are copied into the root of the image.")
    770     print("Directories are copied recursively")
    771     sys.exit(1)
    772 
    773   root = fat(sys.argv[1]).root
    774 
    775   for p in sys.argv[2:]:
    776     add_item(root, p)
    777