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

646 statements  

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3 

4"""FAT and BPB parsing for files.""" 

5 

6import datetime 

7import errno 

8 

9import math 

10import struct 

11import threading 

12import time 

13import warnings 

14 

15from contextlib import contextmanager 

16from io import FileIO, open, BytesIO, IOBase, SEEK_END 

17from os import PathLike 

18from typing import Union 

19 

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 

27 

28 

29def _readonly_check(func): 

30 def _wrapper(*args, **kwargs): 

31 read_only = args[0].is_read_only 

32 

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) 

39 

40 return _wrapper 

41 

42 

43class PyFat(object): 

44 """PyFAT base class, parses generic filesystem information.""" 

45 

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 

54 

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 "} 

60 

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} 

87 

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 

96 

97 #: Dirty bit in FAT header 

98 FAT_DIRTY_BIT_MASK = 0x01 

99 

100 def __init__(self, 

101 encoding: str = 'ibm437', 

102 offset: int = 0, 

103 lazy_load: bool = True): 

104 """Set up PyFat class instance. 

105 

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

128 

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 

135 

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) 

142 

143 @_init_check 

144 def read_cluster_contents(self, cluster: int) -> bytes: 

145 """Read contents of given cluster. 

146 

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) 

155 

156 def __get_clean_shutdown_bitmask(self): 

157 """Get clean shutdown bitmask for current FS. 

158 

159 :raises: AttributeError 

160 """ 

161 return getattr(self, f"FAT{self.fat_type}_CLEAN_SHUTDOWN_BIT_MASK") 

162 

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 

173 

174 nt_dirty = (self.bpb_header["BS_Reserved1"] & 

175 self.FAT_DIRTY_BIT_MASK) == self.FAT_DIRTY_BIT_MASK 

176 

177 return dos_dirty or nt_dirty 

178 

179 def _mark_dirty(self): 

180 """Mark partition as not cleanly unmounted. 

181 

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

194 

195 self.bpb_header["BS_Reserved1"] |= self.FAT_DIRTY_BIT_MASK 

196 self._write_bpb_header() 

197 

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

207 

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

212 

213 def set_fp(self, fp: Union[BytesIO, IOBase]): 

214 """Open a filesystem from a valid file pointer. 

215 

216 This allows using in-memory filesystems (e.g., BytesIO). 

217 

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) 

223 

224 if not fp.seekable(): 

225 raise PyFATException("Cannot seek file object.", 

226 errno=errno.EINVAL) 

227 

228 self.is_read_only = not fp.writable() 

229 

230 self.__set_fp(fp) 

231 

232 # Parse BPB & FAT headers of given file 

233 self.parse_header() 

234 

235 # Parse FAT 

236 self._parse_fat() 

237 

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

244 

245 # Parse root directory 

246 self.parse_root_dir() 

247 

248 def open(self, filename: Union[str, PathLike], read_only: bool = False): 

249 """Open filesystem for usage with PyFat. 

250 

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+' 

259 

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) 

265 

266 @_init_check 

267 def get_fs_location(self): 

268 """Retrieve path of opened filesystem.""" 

269 return self.__fp.name 

270 

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"] 

275 

276 return self.bpb_header["BPB_TotSec32"] 

277 

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"] 

282 

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

288 

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 

295 

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

304 

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

311 

312 # Parse first FAT 

313 self.bytes_per_cluster = self.bpb_header["BPB_BytsPerSec"] * \ 

314 self.bpb_header["BPB_SecPerClus"] 

315 

316 if len(fats[0]) != self.bpb_header["BPB_BytsPerSec"] * self._fat_size: 

317 raise PyFATException("Invalid length of FAT") 

318 

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 

325 

326 curr = 0 

327 cluster = 0 

328 incr = self.fat_type / 8 

329 while curr < fat_size: 

330 offset = curr + incr 

331 

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 

341 

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 

348 

349 if math.ceil(offset) == (fat_size - 1): 

350 # Sector boundary case for FAT12 

351 del self.fat[-1] 

352 break 

353 

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

367 

368 curr += incr 

369 cluster += 1 

370 

371 if None in self.fat: 

372 raise AssertionError("Unknown error during FAT parsing, please " 

373 "report this error.") 

374 

375 @_init_check 

376 def __bytes__(self): 

377 """Represent current state of FAT as bytes. 

378 

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) 

391 

392 else: 

393 if self.fat_type == self.FAT_TYPE_FAT16: 

394 fmt = "H" 

395 else: 

396 # FAT32 

397 fmt = "L" 

398 

399 b = struct.pack(f"<{fmt * len(self.fat)}", 

400 *self.fat) 

401 return b 

402 

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. 

408 

409 Directly writes to the filesystem without any consistency check. 

410 **Use with caution** 

411 

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) 

418 

419 @_init_check 

420 @_readonly_check 

421 def free_cluster_chain(self, cluster: int): 

422 """Mark a cluster(chain) as free in FAT. 

423 

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 

433 

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. 

441 

442 Extends cluster chain if needed. 

443 

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 

459 

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) 

465 

466 new_chain = self.allocate_bytes(data_sz - cluster_sz, 

467 erase=erase)[0] 

468 self.fat[last_cluster] = new_chain 

469 

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) 

475 

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 

485 

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 

492 

493 first_fat_bytes = self.bpb_header["BPB_RsvdSecCnt"] 

494 first_fat_bytes *= self.bpb_header["BPB_BytsPerSec"] 

495 

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) 

501 

502 def calc_num_clusters(self, size: int = 0) -> int: 

503 """Calculate the number of required clusters. 

