entry.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. # SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. from typing import List, Optional, Union
  4. from construct import Const, Int8ul, Int16ul, Int32ul, PaddedString, Struct
  5. from .exceptions import LowerCaseException, TooLongNameException
  6. from .fatfs_state import FATFSState
  7. from .utils import (DATETIME, EMPTY_BYTE, FATFS_INCEPTION, MAX_EXT_SIZE, MAX_NAME_SIZE, SHORT_NAMES_ENCODING,
  8. FATDefaults, build_date_entry, build_time_entry, is_valid_fatfs_name, pad_string)
  9. class Entry:
  10. """
  11. The class Entry represents entry of the directory.
  12. """
  13. ATTR_READ_ONLY: int = 0x01
  14. ATTR_HIDDEN: int = 0x02
  15. ATTR_SYSTEM: int = 0x04
  16. ATTR_VOLUME_ID: int = 0x08
  17. ATTR_DIRECTORY: int = 0x10 # directory
  18. ATTR_ARCHIVE: int = 0x20 # file
  19. ATTR_LONG_NAME: int = ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID
  20. # indexes in the entry structure and sizes in bytes, not in characters (encoded using 2 bytes for lfn)
  21. LDIR_Name1_IDX: int = 1
  22. LDIR_Name1_SIZE: int = 5
  23. LDIR_Name2_IDX: int = 14
  24. LDIR_Name2_SIZE: int = 6
  25. LDIR_Name3_IDX: int = 28
  26. LDIR_Name3_SIZE: int = 2
  27. # one entry can hold 13 characters with size 2 bytes distributed in three regions of the 32 bytes entry
  28. CHARS_PER_ENTRY: int = LDIR_Name1_SIZE + LDIR_Name2_SIZE + LDIR_Name3_SIZE
  29. SHORT_ENTRY: int = -1
  30. # this value is used for short-like entry but with accepted lower case
  31. SHORT_ENTRY_LN: int = 0
  32. # The 1st January 1980 00:00:00
  33. DEFAULT_DATE: DATETIME = (FATFS_INCEPTION.year, FATFS_INCEPTION.month, FATFS_INCEPTION.day)
  34. DEFAULT_TIME: DATETIME = (FATFS_INCEPTION.hour, FATFS_INCEPTION.minute, FATFS_INCEPTION.second)
  35. ENTRY_FORMAT_SHORT_NAME = Struct(
  36. 'DIR_Name' / PaddedString(MAX_NAME_SIZE, SHORT_NAMES_ENCODING),
  37. 'DIR_Name_ext' / PaddedString(MAX_EXT_SIZE, SHORT_NAMES_ENCODING),
  38. 'DIR_Attr' / Int8ul,
  39. 'DIR_NTRes' / Int8ul, # this tagged for lfn (0x00 for lfn prefix, 0x18 for short name in lfn)
  40. 'DIR_CrtTimeTenth' / Const(EMPTY_BYTE), # ignored by esp-idf fatfs library
  41. 'DIR_CrtTime' / Int16ul, # ignored by esp-idf fatfs library
  42. 'DIR_CrtDate' / Int16ul, # ignored by esp-idf fatfs library
  43. 'DIR_LstAccDate' / Int16ul, # must be same as DIR_WrtDate
  44. 'DIR_FstClusHI' / Const(2 * EMPTY_BYTE),
  45. 'DIR_WrtTime' / Int16ul,
  46. 'DIR_WrtDate' / Int16ul,
  47. 'DIR_FstClusLO' / Int16ul,
  48. 'DIR_FileSize' / Int32ul,
  49. )
  50. def __init__(self,
  51. entry_id: int,
  52. parent_dir_entries_address: int,
  53. fatfs_state: FATFSState) -> None:
  54. self.fatfs_state: FATFSState = fatfs_state
  55. self.id: int = entry_id
  56. self.entry_address: int = parent_dir_entries_address + self.id * FATDefaults.ENTRY_SIZE
  57. self._is_alias: bool = False
  58. self._is_empty: bool = True
  59. @staticmethod
  60. def get_cluster_id(obj_: dict) -> int:
  61. cluster_id_: int = obj_['DIR_FstClusLO']
  62. return cluster_id_
  63. @property
  64. def is_empty(self) -> bool:
  65. return self._is_empty
  66. @staticmethod
  67. def _parse_entry(entry_bytearray: Union[bytearray, bytes]) -> dict:
  68. entry_: dict = Entry.ENTRY_FORMAT_SHORT_NAME.parse(entry_bytearray)
  69. return entry_
  70. @staticmethod
  71. def _build_entry(**kwargs) -> bytes: # type: ignore
  72. entry_: bytes = Entry.ENTRY_FORMAT_SHORT_NAME.build(dict(**kwargs))
  73. return entry_
  74. @staticmethod
  75. def _build_entry_long(names: List[bytes], checksum: int, order: int, is_last: bool) -> bytes:
  76. """
  77. Long entry starts with 1 bytes of the order, if the entry is the last in the chain it is or-masked with 0x40,
  78. otherwise is without change (or masked with 0x00). The following example shows 3 entries:
  79. first two (0x2000-0x2040) are long in the reverse order and the last one (0x2040-0x2060) is short.
  80. The entries define file name "thisisverylongfilenama.txt".
  81. 00002000: 42 67 00 66 00 69 00 6C 00 65 00 0F 00 43 6E 00 Bg.f.i.l.e...Cn.
  82. 00002010: 61 00 6D 00 61 00 2E 00 74 00 00 00 78 00 74 00 a.m.a...t...x.t.
  83. 00002020: 01 74 00 68 00 69 00 73 00 69 00 0F 00 43 73 00 .t.h.i.s.i...Cs.
  84. 00002030: 76 00 65 00 72 00 79 00 6C 00 00 00 6F 00 6E 00 v.e.r.y.l...o.n.
  85. 00002040: 54 48 49 53 49 53 7E 31 54 58 54 20 00 00 00 00 THISIS~1TXT.....
  86. 00002050: 21 00 00 00 00 00 00 00 21 00 02 00 15 00 00 00 !.......!.......
  87. """
  88. order |= (0x40 if is_last else 0x00)
  89. long_entry: bytes = (Int8ul.build(order) + # order of the long name entry (possibly masked with 0x40)
  90. names[0] + # first 5 characters (10 bytes) of the name part
  91. Int8ul.build(Entry.ATTR_LONG_NAME) + # one byte entity type ATTR_LONG_NAME
  92. Int8ul.build(0) + # one byte of zeros
  93. Int8ul.build(checksum) + # lfn_checksum defined in utils.py
  94. names[1] + # next 6 characters (12 bytes) of the name part
  95. Int16ul.build(0) + # 2 bytes of zeros
  96. names[2]) # last 2 characters (4 bytes) of the name part
  97. return long_entry
  98. @staticmethod
  99. def parse_entry_long(entry_bytes_: bytes, my_check: int) -> dict:
  100. order_ = Int8ul.parse(entry_bytes_[0:1])
  101. names0 = entry_bytes_[1:11]
  102. if Int8ul.parse(entry_bytes_[12:13]) != 0 or Int16ul.parse(entry_bytes_[26:28]) != 0 or Int8ul.parse(entry_bytes_[11:12]) != 15:
  103. return {}
  104. if Int8ul.parse(entry_bytes_[13:14]) != my_check:
  105. return {}
  106. names1 = entry_bytes_[14:26]
  107. names2 = entry_bytes_[28:32]
  108. return {'order': order_, 'name1': names0, 'name2': names1, 'name3': names2, 'is_last': bool(order_ & 0x40 == 0x40)}
  109. @property
  110. def entry_bytes(self) -> bytes:
  111. """
  112. :returns: Bytes defining the entry belonging to the given instance.
  113. """
  114. start_: int = self.entry_address
  115. entry_: bytes = self.fatfs_state.binary_image[start_: start_ + FATDefaults.ENTRY_SIZE]
  116. return entry_
  117. @entry_bytes.setter
  118. def entry_bytes(self, value: bytes) -> None:
  119. """
  120. :param value: new content of the entry
  121. :returns: None
  122. The setter sets the content of the entry in bytes.
  123. """
  124. self.fatfs_state.binary_image[self.entry_address: self.entry_address + FATDefaults.ENTRY_SIZE] = value
  125. def _clean_entry(self) -> None:
  126. self.entry_bytes: bytes = FATDefaults.ENTRY_SIZE * EMPTY_BYTE
  127. def allocate_entry(self,
  128. first_cluster_id: int,
  129. entity_name: str,
  130. entity_type: int,
  131. entity_extension: str = '',
  132. size: int = 0,
  133. date: DATETIME = DEFAULT_DATE,
  134. time: DATETIME = DEFAULT_TIME,
  135. lfn_order: int = SHORT_ENTRY,
  136. lfn_names: Optional[List[bytes]] = None,
  137. lfn_checksum_: int = 0,
  138. fits_short: bool = False,
  139. lfn_is_last: bool = False) -> None:
  140. """
  141. :param first_cluster_id: id of the first data cluster for given entry
  142. :param entity_name: name recorded in the entry
  143. :param entity_extension: extension recorded in the entry
  144. :param size: size of the content of the file
  145. :param date: denotes year (actual year minus 1980), month number day of the month (minimal valid is (0, 1, 1))
  146. :param time: denotes hour, minute and second with granularity 2 seconds (sec // 2)
  147. :param entity_type: type of the entity (file [0x20] or directory [0x10])
  148. :param lfn_order: if long names support is enabled, defines order in long names entries sequence (-1 for short)
  149. :param lfn_names: if the entry is dedicated for long names the lfn_names contains
  150. LDIR_Name1, LDIR_Name2 and LDIR_Name3 in this order
  151. :param lfn_checksum_: use only for long file names, checksum calculated lfn_checksum function
  152. :param fits_short: determines if the name fits in 8.3 filename
  153. :param lfn_is_last: determines if the long file name entry is holds last part of the name,
  154. thus its address is first in the physical order
  155. :returns: None
  156. :raises LowerCaseException: In case when long_names_enabled is set to False and filename exceeds 8 chars
  157. for name or 3 chars for extension the exception is raised
  158. :raises TooLongNameException: When long_names_enabled is set to False and name doesn't fit to 8.3 filename
  159. an exception is raised
  160. """
  161. valid_full_name: bool = is_valid_fatfs_name(entity_name) and is_valid_fatfs_name(entity_extension)
  162. if not (valid_full_name or lfn_order >= 0):
  163. raise LowerCaseException('Lower case is not supported in short name entry, use upper case.')
  164. if self.fatfs_state.use_default_datetime:
  165. date = self.DEFAULT_DATE
  166. time = self.DEFAULT_TIME
  167. # clean entry before allocation
  168. self._clean_entry()
  169. self._is_empty = False
  170. object_name = entity_name.upper() if not self.fatfs_state.long_names_enabled else entity_name
  171. object_extension = entity_extension.upper() if not self.fatfs_state.long_names_enabled else entity_extension
  172. exceeds_short_name: bool = len(object_name) > MAX_NAME_SIZE or len(object_extension) > MAX_EXT_SIZE
  173. if not self.fatfs_state.long_names_enabled and exceeds_short_name:
  174. raise TooLongNameException(
  175. 'Maximal length of the object name is {} characters and {} characters for extension!'.format(
  176. MAX_NAME_SIZE, MAX_EXT_SIZE
  177. )
  178. )
  179. start_address = self.entry_address
  180. end_address = start_address + FATDefaults.ENTRY_SIZE
  181. if lfn_order in (self.SHORT_ENTRY, self.SHORT_ENTRY_LN):
  182. date_entry_: int = build_date_entry(*date)
  183. time_entry: int = build_time_entry(*time)
  184. self.fatfs_state.binary_image[start_address: end_address] = self._build_entry(
  185. DIR_Name=pad_string(object_name, size=MAX_NAME_SIZE),
  186. DIR_Name_ext=pad_string(object_extension, size=MAX_EXT_SIZE),
  187. DIR_Attr=entity_type,
  188. DIR_NTRes=0x00 if (not self.fatfs_state.long_names_enabled) or (not fits_short) else 0x18,
  189. DIR_FstClusLO=first_cluster_id,
  190. DIR_FileSize=size,
  191. DIR_CrtDate=date_entry_, # ignored by esp-idf fatfs library
  192. DIR_LstAccDate=date_entry_, # must be same as DIR_WrtDate
  193. DIR_WrtDate=date_entry_,
  194. DIR_CrtTime=time_entry, # ignored by esp-idf fatfs library
  195. DIR_WrtTime=time_entry
  196. )
  197. else:
  198. assert lfn_names is not None
  199. self.fatfs_state.binary_image[start_address: end_address] = self._build_entry_long(lfn_names,
  200. lfn_checksum_,
  201. lfn_order,
  202. lfn_is_last)
  203. def update_content_size(self, content_size: int) -> None:
  204. """
  205. :param content_size: the new size of the file content in bytes
  206. :returns: None
  207. This method parses the binary entry to the construct structure, updates the content size of the file
  208. and builds new binary entry.
  209. """
  210. parsed_entry = self._parse_entry(self.entry_bytes)
  211. parsed_entry.DIR_FileSize = content_size # type: ignore
  212. self.entry_bytes = Entry.ENTRY_FORMAT_SHORT_NAME.build(parsed_entry)