Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pyfatfs/PyFat.py: 18%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
4"""FAT and BPB parsing for files."""
6import datetime
7import errno
9import math
10import struct
11import threading
12import time
13import warnings
15from contextlib import contextmanager
16from io import FileIO, open, BytesIO, IOBase, SEEK_END
17from os import PathLike
18from typing import Union
20from pyfatfs import FAT_OEM_ENCODING, _init_check
21from pyfatfs.EightDotThree import EightDotThree
22from pyfatfs.FATDirectoryEntry import FATDirectoryEntry, FATLongDirectoryEntry
23from pyfatfs.FSInfo import FSInfo
24from pyfatfs._exceptions import PyFATException, NotAFatEntryException
25from pyfatfs.BootSectorHeader import BootSectorHeader, FAT12BootSectorHeader, \
26 FAT32BootSectorHeader
29def _readonly_check(func):
30 def _wrapper(*args, **kwargs):
31 read_only = args[0].is_read_only
33 if read_only is False:
34 return func(*args, **kwargs)
35 else:
36 raise PyFATException("Filesystem has been opened read-only, not "
37 "able to perform a write operation!",
38 errno=errno.EROFS)
40 return _wrapper
43class PyFat(object):
44 """PyFAT base class, parses generic filesystem information."""
46 #: Used as fat_type if unable to detect FAT type
47 FAT_TYPE_UNKNOWN = 0
48 #: Used as fat_type if FAT12 fs has been detected
49 FAT_TYPE_FAT12 = 12
50 #: Used as fat_type if FAT16 fs has been detected
51 FAT_TYPE_FAT16 = 16
52 #: Used as fat_type if FAT32 fs has been detected
53 FAT_TYPE_FAT32 = 32
55 #: Maps fat_type to BS_FilSysType from FS header information
56 FS_TYPES = {FAT_TYPE_UNKNOWN: b"FAT ",
57 FAT_TYPE_FAT12: b"FAT12 ",
58 FAT_TYPE_FAT16: b"FAT16 ",
59 FAT_TYPE_FAT32: b"FAT32 "}
61 #: Possible cluster values for FAT12 partitions
62 FAT12_CLUSTER_VALUES = {'FREE_CLUSTER': 0x000,
63 'MIN_DATA_CLUSTER': 0x002,
64 'MAX_DATA_CLUSTER': 0xFEF,
65 'BAD_CLUSTER': 0xFF7,
66 'END_OF_CLUSTER_MIN': 0xFF8,
67 'END_OF_CLUSTER_MAX': 0xFFF}
68 FAT12_SPECIAL_EOC = 0xFF0
69 #: Possible cluster values for FAT16 partitions
70 FAT16_CLUSTER_VALUES = {'FREE_CLUSTER': 0x0000,
71 'MIN_DATA_CLUSTER': 0x0002,
72 'MAX_DATA_CLUSTER': 0xFFEF,
73 'BAD_CLUSTER': 0xFFF7,
74 'END_OF_CLUSTER_MIN': 0xFFF8,
75 'END_OF_CLUSTER_MAX': 0xFFFF}
76 #: Possible cluster values for FAT32 partitions
77 FAT32_CLUSTER_VALUES = {'FREE_CLUSTER': 0x0000000,
78 'MIN_DATA_CLUSTER': 0x0000002,
79 'MAX_DATA_CLUSTER': 0x0FFFFFEF,
80 'BAD_CLUSTER': 0xFFFFFF7,
81 'END_OF_CLUSTER_MIN': 0xFFFFFF8,
82 'END_OF_CLUSTER_MAX': 0xFFFFFFF}
83 #: Maps fat_type to possible cluster values
84 FAT_CLUSTER_VALUES = {FAT_TYPE_FAT12: FAT12_CLUSTER_VALUES,
85 FAT_TYPE_FAT16: FAT16_CLUSTER_VALUES,
86 FAT_TYPE_FAT32: FAT32_CLUSTER_VALUES}
88 #: FAT16 bit mask for clean shutdown bit
89 FAT16_CLEAN_SHUTDOWN_BIT_MASK = 0x8000
90 #: FAT16 bit mask for volume error bit
91 FAT16_DRIVE_ERROR_BIT_MASK = 0x4000
92 #: FAT32 bit mask for clean shutdown bit
93 FAT32_CLEAN_SHUTDOWN_BIT_MASK = 0x8000000
94 #: FAT32 bit mask for volume error bit
95 FAT32_DRIVE_ERROR_BIT_MASK = 0x4000000
97 #: Dirty bit in FAT header
98 FAT_DIRTY_BIT_MASK = 0x01
100 def __init__(self,
101 encoding: str = 'ibm437',
102 offset: int = 0,
103 lazy_load: bool = True):
104 """Set up PyFat class instance.
106 :param encoding: Define encoding to use for filenames
107 :param offset: Offset of the FAT partition in the given file
108 :type encoding: str
109 :type offset: int
110 """
111 self.__fp: FileIO = None
112 self.__fp_offset = offset
113 self._fat_size = 0
114 self.bpb_header: BootSectorHeader = None
115 self.root_dir = None
116 self.root_dir_sector = 0
117 self.root_dir_sectors = 0
118 self.bytes_per_cluster = 0
119 self.first_data_sector = 0
120 self.first_free_cluster = 0
121 self.fat_type = self.FAT_TYPE_UNKNOWN
122 self.fat = {}
123 self.initialized = False
124 self.encoding = encoding
125 self.is_read_only = True
126 self.lazy_load = lazy_load
127 self.__lock = threading.Lock()
129 def __set_fp(self, fp: Union[IOBase, BytesIO]):
130 if self.__fp is not None:
131 raise PyFATException("Cannot overwrite existing file handle, "
132 "create new class instance of PyFAT.",
133 errno=errno.EMFILE)
134 self.__fp = fp
136 def __seek(self, address: int):
137 """Seek to given address with offset."""
138 if self.__fp is None:
139 raise PyFATException("Cannot seek without a file handle!",
140 errno=errno.ENXIO)
141 self.__fp.seek(address + self.__fp_offset)
143 @_init_check
144 def read_cluster_contents(self, cluster: int) -> bytes:
145 """Read contents of given cluster.
147 :param cluster: Cluster number to read contents from
148 :returns: Contents of cluster as `bytes`
149 """
150 sz = self.bytes_per_cluster
151 cluster_address = self.get_data_cluster_address(cluster)
152 with self.__lock:
153 self.__seek(cluster_address)
154 return self.__fp.read(sz)
156 def __get_clean_shutdown_bitmask(self):
157 """Get clean shutdown bitmask for current FS.
159 :raises: AttributeError
160 """
161 return getattr(self, f"FAT{self.fat_type}_CLEAN_SHUTDOWN_BIT_MASK")
163 def _is_dirty(self) -> bool:
164 """Check whether or not the partition currently is dirty."""
165 try:
166 clean_shutdown_bitmask = self.__get_clean_shutdown_bitmask()
167 except AttributeError:
168 # Bit not set on FAT12
169 dos_dirty = False
170 else:
171 dos_dirty = (self.fat[1] &
172 clean_shutdown_bitmask) != clean_shutdown_bitmask
174 nt_dirty = (self.bpb_header["BS_Reserved1"] &
175 self.FAT_DIRTY_BIT_MASK) == self.FAT_DIRTY_BIT_MASK
177 return dos_dirty or nt_dirty
179 def _mark_dirty(self):
180 """Mark partition as not cleanly unmounted.
182 Apparently the dirty bit in FAT[1] is used by DOS,
183 while BS_Reserved1 is used by NT. Always set both.
184 """
185 try:
186 clean_shutdown_bitmask = self.__get_clean_shutdown_bitmask()
187 except AttributeError:
188 pass
189 else:
190 # Only applicable for FAT16/32
191 self.fat[1] = (self.fat[1] & ~clean_shutdown_bitmask) | \
192 (0 & clean_shutdown_bitmask)
193 self.flush_fat()
195 self.bpb_header["BS_Reserved1"] |= self.FAT_DIRTY_BIT_MASK
196 self._write_bpb_header()
198 def _mark_clean(self):
199 """Mark partition as cleanly unmounted."""
200 try:
201 clean_shutdown_bitmask = self.__get_clean_shutdown_bitmask()
202 except AttributeError:
203 pass
204 else:
205 self.fat[1] |= clean_shutdown_bitmask
206 self.flush_fat()
208 self.bpb_header["BS_Reserved1"] = (self.bpb_header["BS_Reserved1"]
209 & ~self.FAT_DIRTY_BIT_MASK) | \
210 (0 & self.FAT_DIRTY_BIT_MASK)
211 self._write_bpb_header()
213 def set_fp(self, fp: Union[BytesIO, IOBase]):
214 """Open a filesystem from a valid file pointer.
216 This allows using in-memory filesystems (e.g., BytesIO).
218 :param fp: `FileIO`: Valid `FileIO` object
219 """
220 if not fp.readable():
221 raise PyFATException("Cannot read data from file pointer.",
222 errno=errno.EACCES)
224 if not fp.seekable():
225 raise PyFATException("Cannot seek file object.",
226 errno=errno.EINVAL)
228 self.is_read_only = not fp.writable()
230 self.__set_fp(fp)
232 # Parse BPB & FAT headers of given file
233 self.parse_header()
235 # Parse FAT
236 self._parse_fat()
238 # Check for clean shutdown
239 if self._is_dirty():
240 warnings.warn("Filesystem was not cleanly unmounted on last "
241 "access. Check for data corruption.")
242 if not self.is_read_only:
243 self._mark_dirty()
245 # Parse root directory
246 self.parse_root_dir()
248 def open(self, filename: Union[str, PathLike], read_only: bool = False):
249 """Open filesystem for usage with PyFat.
251 :param filename: `str`: Name of file to open for usage with PyFat.
252 :param read_only: `bool`: Force read-only mode of filesystem.
253 """
254 self.is_read_only = read_only
255 if read_only is True:
256 mode = 'rb'
257 else:
258 mode = 'rb+'
260 try:
261 return self.set_fp(open(filename, mode=mode))
262 except OSError as ex:
263 raise PyFATException(f"Cannot open given file \'{filename}\'.",
264 errno=ex.errno)
266 @_init_check
267 def get_fs_location(self):
268 """Retrieve path of opened filesystem."""
269 return self.__fp.name
271 def _get_total_sectors(self):
272 """Get total number of sectors for all FAT sizes."""
273 if self.bpb_header["BPB_TotSec16"] != 0:
274 return self.bpb_header["BPB_TotSec16"]
276 return self.bpb_header["BPB_TotSec32"]
278 def _get_fat_size_count(self):
279 """Get BPB_FATsz value."""
280 if self.bpb_header["BPB_FATSz16"] != 0:
281 return self.bpb_header["BPB_FATSz16"]
283 try:
284 return self.bpb_header["BPB_FATSz32"]
285 except KeyError:
286 raise PyFATException("Invalid FAT size of 0 detected in header, "
287 "cannot continue")
289 @_init_check
290 def _parse_fat(self):
291 """Parse information in FAT."""
292 # Read all FATs
293 fat_size = self.bpb_header["BPB_BytsPerSec"]
294 fat_size *= self._fat_size
296 # Seek FAT entries
297 first_fat_bytes = self.bpb_header["BPB_RsvdSecCnt"]
298 first_fat_bytes *= self.bpb_header["BPB_BytsPerSec"]
299 fats = []
300 for i in range(self.bpb_header["BPB_NumFATs"]):
301 with self.__lock:
302 self.__seek(first_fat_bytes + (i * fat_size))
303 fats += [self.__fp.read(fat_size)]
305 if len(fats) < 1:
306 raise PyFATException("Invalid number of FATs configured, "
307 "cannot continue")
308 elif len(set(fats)) > 1:
309 warnings.warn("One or more FATs differ, filesystem most "
310 "likely corrupted. Using first FAT.")
312 # Parse first FAT
313 self.bytes_per_cluster = self.bpb_header["BPB_BytsPerSec"] * \
314 self.bpb_header["BPB_SecPerClus"]
316 if len(fats[0]) != self.bpb_header["BPB_BytsPerSec"] * self._fat_size:
317 raise PyFATException("Invalid length of FAT")
319 # FAT12: 12 bits (1.5 bytes) per FAT entry
320 # FAT16: 16 bits (2 bytes) per FAT entry
321 # FAT32: 32 bits (4 bytes) per FAT entry
322 fat_entry_size = self.fat_type / 8
323 total_entries = int(fat_size // fat_entry_size)
324 self.fat = [None] * total_entries
326 curr = 0
327 cluster = 0
328 incr = self.fat_type / 8
329 while curr < fat_size:
330 offset = curr + incr
332 if self.fat_type == self.FAT_TYPE_FAT12:
333 fat_nibble = fats[0][int(curr):math.ceil(offset)]
334 fat_nibble = fat_nibble.ljust(2, b"\0")
335 try:
336 self.fat[cluster] = struct.unpack("<H", fat_nibble)[0]
337 except IndexError:
338 # Out of bounds, FAT size is not cleanly divisible by 3
339 # Do not touch last clusters
340 break
342 if cluster % 2 == 0:
343 # Even: Keep low 12-bits of word
344 self.fat[cluster] &= 0x0FFF
345 else:
346 # Odd: Keep high 12-bits of word
347 self.fat[cluster] >>= 4
349 if math.ceil(offset) == (fat_size - 1):
350 # Sector boundary case for FAT12
351 del self.fat[-1]
352 break
354 elif self.fat_type == self.FAT_TYPE_FAT16:
355 self.fat[cluster] = struct.unpack("<H",
356 fats[0][int(curr):
357 int(offset)])[0]
358 elif self.fat_type == self.FAT_TYPE_FAT32:
359 self.fat[cluster] = struct.unpack("<L",
360 fats[0][int(curr):
361 int(offset)])[0]
362 # Ignore first four bits, FAT32 clusters are
363 # actually just 28bits long
364 self.fat[cluster] &= 0x0FFFFFFF
365 else:
366 raise PyFATException("Unknown FAT type, cannot continue")
368 curr += incr
369 cluster += 1
371 if None in self.fat:
372 raise AssertionError("Unknown error during FAT parsing, please "
373 "report this error.")
375 @_init_check
376 def __bytes__(self):
377 """Represent current state of FAT as bytes.
379 :returns: `bytes` representation of FAT.
380 """
381 b = b''
382 if self.fat_type == self.FAT_TYPE_FAT12:
383 for i, e in enumerate(self.fat):
384 if i % 2 == 0:
385 b += struct.pack("<H", e)
386 else:
387 nibble = b[-1:]
388 nibble = struct.unpack("<B", nibble)[0]
389 b = b[:-1]
390 b += struct.pack("<BB", ((e & 0xF) << 4) | nibble, e >> 4)
392 else:
393 if self.fat_type == self.FAT_TYPE_FAT16:
394 fmt = "H"
395 else:
396 # FAT32
397 fmt = "L"
399 b = struct.pack(f"<{fmt * len(self.fat)}",
400 *self.fat)
401 return b
403 @_init_check
404 @_readonly_check
405 def _write_data_to_address(self, data: bytes,
406 address: int):
407 """Write given data directly to the filesystem.
409 Directly writes to the filesystem without any consistency check.
410 **Use with caution**
412 :param data: `bytes`: Data to write to address
413 :param address: `int`: Offset to write data to.
414 """
415 with self.__lock:
416 self.__seek(address)
417 self.__fp.write(data)
419 @_init_check
420 @_readonly_check
421 def free_cluster_chain(self, cluster: int):
422 """Mark a cluster(chain) as free in FAT.
424 :param cluster: `int`: Cluster to mark as free
425 """
426 _freeclus = self.FAT_CLUSTER_VALUES[self.fat_type]['FREE_CLUSTER']
427 with self.__lock:
428 tmp_fat = self.fat.copy()
429 for cl in self.get_cluster_chain(cluster):
430 tmp_fat[cl] = _freeclus
431 self.first_free_cluster = min(cl, self.first_free_cluster)
432 self.fat = tmp_fat
434 @_init_check
435 @_readonly_check
436 def write_data_to_cluster(self, data: bytes,
437 cluster: int,
438 extend_cluster: bool = True,
439 erase: bool = False) -> None:
440 """Write given data to cluster.
442 Extends cluster chain if needed.
444 :param data: `bytes`: Data to write to cluster
445 :param cluster: `int`: Cluster to write data to.
446 :param extend_cluster: `bool`: Automatically extend cluster chain
447 if not enough space is available.
448 :param erase: `bool`: Erase cluster contents before writing.
449 This is useful when writing `FATDirectoryEntry` data.
450 """
451 data_sz = len(data)
452 cluster_sz = 0
453 last_cluster = None
454 for c in self.get_cluster_chain(cluster):
455 cluster_sz += self.bytes_per_cluster
456 last_cluster = c
457 if cluster_sz >= data_sz:
458 break
460 if data_sz > cluster_sz:
461 if extend_cluster is False:
462 raise PyFATException("Cannot write data to cluster, "
463 "not enough space available.",
464 errno=errno.ENOSPC)
466 new_chain = self.allocate_bytes(data_sz - cluster_sz,
467 erase=erase)[0]
468 self.fat[last_cluster] = new_chain
470 # Fill rest of data with zeroes if erase is set to True
471 if erase:
472 new_sz = max(1, math.ceil(data_sz / self.bytes_per_cluster))
473 new_sz *= self.bytes_per_cluster
474 data += b'\0' * (new_sz - data_sz)
476 # Write actual data
477 bytes_written = 0
478 for c in self.get_cluster_chain(cluster):
479 b = self.get_data_cluster_address(c)
480 t = bytes_written
481 bytes_written += self.bytes_per_cluster
482 self._write_data_to_address(data[t:bytes_written], b)
483 if bytes_written >= len(data):
484 break
486 @_init_check
487 @_readonly_check
488 def flush_fat(self) -> None:
489 """Flush FAT(s) to disk."""
490 fat_size = self.bpb_header["BPB_BytsPerSec"]
491 fat_size *= self._fat_size
493 first_fat_bytes = self.bpb_header["BPB_RsvdSecCnt"]
494 first_fat_bytes *= self.bpb_header["BPB_BytsPerSec"]
496 with self.__lock:
497 binary_fat = bytes(self)
498 for i in range(self.bpb_header["BPB_NumFATs"]):
499 self.__seek(first_fat_bytes + (i * fat_size))
500 self.__fp.write(binary_fat)
502 def calc_num_clusters(self, size: int = 0) -> int:
503 """Calculate the number of required clusters.
505 :param size: `int`: required bytes to allocate
506 :returns: Number of required clusters
507 """
508 num_clusters = size / self.bytes_per_cluster
509 num_clusters = math.ceil(num_clusters)
511 return num_clusters
513 @_init_check
514 @_readonly_check
515 def allocate_bytes(self, size: int, erase: bool = False) -> list:
516 """Try to allocate a cluster (-chain) in FAT for `size` bytes.
518 :param size: `int`: Size in bytes to try to allocate.
519 :param erase: `bool`: If set to true, the newly allocated
520 space is zeroed-out for clean allocation.
521 :returns: List of newly-allocated clusters.
522 """
523 free_clus = self.FAT_CLUSTER_VALUES[self.fat_type]["FREE_CLUSTER"]
524 min_clus = self.FAT_CLUSTER_VALUES[self.fat_type]["MIN_DATA_CLUSTER"]
525 max_clus = self.FAT_CLUSTER_VALUES[self.fat_type]["MAX_DATA_CLUSTER"]
526 num_clusters = self.calc_num_clusters(size)
528 # Fill list of found free clusters
529 free_clusters = []
530 for i in range(self.first_free_cluster, len(self.fat)):
531 if min_clus > i or i > max_clus:
532 # Ignore out of bound entries
533 continue
535 if num_clusters == len(free_clusters):
536 # Allocated enough clusters!
537 break
539 if self.fat[i] == free_clus:
540 if i == self.FAT_CLUSTER_VALUES[self.fat_type]["BAD_CLUSTER"]:
541 # Do not allocate a BAD_CLUSTER
542 continue
544 if self.fat_type == self.FAT_TYPE_FAT12 and \
545 i == self.FAT12_SPECIAL_EOC:
546 # Do not allocate special EOC marker on FAT12
547 continue
549 free_clusters += [i]
550 else:
551 free_space = len(free_clusters) * self.bytes_per_cluster
552 raise PyFATException(f"Not enough free space to allocate "
553 f"{size} bytes ({free_space} bytes free)",
554 errno=errno.ENOSPC)
555 self.first_free_cluster = i
557 # Allocate cluster chain in FAT
558 eoc_max = self.FAT_CLUSTER_VALUES[self.fat_type]["END_OF_CLUSTER_MAX"]
559 for i, _ in enumerate(free_clusters):
560 try:
561 self.fat[free_clusters[i]] = free_clusters[i+1]
562 except IndexError:
563 self.fat[free_clusters[i]] = eoc_max
565 if erase is True:
566 with self.__lock:
567 self.__seek(self.get_data_cluster_address(
568 free_clusters[i]))
569 self.__fp.write(b'\0' * self.bytes_per_cluster)
571 return free_clusters
573 @_init_check
574 @_readonly_check
575 def update_directory_entry(self, dir_entry: FATDirectoryEntry) -> None:
576 """Update directory entry on disk.
578 Special handling is required, since the root directory
579 on FAT12/16 is on a fixed location on disk.
581 :param dir_entry: `FATDirectoryEntry`: Directory to write to disk
582 """
583 is_root_dir = False
584 extend_cluster_chain = True
585 if self.root_dir == dir_entry:
586 if self.fat_type != self.FAT_TYPE_FAT32:
587 # FAT12/16 doesn't have a root directory cluster,
588 # which cannot be enhanced
589 extend_cluster_chain = False
590 is_root_dir = True
592 # Gather all directory entries
593 dir_entries = b''
594 for d in dir_entry._get_entries_raw():
595 dir_entries += bytes(d)
597 # Write content
598 if not is_root_dir or self.fat_type == self.FAT_TYPE_FAT32:
599 # FAT32 and non-root dir entries can be handled normally
600 self.write_data_to_cluster(dir_entries,
601 dir_entry.get_cluster(),
602 extend_cluster=extend_cluster_chain,
603 erase=True)
604 else:
605 # FAT12/16 does not have a root directory cluster
606 root_dir_addr = self.root_dir_sector * \
607 self.bpb_header["BPB_BytsPerSec"]
608 root_dir_sz = self.root_dir_sectors * \
609 self.bpb_header["BPB_BytsPerSec"]
611 if len(dir_entries) > root_dir_sz:
612 raise PyFATException("Cannot create directory, maximum number "
613 "of root directory entries exhausted!",
614 errno=errno.ENOSPC)
616 # Overwrite empty space as well
617 dir_entries += b'\0' * (root_dir_sz - len(dir_entries))
618 self._write_data_to_address(dir_entries, root_dir_addr)
620 def _fat12_parse_root_dir(self):
621 """Parse FAT12/16 root dir entries.
623 FAT12/16 has a fixed location of root directory entries
624 and is therefore size limited (BPB_RootEntCnt).
625 """
626 root_dir_byte = self.root_dir_sector * \
627 self.bpb_header["BPB_BytsPerSec"]
628 self.root_dir.set_cluster(self.root_dir_sector //
629 self.bpb_header["BPB_SecPerClus"])
630 max_bytes = self.bpb_header["BPB_RootEntCnt"] * \
631 FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE
633 # Parse all directory entries in root directory
634 subdirs, _ = self.parse_dir_entries_in_address(root_dir_byte,
635 root_dir_byte +
636 max_bytes)
637 for dir_entry in subdirs:
638 self.root_dir.add_subdirectory(dir_entry)
640 def _fat32_parse_root_dir(self):
641 """Parse FAT32 root dir entries.
643 FAT32 actually has its root directory entries distributed
644 across a cluster chain that we need to follow
645 """
646 root_cluster = self.bpb_header["BPB_RootClus"]
647 self.root_dir.set_cluster(root_cluster)
649 # Follow root directory cluster chain
650 for dir_entry in self.parse_dir_entries_in_cluster_chain(root_cluster):
651 self.root_dir.add_subdirectory(dir_entry,
652 recursive=not self.lazy_load)
654 def parse_root_dir(self):
655 """Parse root directory entry."""
656 root_dir_sfn = EightDotThree()
657 root_dir_sfn.set_str_name("")
658 dir_attr = FATDirectoryEntry.ATTR_DIRECTORY
659 self.root_dir = FATDirectoryEntry(fs=self,
660 DIR_Name=root_dir_sfn,
661 DIR_Attr=dir_attr,
662 DIR_NTRes=0,
663 DIR_CrtTimeTenth=0,
664 DIR_CrtTime=0,
665 DIR_CrtDate=0,
666 DIR_LstAccessDate=0,
667 DIR_FstClusHI=0,
668 DIR_WrtTime=0,
669 DIR_WrtDate=0,
670 DIR_FstClusLO=0,
671 DIR_FileSize=0,
672 encoding=self.encoding)
674 if self.fat_type in [self.FAT_TYPE_FAT12, self.FAT_TYPE_FAT16]:
675 self._fat12_parse_root_dir()
676 else:
677 self._fat32_parse_root_dir()
679 def parse_lfn_entry(self,
680 lfn_entry: FATLongDirectoryEntry = None,
681 address: int = 0):
682 """Parse LFN entry at given address."""
683 dir_hdr_sz = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE
685 with self.__lock:
686 self.__seek(address)
687 lfn_dir_data = self.__fp.read(dir_hdr_sz)
689 lfn_hdr_layout = FATLongDirectoryEntry.FAT_LONG_DIRECTORY_LAYOUT
690 lfn_dir_hdr = struct.unpack(lfn_hdr_layout, lfn_dir_data)
691 lfn_dir_hdr = dict(zip(FATLongDirectoryEntry.FAT_LONG_DIRECTORY_VARS,
692 lfn_dir_hdr))
694 lfn_entry.add_lfn_entry(**lfn_dir_hdr)
696 def __parse_dir_entry(self, address):
697 """Parse directory entry at given address."""
698 with self.__lock:
699 self.__seek(address)
700 dir_hdr_size = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE
701 dir_data = self.__fp.read(dir_hdr_size)
703 dir_hdr = struct.unpack(FATDirectoryEntry.FAT_DIRECTORY_LAYOUT,
704 dir_data)
705 dir_hdr = dict(zip(FATDirectoryEntry.FAT_DIRECTORY_VARS, dir_hdr))
706 return dir_hdr
708 def parse_dir_entries_in_address(self,
709 address: int = 0,
710 max_address: int = 0,
711 tmp_lfn_entry: FATLongDirectoryEntry =
712 None):
713 """Parse directory entries in address range."""
714 if tmp_lfn_entry is None:
715 tmp_lfn_entry = FATLongDirectoryEntry()
717 dir_hdr_size = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE
719 if max_address == 0:
720 max_address = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE
722 dir_entries = []
724 for hdr_addr in range(address, max_address, dir_hdr_size):
725 # Parse each entry
726 dir_hdr = self.__parse_dir_entry(hdr_addr)
727 dir_sn = EightDotThree(encoding=self.encoding)
728 dir_first_byte = dir_hdr["DIR_Name"][0]
729 try:
730 dir_sn.set_byte_name(dir_hdr["DIR_Name"])
731 except NotAFatEntryException as ex:
732 # Not a directory of any kind, invalidate temporary LFN entries
733 tmp_lfn_entry = FATLongDirectoryEntry()
734 if ex.free_type == FATDirectoryEntry.FREE_DIR_ENTRY_MARK:
735 # Empty directory entry,
736 continue
737 elif ex.free_type == FATDirectoryEntry.LAST_DIR_ENTRY_MARK:
738 # Last directory entry, do not parse any further
739 break
740 else:
741 dir_hdr["DIR_Name"] = dir_sn
743 # Long File Names
744 if FATLongDirectoryEntry.is_lfn_entry(dir_first_byte,
745 dir_hdr["DIR_Attr"]):
746 self.parse_lfn_entry(tmp_lfn_entry, hdr_addr)
747 continue
749 # Normal directory entries
750 if not tmp_lfn_entry.is_lfn_entry_complete():
751 # Ignore incomplete LFN entries altogether
752 tmp_lfn_entry = None
754 dir_entry = FATDirectoryEntry(fs=self,
755 encoding=self.encoding,
756 lfn_entry=tmp_lfn_entry,
757 lazy_load=self.lazy_load,
758 **dir_hdr)
759 dir_entries += [dir_entry]
761 if not self.lazy_load:
762 if dir_entry.is_directory() and not dir_entry.is_special():
763 # Iterate all subdirectories except for dot and dotdot
764 cluster = dir_entry.get_cluster()
765 subdirs = self.parse_dir_entries_in_cluster_chain(cluster)
766 for d in subdirs:
767 dir_entry.add_subdirectory(d)
769 # Reset temporary LFN entry
770 tmp_lfn_entry = FATLongDirectoryEntry()
772 return dir_entries, tmp_lfn_entry
774 def parse_dir_entries_in_cluster_chain(self, cluster) -> list:
775 """Parse directory entries while following given cluster chain."""
776 dir_entries = []
777 tmp_lfn_entry = FATLongDirectoryEntry()
778 max_bytes = (self.bpb_header["BPB_SecPerClus"] *
779 self.bpb_header["BPB_BytsPerSec"])
780 for c in self.get_cluster_chain(cluster):
781 # Parse all directory entries in chain
782 b = self.get_data_cluster_address(c)
783 ret = self.parse_dir_entries_in_address(b, b+max_bytes,
784 tmp_lfn_entry)
785 tmp_dir_entries, tmp_lfn_entry = ret
786 dir_entries += tmp_dir_entries
788 return dir_entries
790 def get_data_cluster_address(self, cluster: int) -> int:
791 """Get offset of given cluster in bytes.
793 :param cluster: Cluster number as `int`
794 :returns: Bytes address location of cluster
795 """
796 # First two cluster entries are reserved
797 sector = (cluster - 2) * self.bpb_header["BPB_SecPerClus"] + \
798 self.first_data_sector
799 return sector * self.bpb_header["BPB_BytsPerSec"]
801 @_init_check
802 def get_cluster_chain(self, first_cluster):
803 """Follow a cluster chain beginning with the first cluster address."""
804 cluster_vals = self.FAT_CLUSTER_VALUES[self.fat_type]
805 min_data_cluster = cluster_vals["MIN_DATA_CLUSTER"]
806 max_data_cluster = cluster_vals["MAX_DATA_CLUSTER"]
807 eoc_min = cluster_vals["END_OF_CLUSTER_MIN"]
808 eoc_max = cluster_vals["END_OF_CLUSTER_MAX"]
810 i = first_cluster
811 while i <= len(self.fat):
812 if min_data_cluster <= self.fat[i] <= max_data_cluster:
813 # Normal data cluster, follow chain
814 yield i
815 elif self.fat_type == self.FAT_TYPE_FAT12 and \
816 self.fat[i] == self.FAT12_SPECIAL_EOC:
817 # Special EOC
818 yield i
819 return
820 elif eoc_min <= self.fat[i] <= eoc_max:
821 # End of cluster, end chain
822 yield i
823 return
824 elif self.fat[i] == cluster_vals["BAD_CLUSTER"]:
825 # Bad cluster, cannot follow chain, file broken!
826 raise PyFATException("Bad cluster found in FAT cluster "
827 "chain, cannot access file")
828 elif self.fat[i] == cluster_vals["FREE_CLUSTER"]:
829 # FREE_CLUSTER mark when following a chain is treated an error
830 raise PyFATException("FREE_CLUSTER mark found in FAT cluster "
831 "chain, cannot access file")
832 else:
833 raise PyFATException("Invalid or unknown FAT cluster "
834 "entry found with value "
835 "\'{}\'".format(hex(self.fat[i])))
837 i = self.fat[i]
839 @_init_check
840 def close(self):
841 """Close session and free up all handles."""
842 if not self.is_read_only:
843 self._mark_clean()
845 self.__fp.close()
846 self.initialized = False
848 def __del__(self):
849 """Try to close open handles."""
850 try:
851 self.close()
852 except PyFATException:
853 pass
855 def __determine_fat_type(self) -> Union["PyFat.FAT_TYPE_FAT12",
856 "PyFat.FAT_TYPE_FAT16",
857 "PyFat.FAT_TYPE_FAT32"]:
858 """Determine FAT type.
860 An internal method to determine whether this volume is FAT12,
861 FAT16 or FAT32.
863 returns: `str`: Any of PyFat.FAT_TYPE_FAT12, PyFat.FAT_TYPE_FAT16
864 or PyFat.FAT_TYPE_FAT32
865 """
866 total_sectors = self._get_total_sectors()
867 rsvd_sectors = self.bpb_header["BPB_RsvdSecCnt"]
868 fat_sz = self.bpb_header["BPB_NumFATs"] * self._fat_size
869 root_dir_sectors = self.root_dir_sectors
870 data_sec = total_sectors - (rsvd_sectors + fat_sz + root_dir_sectors)
871 count_of_clusters = data_sec // self.bpb_header["BPB_SecPerClus"]
873 if count_of_clusters < 4085:
874 msft_fat_type = self.FAT_TYPE_FAT12
875 elif count_of_clusters < 65525:
876 msft_fat_type = self.FAT_TYPE_FAT16
877 else:
878 msft_fat_type = self.FAT_TYPE_FAT32
880 if self.bpb_header["BPB_FATSz16"] == 0:
881 if self.bpb_header["BPB_FATSz32"] != 0:
882 linux_fat_type = self.FAT_TYPE_FAT32
883 else:
884 linux_fat_type = msft_fat_type
885 elif count_of_clusters >= 4085:
886 linux_fat_type = self.FAT_TYPE_FAT16
887 else:
888 linux_fat_type = self.FAT_TYPE_FAT12
890 if msft_fat_type != linux_fat_type:
891 warnings.warn(f"Unable to reliably determine FAT type, "
892 f"guessing either FAT{msft_fat_type} or "
893 f"FAT{linux_fat_type}. Opting for "
894 f"FAT{linux_fat_type}.")
895 return linux_fat_type
897 @_readonly_check
898 def _write_bpb_header(self):
899 with self.__lock:
900 self.__seek(0)
901 self.__fp.write(bytes(self.bpb_header))
902 self.__seek(510)
903 self.__fp.write(b'\x55\xAA')
905 if self.fat_type == PyFat.FAT_TYPE_FAT32:
906 # write backup
907 backup_offset = self.bpb_header["BPB_BkBootSec"] * \
908 self.bpb_header["BPB_BytsPerSec"]
909 self.__seek(backup_offset)
910 self.__fp.write(bytes(self.bpb_header))
911 self.__seek(510 + backup_offset)
912 self.__fp.write(b'\x55\xAA')
914 def parse_header(self):
915 """Parse BPB & FAT headers in opened file."""
916 with self.__lock:
917 self.__seek(0)
918 boot_sector = self.__fp.read(512)
920 self.bpb_header = BootSectorHeader()
921 self.bpb_header.parse_header(boot_sector[:36])
923 # Verify BPB headers
924 self.__verify_bpb_header()
926 # Parse FAT type specific header
927 self.bpb_header = FAT12BootSectorHeader() \
928 if self.bpb_header["BPB_FATSz16"] > 0 else FAT32BootSectorHeader()
929 self.bpb_header.parse_header(boot_sector)
931 # Determine FAT type
932 self._fat_size = self._get_fat_size_count()
933 self.fat_type = self.__determine_fat_type()
935 # Calculate root directory sectors and starting point of root directory
936 root_entries = self.bpb_header["BPB_RootEntCnt"]
937 hdr_size = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE
938 bytes_per_sec = self.bpb_header["BPB_BytsPerSec"]
939 rsvd_secs = self.bpb_header["BPB_RsvdSecCnt"]
940 num_fats = self.bpb_header["BPB_NumFATs"]
942 self.root_dir_sectors = ((root_entries * hdr_size) +
943 (bytes_per_sec - 1)) // bytes_per_sec
944 self.root_dir_sector = rsvd_secs + (self._fat_size * num_fats)
946 # Calculate first data sector
947 self.first_data_sector = (rsvd_secs + (num_fats * self._fat_size) +
948 self.root_dir_sectors)
950 # Check signature
951 with self.__lock:
952 self.__seek(510)
953 signature = struct.unpack("<H", self.__fp.read(2))[0]
955 if signature != 0xAA55:
956 raise PyFATException(f"Invalid signature: \'{hex(signature)}\'.")
958 # Initialization finished
959 self.initialized = True
961 def __verify_bpb_header(self):
962 """Verify BPB header for correctness."""
963 if self.bpb_header["BS_jmpBoot"][0] == 0xEB:
964 if self.bpb_header["BS_jmpBoot"][2] != 0x90:
965 raise PyFATException("Boot code must end with 0x90")
966 elif self.bpb_header["BS_jmpBoot"][0] == 0xE9:
967 pass
968 else:
969 raise PyFATException("Boot code must start with 0xEB or "
970 "0xE9. Is this a FAT partition?")
972 #: 512,1024,2048,4096: As per fatgen103.doc
973 byts_per_sec_range = [2**x for x in range(9, 13)]
974 if self.bpb_header["BPB_BytsPerSec"] not in byts_per_sec_range:
975 raise PyFATException(f"Expected one of {byts_per_sec_range} "
976 f"bytes per sector, got: "
977 f"\'{self.bpb_header['BPB_BytsPerSec']}\'.")
979 #: 1,2,4,8,16,32,64,128: As per fatgen103.doc
980 sec_per_clus_range = [2**x for x in range(8)]
981 if self.bpb_header["BPB_SecPerClus"] not in sec_per_clus_range:
982 raise PyFATException(f"Expected one of {sec_per_clus_range} "
983 f"sectors per cluster, got: "
984 f"\'{self.bpb_header['BPB_SecPerClus']}\'.")
986 bytes_per_cluster = self.bpb_header["BPB_BytsPerSec"]
987 bytes_per_cluster *= self.bpb_header["BPB_SecPerClus"]
988 if bytes_per_cluster > 32768:
989 warnings.warn("Bytes per cluster should not be more than 32K, "
990 "but got: {}K. Trying to continue "
991 "anyway.".format(bytes_per_cluster // 1024), Warning)
993 if self.bpb_header["BPB_RsvdSecCnt"] == 0:
994 raise PyFATException("Number of reserved sectors must not be 0")
996 if self.bpb_header["BPB_Media"] not in [0xf0, 0xf8, 0xf9, 0xfa, 0xfb,
997 0xfc, 0xfd, 0xfe, 0xff]:
998 raise PyFATException("Invalid media type")
1000 if self.bpb_header["BPB_NumFATs"] < 1:
1001 raise PyFATException("At least one FAT expected, None found.")
1003 root_entry_count = self.bpb_header["BPB_RootEntCnt"] * 32
1004 root_entry_count %= self.bpb_header["BPB_BytsPerSec"]
1005 if self.bpb_header["BPB_RootEntCnt"] != 0 and root_entry_count != 0:
1006 raise PyFATException("Root entry count does not cleanly align with"
1007 " bytes per sector!")
1009 if self.bpb_header["BPB_TotSec16"] == 0 and \
1010 self.bpb_header["BPB_TotSec32"] == 0:
1011 raise PyFATException("16-Bit and 32-Bit total sector count "
1012 "value empty.")
1014 @staticmethod
1015 @contextmanager
1016 def open_fs(filename: str, offset: int = 0,
1017 encoding=FAT_OEM_ENCODING):
1018 """Context manager for direct use of PyFAT."""
1019 pf = PyFat(encoding=encoding, offset=offset)
1020 pf.open(filename)
1021 yield pf
1022 pf.close()
1024 def mkfs(self, filename: str,
1025 fat_type: Union["PyFat.FAT_TYPE_FAT12",
1026 "PyFat.FAT_TYPE_FAT16",
1027 "PyFat.FAT_TYPE_FAT32"],
1028 size: int = None,
1029 sector_size: int = 512,
1030 number_of_fats: int = 2,
1031 label: str = "NO NAME",
1032 volume_id: int = None,
1033 media_type: int = 0xF8):
1034 """Create a new FAT filesystem.
1036 :param filename: `str`: Name of file to create filesystem in
1037 :param fat_type: `FAT_TYPE_FAT{12,16,32}`: FAT type
1038 :param size: `int`: Size of new filesystem in bytes
1039 :param sector_size: `int`: Size of a sector in bytes
1040 :param number_of_fats: `int`: Number of FATs on the disk
1041 :param label: `str`: Volume label
1042 :param volume_id: `bytes`: Volume id (4 bytes)
1043 :param media_type: `int`: Media type (0xF{0,8-F})
1044 """
1045 self.initialized = True
1046 self.is_read_only = False
1048 if fat_type not in [PyFat.FAT_TYPE_FAT12, PyFat.FAT_TYPE_FAT16,
1049 PyFat.FAT_TYPE_FAT32]:
1050 raise PyFATException("Unsupported FAT type given.")
1052 self.fat_type = fat_type
1053 self.__set_fp(open(filename, mode='rb+'))
1055 if size is None:
1056 try:
1057 size = self.__fp.seek(-self.__fp_offset, SEEK_END)
1058 except OSError:
1059 raise PyFATException("Unable to determine partition size.",
1060 errno=errno.EFBIG)
1061 self.__fp.seek(0)
1063 try:
1064 self.__fp.truncate(size + self.__fp_offset)
1065 except OSError:
1066 raise PyFATException("Failed to truncate file to given size. "
1067 "Most likely the file can't be extended.",
1068 errno=errno.EFBIG)
1070 if sector_size < 512:
1071 raise PyFATException("Sector size cannot be less than 512.")
1072 elif sector_size % 2 != 0:
1073 raise PyFATException("Sector size must be a power of two.")
1075 if not volume_id:
1076 # generate random but valid volume id
1077 tm = time.localtime()
1078 cdate = ((tm[0]-1980) << 9) | (tm[1] << 5) | (tm[2])
1079 ctime = (tm[3] << 11) | (tm[4] << 5) | (tm[5]//2)
1080 volume_id = cdate << 16 | ctime
1082 num_sec = math.ceil(size / sector_size)
1083 num_sec_to_sec_per_clus = {
1084 PyFat.FAT_TYPE_FAT32: [
1085 (66600, 0), # disks up to 32.5 MB, error
1086 (532480, 1), # disks up to 260 MB, .5k cluster
1087 (16777216, 8), # disks up to 8 GB, 4 k cluster
1088 (33554432, 16), # disks up to 16 GB, 8 k cluster
1089 (67108864, 32) # disks up to 32 GB, 16 k cluster
1090 ],
1091 PyFat.FAT_TYPE_FAT16: [
1092 (8400, 0), # disks up to 4.1 MB, error
1093 (32680, 2), # disks up to 16 MB, 1k cluster
1094 (262144, 4), # disks up to 128 MB, 2k cluster
1095 (524288, 8), # disks up to 256 MB, 4k cluster
1096 (1048576, 16), # disks up to 512 MB, 8k cluster
1097 (2097152, 32), # disks up to 1 GB, 16k cluster
1098 (4194304, 64) # disks up to 2 GB, 32k cluster
1099 ],
1100 PyFat.FAT_TYPE_FAT12: [
1101 (4084, 1), # disks up to 2 MB, .5k cluster
1102 (8168, 2), # disks up to 4 MB, 1k cluster
1103 (16336, 4), # disks up to 8 MB, 2k cluster
1104 (32672, 8), # disks up to 16 MB, 4k cluster
1105 (65344, 16), # disks up to 32 MB, 8k cluster
1106 (130688, 32), # disks up to 64 MB, 16k cluster
1107 (261376, 64), # disks up to 128 MB, 32k cluster
1108 (522752, 128) # disks up to 256 MB, 64k cluster
1109 ]
1110 }
1111 sec_per_clus = 0
1112 for sec, spc in num_sec_to_sec_per_clus[fat_type]:
1113 if num_sec <= sec:
1114 sec_per_clus = spc
1115 break
1117 boot_code = b"\x0e" # push cs
1118 boot_code += b"\x1f" # pop ds
1119 boot_code += b"\xbe\x5b\x7c" # mov si, offset message_txt
1120 # write_msg:
1121 boot_code += b"\xac" # lodsb
1122 boot_code += b"\x22\xc0" # and al, al
1123 boot_code += b"\x74\x0b" # jz key_press
1124 boot_code += b"\x56" # push si
1125 boot_code += b"\xb4\x0e" # mov ah, 0eh
1126 boot_code += b"\xbb\x07\x00" # mov bx, 0007h
1127 boot_code += b"\xcd\x10" # int 10h
1128 boot_code += b"\x5e" # pop si
1129 boot_code += b"\xeb\xf0" # jmp write_msg
1130 # key_press:
1131 boot_code += b"\x32\xe4" # xor ah, ah
1132 boot_code += b"\xcd\x16" # int 16h
1133 boot_code += b"\xcd\x19" # int 19h
1134 boot_code += b"\xeb\xfe" # foo: jmp foo
1135 # message_txt:
1136 boot_code += \
1137 b"This is not a bootable disk. " \
1138 b"Please insert a bootable floppy and " \
1139 b"press any key to try again ...\n"
1141 if fat_type == PyFat.FAT_TYPE_FAT32:
1142 root_ent_cnt = 0
1143 elif fat_type == PyFat.FAT_TYPE_FAT16:
1144 root_ent_cnt = 512
1145 else:
1146 if sector_size == 512:
1147 root_ent_cnt = 224 # Floppy typical
1148 else:
1149 root_ent_cnt = 512
1151 rsvd_sec_cnt = 32 if fat_type == PyFat.FAT_TYPE_FAT32 else 1
1153 # fat size calculation taken from fatgen103.doc
1154 self.root_dir_sectors = \
1155 ((root_ent_cnt * 32) + (sector_size - 1)) // sector_size
1156 tmp_val1 = size - (rsvd_sec_cnt + self.root_dir_sectors)
1157 tmp_val2 = (256 * sec_per_clus) + number_of_fats
1158 if fat_type == PyFat.FAT_TYPE_FAT32:
1159 tmp_val2 = tmp_val2 // 2
1160 self._fat_size = \
1161 math.ceil((tmp_val1 + tmp_val2 - 1) // tmp_val2 / sector_size)
1162 if fat_type == PyFat.FAT_TYPE_FAT32:
1163 fat_size_16 = 0
1164 fat_size_32 = self._fat_size
1165 else:
1166 fat_size_16 = self._fat_size % 0x10000
1167 # there is no BPB_FATSz32 in a FAT16 BPB
1169 if fat_type == PyFat.FAT_TYPE_FAT32 or num_sec >= 0x10000:
1170 total_sectors_16 = 0
1171 total_sectors_32 = num_sec
1172 else:
1173 total_sectors_16 = num_sec
1174 total_sectors_32 = 0
1176 self.bpb_header = FAT32BootSectorHeader() \
1177 if fat_type == PyFat.FAT_TYPE_FAT32 \
1178 else FAT12BootSectorHeader()
1180 self.root_dir_sector = rsvd_sec_cnt + (self._fat_size * number_of_fats)
1181 self.bytes_per_cluster = sec_per_clus * sector_size
1182 self.first_data_sector = \
1183 rsvd_sec_cnt + number_of_fats * self._fat_size
1185 self.bpb_header.update({
1186 "BS_jmpBoot":
1187 bytearray([0xEB, len(self.bpb_header) - 2, 0x90]),
1188 "BS_OEMName": b"MSWIN4.1",
1189 "BPB_BytsPerSec": sector_size,
1190 "BPB_SecPerClus": sec_per_clus,
1191 "BPB_RsvdSecCnt": rsvd_sec_cnt,
1192 "BPB_NumFATs": number_of_fats,
1193 "BPB_RootEntCnt": root_ent_cnt,
1194 "BPB_TotSec16": total_sectors_16,
1195 "BPB_Media": media_type,
1196 "BPB_FATSz16": fat_size_16,
1197 "BPB_SecPerTrk": 0,
1198 "BPB_NumHeads": 0,
1199 "BPB_HiddSec": 0,
1200 "BPB_TotSec32": total_sectors_32,
1201 "BS_VolID": volume_id,
1202 "BS_VolLab": label[:11].ljust(11).encode('ascii'),
1203 "BS_DrvNum": 0x80 if media_type == 0xF8 else 0,
1204 "BS_Reserved1": 0,
1205 "BS_BootSig": 0x29,
1206 "BS_FilSysType": PyFat.FS_TYPES[fat_type],
1207 })
1209 if fat_type == PyFat.FAT_TYPE_FAT32:
1210 self.bpb_header.update({
1211 "BPB_FATSz32": fat_size_32,
1212 "BPB_ExtFlags": 0,
1213 "BPB_FSVer": 0,
1214 "BPB_RootClus": 2,
1215 "BPB_FSInfo": 1,
1216 "BPB_BkBootSec": 6,
1217 "BPB_Reserved": b'\x00' * 12,
1218 })
1220 self.__verify_bpb_header()
1222 # write fat sector
1223 self.fat = [0] * self.bpb_header["BPB_BytsPerSec"]
1224 if fat_type == PyFat.FAT_TYPE_FAT12:
1225 self.fat[0] = 0x0FF0 | (self.bpb_header["BPB_Media"] % 0xF)
1226 self.fat[1] = PyFat.FAT12_SPECIAL_EOC
1227 elif fat_type == PyFat.FAT_TYPE_FAT16:
1228 self.fat[0] = 0xFFF0 | (self.bpb_header["BPB_Media"] % 0xF)
1229 self.fat[1] = 0xFFFF
1230 elif fat_type == PyFat.FAT_TYPE_FAT32:
1231 self.fat[0] = 0x0FFFFFF0 | (self.bpb_header["BPB_Media"] % 0xF)
1232 self.fat[1] = 0x0FFFFFFF
1233 self.flush_fat()
1235 self.__seek(len(self.bpb_header))
1236 self.__fp.write(boot_code)
1238 if fat_type == PyFat.FAT_TYPE_FAT32:
1239 free_count = (total_sectors_32 - rsvd_sec_cnt -
1240 number_of_fats * self._fat_size) // sec_per_clus - 1
1241 fsinfo = FSInfo(free_count=free_count, next_free=2)
1242 self.__seek(512)
1243 self.__fp.write(bytes(fsinfo))
1245 first_cluster = self.allocate_bytes(
1246 FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE,
1247 erase=True)[0]
1248 self.bpb_header["BPB_RootClus"] = first_cluster
1250 # write backup
1251 backup_offset = self.bpb_header["BPB_BkBootSec"] * \
1252 self.bpb_header["BPB_BytsPerSec"]
1253 self.__seek(len(self.bpb_header) + backup_offset)
1254 self.__fp.write(boot_code)
1256 self.__seek(512 + backup_offset)
1257 self.__fp.write(bytes(fsinfo))
1259 self.parse_root_dir()
1260 vol_label_in_8_3 = EightDotThree(encoding=self.encoding)
1261 vol_label_in_8_3.set_str_name(
1262 EightDotThree.make_8dot3_name(label[:11], self.root_dir))
1263 volume_file = FATDirectoryEntry.new(
1264 name=vol_label_in_8_3,
1265 tz=datetime.timezone.utc,
1266 encoding=self.encoding,
1267 attr=FATDirectoryEntry.ATTR_VOLUME_ID)
1268 self.root_dir.add_subdirectory(volume_file)
1269 self.update_directory_entry(self.root_dir)
1271 self._write_bpb_header()