504 

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) 

510 

511 return num_clusters 

512 

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. 

517 

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) 

527 

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 

534 

535 if num_clusters == len(free_clusters): 

536 # Allocated enough clusters! 

537 break 

538 

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 

543 

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 

548 

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 

556 

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 

564 

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) 

570 

571 return free_clusters 

572 

573 @_init_check 

574 @_readonly_check 

575 def update_directory_entry(self, dir_entry: FATDirectoryEntry) -> None: 

576 """Update directory entry on disk. 

577 

578 Special handling is required, since the root directory 

579 on FAT12/16 is on a fixed location on disk. 

580 

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 

591 

592 # Gather all directory entries 

593 dir_entries = b'' 

594 for d in dir_entry._get_entries_raw(): 

595 dir_entries += bytes(d) 

596 

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"] 

610 

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) 

615 

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) 

619 

620 def _fat12_parse_root_dir(self): 

621 """Parse FAT12/16 root dir entries. 

622 

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 

632 

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) 

639 

640 def _fat32_parse_root_dir(self): 

641 """Parse FAT32 root dir entries. 

642 

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) 

648 

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) 

653 

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) 

673 

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

678 

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 

684 

685 with self.__lock: 

686 self.__seek(address) 

687 lfn_dir_data = self.__fp.read(dir_hdr_sz) 

688 

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

693 

694 lfn_entry.add_lfn_entry(**lfn_dir_hdr) 

695 

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) 

702 

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 

707 

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

716 

717 dir_hdr_size = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE 

718 

719 if max_address == 0: 

720 max_address = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE 

721 

722 dir_entries = [] 

723 

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 

742 

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 

748 

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 

753 

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] 

760 

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) 

768 

769 # Reset temporary LFN entry 

770 tmp_lfn_entry = FATLongDirectoryEntry() 

771 

772 return dir_entries, tmp_lfn_entry 

773 

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 

787 

788 return dir_entries 

789 

790 def get_data_cluster_address(self, cluster: int) -> int: 

791 """Get offset of given cluster in bytes. 

792 

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"] 

800 

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"] 

809 

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

836 

837 i = self.fat[i] 

838 

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

844 

845 self.__fp.close() 

846 self.initialized = False 

847 

848 def __del__(self): 

849 """Try to close open handles.""" 

850 try: 

851 self.close() 

852 except PyFATException: 

853 pass 

854 

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. 

859 

860 An internal method to determine whether this volume is FAT12, 

861 FAT16 or FAT32. 

862 

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"] 

872 

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 

879 

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 

889 

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 

896 

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

904 

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

913 

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) 

919 

920 self.bpb_header = BootSectorHeader() 

921 self.bpb_header.parse_header(boot_sector[:36]) 

922 

923 # Verify BPB headers 

924 self.__verify_bpb_header() 

925 

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) 

930 

931 # Determine FAT type 

932 self._fat_size = self._get_fat_size_count() 

933 self.fat_type = self.__determine_fat_type() 

934 

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"] 

941 

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) 

945 

946 # Calculate first data sector 

947 self.first_data_sector = (rsvd_secs + (num_fats * self._fat_size) + 

948 self.root_dir_sectors) 

949 

950 # Check signature 

951 with self.__lock: 

952 self.__seek(510) 

953 signature = struct.unpack("<H", self.__fp.read(2))[0] 

954 

955 if signature != 0xAA55: 

956 raise PyFATException(f"Invalid signature: \'{hex(signature)}\'.") 

957 

958 # Initialization finished 

959 self.initialized = True 

960 

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

971 

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']}\'.") 

978 

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']}\'.") 

985 

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) 

992 

993 if self.bpb_header["BPB_RsvdSecCnt"] == 0: 

994 raise PyFATException("Number of reserved sectors must not be 0") 

995 

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

999 

1000 if self.bpb_header["BPB_NumFATs"] < 1: 

1001 raise PyFATException("At least one FAT expected, None found.") 

1002 

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

1008 

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

1013 

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

1023 

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. 

1035 

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 

1047 

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

1051 

1052 self.fat_type = fat_type 

1053 self.__set_fp(open(filename, mode='rb+')) 

1054 

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) 

1062 

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) 

1069 

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

1074 

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 

1081 

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 

1116 

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" 

1140 

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 

1150 

1151 rsvd_sec_cnt = 32 if fat_type == PyFat.FAT_TYPE_FAT32 else 1 

1152 

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 

1168 

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 

1175 

1176 self.bpb_header = FAT32BootSectorHeader() \ 

1177 if fat_type == PyFat.FAT_TYPE_FAT32 \ 

1178 else FAT12BootSectorHeader() 

1179 

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 

1184 

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

1208 

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

1219 

1220 self.__verify_bpb_header() 

1221 

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

1234 

1235 self.__seek(len(self.bpb_header)) 

1236 self.__fp.write(boot_code) 

1237 

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

1244 

1245 first_cluster = self.allocate_bytes( 

1246 FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE, 

1247 erase=True)[0] 

1248 self.bpb_header["BPB_RootClus"] = first_cluster 

1249 

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) 

1255 

1256 self.__seek(512 + backup_offset) 

1257 self.__fp.write(bytes(fsinfo)) 

1258 

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) 

1270 

1271 self._write_bpb_header()