blob: af8bd838ccd342f6603b9792387114e859f5ad2f [file] [log] [blame]
Casey Dahlin29e2b212016-09-01 18:07:15 -07001#!/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
17import os
18import sys
19import struct
20
21FAT_TABLE_START = 0x200
22DEL_MARKER = 0xe5
23ESCAPE_DEL_MARKER = 0x05
24
25ATTRIBUTE_READ_ONLY = 0x1
26ATTRIBUTE_HIDDEN = 0x2
27ATTRIBUTE_SYSTEM = 0x4
28ATTRIBUTE_VOLUME_LABEL = 0x8
29ATTRIBUTE_SUBDIRECTORY = 0x10
30ATTRIBUTE_ARCHIVE = 0x20
31ATTRIBUTE_DEVICE = 0x40
32
33LFN_ATTRIBUTES = \
34 ATTRIBUTE_VOLUME_LABEL | \
35 ATTRIBUTE_SYSTEM | \
36 ATTRIBUTE_HIDDEN | \
37 ATTRIBUTE_READ_ONLY
38LFN_ATTRIBUTES_BYTE = struct.pack("B", LFN_ATTRIBUTES)
39
40MAX_CLUSTER_ID = 0x7FFF
41
42def 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
46def 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
50def 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
54def skip_bytes(f, n):
55 "Fast-forward the given file-like object by n bytes"
56 f.seek(n, os.SEEK_CUR)
57
58def skip_short(f):
59 "Fast-forward the given file-like object 2 bytes"
60 skip_bytes(f, 2)
61
62def skip_byte(f):
63 "Fast-forward the given file-like object 1 byte"
64 skip_bytes(f, 1)
65
66def rewind_bytes(f, n):
67 "Rewind the given file-like object n bytes"
68 skip_bytes(f, -n)
69
70def rewind_short(f):
71 "Rewind the given file-like object 2 bytes"
72 rewind_bytes(f, 2)
73
74class 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
95class 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
134def 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
144class 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 Deymoa1c97772016-09-19 14:32:53 -0700225 # Empty files shouldn't have any clusters assigned.
226 chunk = self.backing.fs.allocate(size) if size > 0 else 0
Casey Dahlin29e2b212016-09-01 18:07:15 -0700227 (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 new_subdirectory(self, name):
238 """
239 Create a new subdirectory of this directory with the given name.
240 Returns a fat_dir().
241 """
242 chunk = self.backing.fs.allocate(1)
243 (shortname, ext) = self.make_short_name(name)
Casey Dahlindf71efe2016-09-14 13:52:29 -0700244 new_dentry = self.add_dentry(ATTRIBUTE_SUBDIRECTORY, shortname,
245 ext, name, chunk, 0)
246 result = new_dentry.open_directory()
247
248 parent_cluster = 0
249
250 if hasattr(self.backing, 'start_cluster'):
251 parent_cluster = self.backing.start_cluster
252
253 result.add_dentry(ATTRIBUTE_SUBDIRECTORY, '.', '', '', chunk, 0)
254 result.add_dentry(ATTRIBUTE_SUBDIRECTORY, '..', '', '', parent_cluster, 0)
255
256 return result
Casey Dahlin29e2b212016-09-01 18:07:15 -0700257
258def lfn_checksum(name_data):
259 """
260 Given the characters of an 8.3 file name (concatenated *without* the dot),
261 Compute a one-byte checksum which needs to appear in corresponding long file
262 name entries.
263 """
264 assert len(name_data) == 11, "Name data should be exactly 11 characters"
265 name_data = struct.unpack("B" * 11, name_data)
266
267 result = 0
268
269 for char in name_data:
270 last_bit = (result & 1) << 7
271 result = (result >> 1) | last_bit
272 result += char
273 result = result & 0xFF
274
275 return struct.pack("B", result)
276
277class dentry(object):
278 "A directory entry"
279 def __init__(self, fs, attributes, shortname, ext, longname,
280 first_cluster, size):
281 """
282 fs: The fat() object for the image we're stored in.
283 attributes: The attribute flags for this dentry. See the ATTRIBUTE_ flags
284 above.
285 shortname: The short name stored in this dentry. Up to 8 characters, no
286 dots.
287 ext: The file extension stored in this dentry. Up to 3 characters, no
288 dots.
289 longname: The long file name stored in this dentry.
290 first_cluster: The first cluster in the cluster chain backing the file
291 this dentry points to.
292 size: Size of the file this dentry points to. 0 for subdirectories.
293 """
294 self.fs = fs
295 self.attributes = attributes
296 self.shortname = shortname
297 self.ext = ext
298 self.longname = longname
299 self.first_cluster = first_cluster
300 self.size = size
301
302 def name(self):
303 "A friendly text file name for this dentry."
304 if self.longname:
305 return self.longname
306
307 if not self.ext or len(self.ext) == 0:
308 return self.shortname
309
310 return self.shortname + "." + self.ext
311
312 def __str__(self):
313 return self.name() + " (" + str(self.size) + \
314 " bytes @ " + str(self.first_cluster) + ")"
315
316 def is_directory(self):
317 "Return whether this dentry points to a directory."
318 return (self.attributes & ATTRIBUTE_SUBDIRECTORY) != 0
319
320 def open_file(self):
321 "Open the target of this dentry if it is a regular file."
322 assert not self.is_directory(), "Cannot open directory as file"
323 return fat_file(self.fs, self.first_cluster, self.size)
324
325 def open_directory(self):
326 "Open the target of this dentry if it is a directory."
327 assert self.is_directory(), "Cannot open file as directory"
328 return fat_dir(fat_file(self.fs, self.first_cluster))
329
330 def longname_records(self, checksum):
331 """
332 Get the longname records necessary to store this dentry's long name,
333 packed as a series of 32-byte strings.
334 """
335 if self.longname is None:
336 return []
337 if len(self.longname) == 0:
338 return []
339
340 encoded_long_name = self.longname.encode('utf-16-le')
341 long_name_padding = "\0" * (26 - (len(encoded_long_name) % 26))
342 padded_long_name = encoded_long_name + long_name_padding
343
344 chunks = [padded_long_name[i:i+26] for i in range(0,
345 len(padded_long_name), 26)]
346 records = []
347 sequence_number = 1
348
349 for c in chunks:
350 sequence_byte = struct.pack("B", sequence_number)
351 sequence_number += 1
352 record = sequence_byte + c[:10] + LFN_ATTRIBUTES_BYTE + "\0" + \
353 checksum + c[10:22] + "\0\0" + c[22:]
354 records.append(record)
355
356 last = records.pop()
357 last_seq = struct.unpack("B", last[0])[0]
358 last_seq = last_seq | 0x40
359 last = struct.pack("B", last_seq) + last[1:]
360 records.append(last)
361 records.reverse()
362
363 return records
364
365 def commit(self, f):
366 """
367 Write this dentry into the given file-like object,
368 which is assumed to contain a FAT directory.
369 """
370 f.seek(0)
371 padded_short_name = self.shortname.ljust(8)
372 padded_ext = self.ext.ljust(3)
373 name_data = padded_short_name + padded_ext
374 longname_record_data = self.longname_records(lfn_checksum(name_data))
375 record = struct.pack("<11sBBBHHHHHHHL",
376 name_data,
377 self.attributes,
378 0,
379 0,
380 0,
381 0,
382 0,
383 0,
384 0,
385 0,
386 self.first_cluster,
387 self.size)
388 entry = "".join(longname_record_data + [record])
389
390 record_count = len(longname_record_data) + 1
391
392 found_count = 0
Alex Deymoa1c97772016-09-19 14:32:53 -0700393 while found_count < record_count:
Casey Dahlin29e2b212016-09-01 18:07:15 -0700394 record = f.read(32)
395
396 if record is None or len(record) != 32:
Alex Deymoa1c97772016-09-19 14:32:53 -0700397 # We reached the EOF, so we need to extend the file with a new cluster.
398 f.write("\0" * self.fs.bytes_per_cluster)
399 f.seek(-self.fs.bytes_per_cluster, os.SEEK_CUR)
400 record = f.read(32)
Casey Dahlin29e2b212016-09-01 18:07:15 -0700401
402 marker = struct.unpack("B", record[0])[0]
403
404 if marker == DEL_MARKER or marker == 0:
405 found_count += 1
Casey Dahlin29e2b212016-09-01 18:07:15 -0700406 else:
407 found_count = 0
408
Alex Deymoa1c97772016-09-19 14:32:53 -0700409 f.seek(-(record_count * 32), os.SEEK_CUR)
Casey Dahlin29e2b212016-09-01 18:07:15 -0700410 f.write(entry)
411
412class root_dentry_file(fake_file):
413 """
414 File-like object for the root directory. The root directory isn't stored in a
415 normal file, so we can't use a normal fat_file object to create a view of it.
416 """
417 def __init__(self, fs):
418 self.fs = fs
419 self.idx = 0
420 self.size = fs.root_entries * 32
421
422 def read(self, count):
423 f = self.fs.f
424 f.seek(self.fs.data_start() + self.idx)
425
426 if self.idx + count > self.size:
427 count = self.size - self.idx
428
429 ret = f.read(count)
430 self.idx += len(ret)
431 return ret
432
433 def write(self, data):
434 f = self.fs.f
435 f.seek(self.fs.data_start() + self.idx)
436
437 if self.idx + len(data) > self.size:
438 data = data[:self.size - self.idx]
439
440 f.write(data)
441 self.idx += len(data)
442 if self.idx > self.size:
443 self.size = self.idx
444
445class fat(object):
446 "A FAT image"
447
448 def __init__(self, path):
449 """
450 path: Path to an image file containing a FAT file system.
451 """
452 f = open(path, "r+b")
453
454 self.f = f
455
456 f.seek(0xb)
457 bytes_per_sector = read_le_short(f)
458 sectors_per_cluster = read_byte(f)
459
460 self.bytes_per_cluster = bytes_per_sector * sectors_per_cluster
461
462 reserved_sectors = read_le_short(f)
463 assert reserved_sectors == 1, \
464 "Can only handle FAT with 1 reserved sector"
465
466 fat_count = read_byte(f)
467 assert fat_count == 2, "Can only handle FAT with 2 tables"
468
469 self.root_entries = read_le_short(f)
470
471 skip_short(f) # Image size. Sort of. Useless field.
472 skip_byte(f) # Media type. We don't care.
473
474 self.fat_size = read_le_short(f) * bytes_per_sector
475 self.root = fat_dir(root_dentry_file(self))
476
477 def data_start(self):
478 """
479 Index of the first byte after the FAT tables.
480 """
481 return FAT_TABLE_START + self.fat_size * 2
482
483 def get_chain_size(self, head_cluster):
484 """
485 Return how many total bytes are in the cluster chain rooted at the given
486 cluster.
487 """
488 if head_cluster == 0:
489 return 0
490
491 f = self.f
492 f.seek(FAT_TABLE_START + head_cluster * 2)
493
494 cluster_count = 0
495
496 while head_cluster <= MAX_CLUSTER_ID:
497 cluster_count += 1
498 head_cluster = read_le_short(f)
499 f.seek(FAT_TABLE_START + head_cluster * 2)
500
501 return cluster_count * self.bytes_per_cluster
502
503 def read_dentry(self, f=None):
504 """
505 Read and decode a dentry from the given file-like object at its current
506 seek position.
507 """
508 f = f or self.f
509 attributes = None
510
511 consumed = 1
512
513 lfn_entries = {}
514
515 while True:
516 skip_bytes(f, 11)
517 attributes = read_byte(f)
518 rewind_bytes(f, 12)
519
520 if attributes & LFN_ATTRIBUTES != LFN_ATTRIBUTES:
521 break
522
523 consumed += 1
524
525 seq = read_byte(f)
526 chars = f.read(10)
527 skip_bytes(f, 3) # Various hackish nonsense
528 chars += f.read(12)
529 skip_short(f) # Lots more nonsense
530 chars += f.read(4)
531
532 chars = unicode(chars, "utf-16-le").encode("utf-8")
533
534 lfn_entries[seq] = chars
535
536 ind = read_byte(f)
537
538 if ind == 0 or ind == DEL_MARKER:
539 skip_bytes(f, 31)
540 return (None, consumed)
541
542 if ind == ESCAPE_DEL_MARKER:
543 ind = DEL_MARKER
544
545 ind = str(unichr(ind))
546
547 if ind == '.':
548 skip_bytes(f, 31)
549 return (None, consumed)
550
551 shortname = ind + f.read(7).rstrip()
552 ext = f.read(3).rstrip()
553 skip_bytes(f, 15) # Assorted flags, ctime/atime/mtime, etc.
554 first_cluster = read_le_short(f)
555 size = read_le_long(f)
556
557 lfn = lfn_entries.items()
558 lfn.sort(key=lambda x: x[0])
559 lfn = reduce(lambda x, y: x + y[1], lfn, "")
560
561 if len(lfn) == 0:
562 lfn = None
563 else:
564 lfn = lfn.split('\0', 1)[0]
565
566 return (dentry(self, attributes, shortname, ext, lfn, first_cluster,
567 size), consumed)
568
569 def read_file(self, head_cluster, start_byte, size):
570 """
571 Read from a given FAT file.
572 head_cluster: The first cluster in the file.
573 start_byte: How many bytes in to the file to begin the read.
574 size: How many bytes to read.
575 """
576 f = self.f
577
578 assert size >= 0, "Can't read a negative amount"
579 if size == 0:
580 return ""
581
582 got_data = ""
583
584 while True:
585 size_now = size
586 if start_byte + size > self.bytes_per_cluster:
587 size_now = self.bytes_per_cluster - start_byte
588
589 if start_byte < self.bytes_per_cluster:
590 size -= size_now
591
592 cluster_bytes_from_root = (head_cluster - 2) * \
593 self.bytes_per_cluster
594 bytes_from_root = cluster_bytes_from_root + start_byte
595 bytes_from_data_start = bytes_from_root + self.root_entries * 32
596
597 f.seek(self.data_start() + bytes_from_data_start)
598 line = f.read(size_now)
599 got_data += line
600
601 if size == 0:
602 return got_data
603
604 start_byte -= self.bytes_per_cluster
605
606 if start_byte < 0:
607 start_byte = 0
608
609 f.seek(FAT_TABLE_START + head_cluster * 2)
610 assert head_cluster <= MAX_CLUSTER_ID, "Out-of-bounds read"
611 head_cluster = read_le_short(f)
612 assert head_cluster > 0, "Read free cluster"
613
614 return got_data
615
616 def write_cluster_entry(self, entry):
617 """
618 Write a cluster entry to the FAT table. Assumes our backing file is already
619 seeked to the correct entry in the first FAT table.
620 """
621 f = self.f
622 f.write(struct.pack("<H", entry))
623 skip_bytes(f, self.fat_size - 2)
624 f.write(struct.pack("<H", entry))
625 rewind_bytes(f, self.fat_size)
626
627 def allocate(self, amount):
628 """
629 Allocate a new cluster chain big enough to hold at least the given amount
630 of bytes.
631 """
Casey Dahlindf71efe2016-09-14 13:52:29 -0700632 assert amount > 0, "Must allocate a non-zero amount."
633
Casey Dahlin29e2b212016-09-01 18:07:15 -0700634 f = self.f
635 f.seek(FAT_TABLE_START + 4)
636
637 current = None
638 current_size = 0
639 free_zones = {}
640
641 pos = 2
642 while pos < self.fat_size / 2:
643 data = read_le_short(f)
644
645 if data == 0 and current is not None:
646 current_size += 1
647 elif data == 0:
648 current = pos
649 current_size = 1
650 elif current is not None:
651 free_zones[current] = current_size
652 current = None
653
654 pos += 1
655
656 if current is not None:
657 free_zones[current] = current_size
658
659 free_zones = free_zones.items()
660 free_zones.sort(key=lambda x: x[1])
661
662 grabbed_zones = []
663 grabbed = 0
664
665 while grabbed < amount and len(free_zones) > 0:
666 zone = free_zones.pop()
667 grabbed += zone[1] * self.bytes_per_cluster
668 grabbed_zones.append(zone)
669
670 if grabbed < amount:
671 return None
672
673 excess = (grabbed - amount) / self.bytes_per_cluster
674
675 grabbed_zones[-1] = (grabbed_zones[-1][0],
676 grabbed_zones[-1][1] - excess)
677
678 out = None
679 grabbed_zones.reverse()
680
681 for cluster, size in grabbed_zones:
682 entries = range(cluster + 1, cluster + size)
683 entries.append(out or 0xFFFF)
684 out = cluster
685 f.seek(FAT_TABLE_START + cluster * 2)
686 for entry in entries:
687 self.write_cluster_entry(entry)
688
689 return out
690
691 def extend_cluster(self, cluster, amount):
692 """
693 Given a cluster which is the *last* cluster in a chain, extend it to hold
694 at least `amount` more bytes.
695 """
Alex Deymoa1c97772016-09-19 14:32:53 -0700696 if amount == 0:
697 return
Casey Dahlin29e2b212016-09-01 18:07:15 -0700698 f = self.f
Alex Deymoa1c97772016-09-19 14:32:53 -0700699 entry_offset = FAT_TABLE_START + cluster * 2
700 f.seek(entry_offset)
Casey Dahlin29e2b212016-09-01 18:07:15 -0700701 assert read_le_short(f) == 0xFFFF, "Extending from middle of chain"
Casey Dahlin29e2b212016-09-01 18:07:15 -0700702
Alex Deymoa1c97772016-09-19 14:32:53 -0700703 return_cluster = self.allocate(amount)
704 f.seek(entry_offset)
705 self.write_cluster_entry(return_cluster)
706 return return_cluster
Casey Dahlin29e2b212016-09-01 18:07:15 -0700707
708 def write_file(self, head_cluster, start_byte, data):
709 """
710 Write to a given FAT file.
711
712 head_cluster: The first cluster in the file.
713 start_byte: How many bytes in to the file to begin the write.
714 data: The data to write.
715 """
716 f = self.f
Alex Deymoa1c97772016-09-19 14:32:53 -0700717 last_offset = start_byte + len(data)
718 current_offset = 0
719 current_cluster = head_cluster
Casey Dahlin29e2b212016-09-01 18:07:15 -0700720
Alex Deymoa1c97772016-09-19 14:32:53 -0700721 while current_offset < last_offset:
722 # Write everything that falls in the cluster starting at current_offset.
723 data_begin = max(0, current_offset - start_byte)
724 data_end = min(len(data),
725 current_offset + self.bytes_per_cluster - start_byte)
726 if data_end > data_begin:
727 cluster_file_offset = (self.data_start() + self.root_entries * 32 +
728 (current_cluster - 2) * self.bytes_per_cluster)
729 f.seek(cluster_file_offset + max(0, start_byte - current_offset))
730 f.write(data[data_begin:data_end])
Casey Dahlin29e2b212016-09-01 18:07:15 -0700731
Alex Deymoa1c97772016-09-19 14:32:53 -0700732 # Advance to the next cluster in the chain or get a new cluster if needed.
733 current_offset += self.bytes_per_cluster
734 if last_offset > current_offset:
735 f.seek(FAT_TABLE_START + current_cluster * 2)
736 next_cluster = read_le_short(f)
737 if next_cluster > MAX_CLUSTER_ID:
738 next_cluster = self.extend_cluster(current_cluster, len(data))
739 current_cluster = next_cluster
740 assert current_cluster > 0, "Cannot write free cluster"
Casey Dahlin29e2b212016-09-01 18:07:15 -0700741
Casey Dahlin29e2b212016-09-01 18:07:15 -0700742
743def add_item(directory, item):
744 """
745 Copy a file into the given FAT directory. If the path given is a directory,
746 copy recursively.
747 directory: fat_dir to copy the file in to
748 item: Path of local file to copy
749 """
750 if os.path.isdir(item):
751 base = os.path.basename(item)
752 if len(base) == 0:
753 base = os.path.basename(item[:-1])
754 sub = directory.new_subdirectory(base)
Alex Deymo567c5d02016-09-23 13:12:33 -0700755 for next_item in sorted(os.listdir(item)):
Casey Dahlin29e2b212016-09-01 18:07:15 -0700756 add_item(sub, os.path.join(item, next_item))
757 else:
758 with open(item, 'rb') as f:
759 directory.new_file(os.path.basename(item), f)
760
761if __name__ == "__main__":
762 if len(sys.argv) < 3:
763 print("Usage: fat16copy.py <image> <file> [<file> ...]")
764 print("Files are copied into the root of the image.")
765 print("Directories are copied recursively")
766 sys.exit(1)
767
768 root = fat(sys.argv[1]).root
769
770 for p in sys.argv[2:]:
771 add_item(root, p)