| 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) |