fatfsparse.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. #!/usr/bin/env python
  2. # SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
  3. # SPDX-License-Identifier: Apache-2.0
  4. import argparse
  5. import os
  6. import construct
  7. from fatfs_utils.boot_sector import BootSector
  8. from fatfs_utils.entry import Entry
  9. from fatfs_utils.fat import FAT
  10. from fatfs_utils.fatfs_state import BootSectorState
  11. from fatfs_utils.utils import FULL_BYTE, LONG_NAMES_ENCODING, PAD_CHAR, FATDefaults, lfn_checksum, read_filesystem
  12. from wl_fatfsgen import remove_wl
  13. def build_file_name(name1: bytes, name2: bytes, name3: bytes) -> str:
  14. full_name_ = name1 + name2 + name3
  15. # need to strip empty bytes and null-terminating char ('\x00')
  16. return full_name_.rstrip(FULL_BYTE).decode(LONG_NAMES_ENCODING).rstrip('\x00')
  17. def get_obj_name(obj_: dict, directory_bytes_: bytes, entry_position_: int, lfn_checksum_: int) -> str:
  18. obj_ext_ = obj_['DIR_Name_ext'].rstrip(chr(PAD_CHAR))
  19. ext_ = f'.{obj_ext_}' if len(obj_ext_) > 0 else ''
  20. obj_name_: str = obj_['DIR_Name'].rstrip(chr(PAD_CHAR)) + ext_ # short entry name
  21. # if LFN was detected, the record is considered as single SFN record only if DIR_NTRes == 0x18 (LDIR_DIR_NTRES)
  22. # if LFN was not detected, the record cannot be part of the LFN, no matter the value of DIR_NTRes
  23. if not args.long_name_support or obj_['DIR_NTRes'] == Entry.LDIR_DIR_NTRES:
  24. return obj_name_
  25. full_name = {}
  26. for pos in range(entry_position_ - 1, -1, -1): # loop from the current entry back to the start
  27. obj_address_: int = FATDefaults.ENTRY_SIZE * pos
  28. entry_bytes_: bytes = directory_bytes_[obj_address_: obj_address_ + FATDefaults.ENTRY_SIZE]
  29. struct_ = Entry.parse_entry_long(entry_bytes_, lfn_checksum_)
  30. if len(struct_.items()) > 0:
  31. full_name[struct_['order']] = build_file_name(struct_['name1'], struct_['name2'], struct_['name3'])
  32. if struct_['is_last']:
  33. break
  34. return ''.join(map(lambda x: x[1], sorted(full_name.items()))) or obj_name_
  35. def traverse_folder_tree(directory_bytes_: bytes,
  36. name: str,
  37. state_: BootSectorState,
  38. fat_: FAT,
  39. binary_array_: bytes) -> None:
  40. os.makedirs(name)
  41. assert len(directory_bytes_) % FATDefaults.ENTRY_SIZE == 0
  42. entries_count_: int = len(directory_bytes_) // FATDefaults.ENTRY_SIZE
  43. for i in range(entries_count_):
  44. obj_address_: int = FATDefaults.ENTRY_SIZE * i
  45. try:
  46. obj_: dict = Entry.ENTRY_FORMAT_SHORT_NAME.parse(
  47. directory_bytes_[obj_address_: obj_address_ + FATDefaults.ENTRY_SIZE])
  48. except (construct.core.ConstError, UnicodeDecodeError):
  49. args.long_name_support = True
  50. continue
  51. if obj_['DIR_Attr'] == 0: # empty entry
  52. continue
  53. obj_name_: str = get_obj_name(obj_,
  54. directory_bytes_,
  55. entry_position_=i,
  56. lfn_checksum_=lfn_checksum(obj_['DIR_Name'] + obj_['DIR_Name_ext']))
  57. if obj_['DIR_Attr'] == Entry.ATTR_ARCHIVE:
  58. content_ = fat_.get_chained_content(cluster_id_=Entry.get_cluster_id(obj_),
  59. size=obj_['DIR_FileSize'])
  60. with open(os.path.join(name, obj_name_), 'wb') as new_file:
  61. new_file.write(content_)
  62. elif obj_['DIR_Attr'] == Entry.ATTR_DIRECTORY:
  63. # avoid creating symlinks to itself and parent folder
  64. if obj_name_ in ('.', '..'):
  65. continue
  66. child_directory_bytes_ = fat_.get_chained_content(cluster_id_=obj_['DIR_FstClusLO'])
  67. traverse_folder_tree(directory_bytes_=child_directory_bytes_,
  68. name=os.path.join(name, obj_name_),
  69. state_=state_,
  70. fat_=fat_,
  71. binary_array_=binary_array_)
  72. def remove_wear_levelling_if_exists(fs_: bytes) -> bytes:
  73. """
  74. Detection of the wear levelling layer is performed in two steps:
  75. 1) check if the first sector is a valid boot sector
  76. 2) check if the size defined in the boot sector is the same as the partition size:
  77. - if it is, there is no wear levelling layer
  78. - otherwise, we need to remove wl for further processing
  79. """
  80. try:
  81. boot_sector__ = BootSector()
  82. boot_sector__.parse_boot_sector(fs_)
  83. if boot_sector__.boot_sector_state.size == len(fs_):
  84. return fs_
  85. except construct.core.ConstError:
  86. pass
  87. plain_fs: bytes = remove_wl(fs_)
  88. return plain_fs
  89. if __name__ == '__main__':
  90. desc = 'Tool for parsing fatfs image and extracting directory structure on host.'
  91. argument_parser: argparse.ArgumentParser = argparse.ArgumentParser(description=desc)
  92. argument_parser.add_argument('input_image',
  93. help='Path to the image that will be parsed and extracted.')
  94. argument_parser.add_argument('--long-name-support',
  95. action='store_true',
  96. help=argparse.SUPPRESS)
  97. # ensures backward compatibility
  98. argument_parser.add_argument('--wear-leveling',
  99. action='store_true',
  100. help=argparse.SUPPRESS)
  101. argument_parser.add_argument('--wl-layer',
  102. choices=['detect', 'enabled', 'disabled'],
  103. default=None,
  104. help="If detection doesn't work correctly, "
  105. 'you can force analyzer to or not to assume WL.')
  106. args = argument_parser.parse_args()
  107. # if wear levelling is detected or user explicitly sets the parameter `--wl_layer enabled`
  108. # the partition with wear levelling is transformed to partition without WL for convenient parsing
  109. # in some cases the partitions with and without wear levelling can be 100% equivalent
  110. # and only user can break this tie by explicitly setting
  111. # the parameter --wl-layer to enabled, respectively disabled
  112. if args.wear_leveling and args.wl_layer:
  113. raise NotImplementedError('Argument --wear-leveling cannot be combined with --wl-layer!')
  114. if args.wear_leveling:
  115. args.wl_layer = 'enabled'
  116. args.wl_layer = args.wl_layer or 'detect'
  117. fs = read_filesystem(args.input_image)
  118. # An algorithm for removing wear levelling:
  119. # 1. find an remove dummy sector:
  120. # a) dummy sector is at the position defined by the number of records in the state sector
  121. # b) dummy may not be placed in state nor cfg sectors
  122. # c) first (boot) sector position (boot_s_pos) is calculated using value of move count
  123. # boot_s_pos = - mc
  124. # 2. remove state sectors (trivial)
  125. # 3. remove cfg sector (trivial)
  126. # 4. valid fs is then old_fs[-mc:] + old_fs[:-mc]
  127. if args.wl_layer == 'enabled':
  128. fs = remove_wl(fs)
  129. elif args.wl_layer != 'disabled':
  130. # wear levelling is removed to enable parsing using common algorithm
  131. fs = remove_wear_levelling_if_exists(fs)
  132. boot_sector_ = BootSector()
  133. boot_sector_.parse_boot_sector(fs)
  134. fat = FAT(boot_sector_.boot_sector_state, init_=False)
  135. boot_dir_start_ = boot_sector_.boot_sector_state.root_directory_start
  136. boot_dir_sectors = boot_sector_.boot_sector_state.root_dir_sectors_cnt
  137. full_ = fs[boot_dir_start_: boot_dir_start_ + boot_dir_sectors * boot_sector_.boot_sector_state.sector_size]
  138. traverse_folder_tree(full_,
  139. boot_sector_.boot_sector_state.volume_label.rstrip(chr(PAD_CHAR)),
  140. boot_sector_.boot_sector_state, fat, fs)