| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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 |  | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 225 |     # Empty files shouldn't have any clusters assigned. | 
 | 226 |     chunk = self.backing.fs.allocate(size) if size > 0 else 0 | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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 |  | 
| Alex Deymo | 9a535b5 | 2017-01-31 15:23:29 -0800 | [diff] [blame] | 237 |   def open_subdirectory(self, name): | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 238 |     """ | 
| Alex Deymo | 9a535b5 | 2017-01-31 15:23:29 -0800 | [diff] [blame] | 239 |     Open a subdirectory of this directory with the given name. If the | 
 | 240 |     subdirectory doesn't exist, a new one is created instead. | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 241 |     Returns a fat_dir(). | 
 | 242 |     """ | 
| Alex Deymo | 9a535b5 | 2017-01-31 15:23:29 -0800 | [diff] [blame] | 243 |     for dent in self.dentries: | 
 | 244 |       if dent.longname == name: | 
 | 245 |         return dent.open_directory() | 
 | 246 |  | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 247 |     chunk = self.backing.fs.allocate(1) | 
 | 248 |     (shortname, ext) = self.make_short_name(name) | 
| Casey Dahlin | df71efe | 2016-09-14 13:52:29 -0700 | [diff] [blame] | 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 | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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 | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 398 |     while found_count < record_count: | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 399 |       record = f.read(32) | 
 | 400 |  | 
 | 401 |       if record is None or len(record) != 32: | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 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) | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 406 |  | 
 | 407 |       marker = struct.unpack("B", record[0])[0] | 
 | 408 |  | 
 | 409 |       if marker == DEL_MARKER or marker == 0: | 
 | 410 |         found_count += 1 | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 411 |       else: | 
 | 412 |         found_count = 0 | 
 | 413 |  | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 414 |     f.seek(-(record_count * 32), os.SEEK_CUR) | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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 |     """ | 
| Casey Dahlin | df71efe | 2016-09-14 13:52:29 -0700 | [diff] [blame] | 637 |     assert amount > 0, "Must allocate a non-zero amount." | 
 | 638 |  | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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 |     """ | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 701 |     if amount == 0: | 
 | 702 |       return | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 703 |     f = self.f | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 704 |     entry_offset = FAT_TABLE_START + cluster * 2 | 
 | 705 |     f.seek(entry_offset) | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 706 |     assert read_le_short(f) == 0xFFFF, "Extending from middle of chain" | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 707 |  | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 708 |     return_cluster = self.allocate(amount) | 
 | 709 |     f.seek(entry_offset) | 
 | 710 |     self.write_cluster_entry(return_cluster) | 
 | 711 |     return return_cluster | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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 | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 722 |     last_offset = start_byte + len(data) | 
 | 723 |     current_offset = 0 | 
 | 724 |     current_cluster = head_cluster | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 725 |  | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 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]) | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 736 |  | 
| Alex Deymo | a1c9777 | 2016-09-19 14:32:53 -0700 | [diff] [blame] | 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" | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 746 |  | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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]) | 
| Alex Deymo | 9a535b5 | 2017-01-31 15:23:29 -0800 | [diff] [blame] | 759 |     sub = directory.open_subdirectory(base) | 
| Alex Deymo | 567c5d0 | 2016-09-23 13:12:33 -0700 | [diff] [blame] | 760 |     for next_item in sorted(os.listdir(item)): | 
| Casey Dahlin | 29e2b21 | 2016-09-01 18:07:15 -0700 | [diff] [blame] | 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) |