Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pyfatfs/EightDotThree.py: 24%

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

123 statements  

1# -*- coding: utf-8 -*- 

2 

3"""8DOT3 file name helper class & functions.""" 

4 

5import errno 

6import os 

7import struct 

8 

9from pyfatfs import FAT_OEM_ENCODING, _init_check 

10from pyfatfs._exceptions import PyFATException, NotAFatEntryException 

11 

12 

13class EightDotThree: 

14 """8DOT3 filename representation.""" 

15 

16 #: Length of the byte representation in a directory entry header 

17 SFN_LENGTH = 11 

18 

19 #: Invalid characters for 8.3 file names 

20 INVALID_CHARACTERS = [range(0x0, 0x20)] + [0x22, 0x2A, 0x2B, 0x2C, 0x2E, 

21 0x2F, 0x3A, 0x3B, 0x3C, 0x3D, 

22 0x3E, 0x3F, 0x5B, 0x5C, 0x5D, 

23 0x7C] 

24 

25 def __init__(self, encoding: str = FAT_OEM_ENCODING): 

26 """Offer 8DOT3 filename operation. 

27 

28 :param encoding: Codepage for the 8.3 filename. 

29 Defaults to `FAT_OEM_ENCODING` as per FAT spec. 

30 """ 

31 self.name: bytearray = None 

32 self.encoding = encoding 

33 self.initialized = False 

34 

35 def __str__(self): 

36 """Decode and un-pad SFN string.""" 

37 name = self.name 

38 if name[0] == 0x05: 

39 # Translate 0x05 to 0xE5 

40 name[0] = 0xE5 

41 

42 base = name[:8].decode(self.encoding).rstrip() 

43 ext = name[8:11].decode(self.encoding).rstrip() 

44 sep = "." if len(ext) > 0 else "" 

45 

46 return sep.join([base, ext]) 

47 

48 def __bytes__(self): 

49 """Byte representation of the 8DOT3 name dir entry headers.""" 

50 return bytes(self.name) 

51 

52 @_init_check 

53 def get_unpadded_filename(self) -> str: 

54 """Retrieve the human readable filename.""" 

55 return str(self) 

56 

57 @staticmethod 

58 def __raise_8dot3_nonconformant(name: str): 

59 raise PyFATException(f"Given directory name " 

60 f"{name} is not conform " 

61 f"to 8.3 file naming convention.", 

62 errno=errno.EINVAL) 

63 

64 def __set_name(self, name: bytes): 

65 """Set self.name and verify for correctness.""" 

66 if len(name) != 11: 

67 self.__raise_8dot3_nonconformant(name.decode(self.encoding)) 

68 

69 self.name = name 

70 self.initialized = True 

71 

72 def set_byte_name(self, name: bytes): 

73 """Set the name as byte input from a directory entry header. 

74 

75 :param name: `bytes`: Padded (must be 11 bytes) 8dot3 name 

76 """ 

77 if not isinstance(name, bytes): 

78 raise TypeError(f"Given parameter must be of type bytes, " 

79 f"but got {type(name)} instead.") 

80 

81 name = bytearray(name) 

82 

83 if len(name) != 11: 

84 raise ValueError("Invalid byte name supplied, must be exactly " 

85 "11 bytes long (8+3).") 

86 

87 if name[0] == 0x0 or name[0] == 0xE5: 

88 # Empty directory entry 

89 raise NotAFatEntryException("Given dir entry is invalid and has " 

90 "no valid name.", free_type=name[0]) 

91 

92 self.__set_name(name) 

93 

94 def set_str_name(self, name: str): 

95 """Set the name as string from user input (i.e. folder creation).""" 

96 if not isinstance(name, str): 

97 raise TypeError(f"Given parameter must be of type str, " 

98 f"but got {type(name)} instead.") 

99 

100 if not self.is_8dot3_conform(name, self.encoding): 

101 self.__raise_8dot3_nonconformant(name) 

102 

103 name = bytearray(self._pad_8dot3_name(name).encode(self.encoding)) 

104 if name[0] == 0xE5: 

105 name[0] = 0x05 

106 self.name = name 

107 self.initialized = True 

108 

109 @_init_check 

110 def checksum(self) -> int: 

111 """Calculate checksum of byte string. 

112 

113 :returns: Checksum as int 

114 """ 

115 chksum = 0 

116 for c in self.name: 

117 chksum = ((chksum >> 1) | (chksum & 1) << 7) + c 

118 chksum &= 0xFF 

119 return chksum 

120 

121 @staticmethod 

122 def __check_characters(name: str, encoding: str) -> bool: 

123 """Test if given string contains invalid chars for 8.3 names. 

124 

125 :param name: `str`: Filename to parse 

126 :raises: `ValueError` if the given string contains invalid 

127 8.3 filename characters. 

128 """ 

129 name = name.encode(encoding) 

130 name = list(struct.unpack(f"{len(name)}c", name)) 

131 for c in name: 

132 if ord(c) in EightDotThree.INVALID_CHARACTERS: 

133 raise ValueError(f"Invalid characters in string '{name}', " 

134 f"cannot be used as part of an 8.3 " 

135 f"conform file name.") 

136 

137 @staticmethod 

138 def is_8dot3_conform(entry_name: str, encoding: str = FAT_OEM_ENCODING): 

139 """Indicate conformance of given entries name to 8.3 standard. 

140 

141 :param entry_name: Name of entry to check 

142 :param encoding: ``str``: Encoding for SFN 

143 :returns: bool indicating conformance of name to 8.3 standard 

144 """ 

145 if entry_name != entry_name.upper(): 

146 # Case sensitivity check 

147 return False 

148 

149 root, ext = os.path.splitext(entry_name) 

150 ext = ext[1:] 

151 if len(root) + len(ext) > 11: 

152 return False 

153 elif len(root) > 8 or len(ext) > 3: 

154 return False 

155 

156 # Check for valid characters in both filename segments 

157 for i in [root, ext]: 

158 try: 

159 EightDotThree.__check_characters(i, encoding=encoding) 

160 except ValueError: 

161 return False 

162 

163 return True 

164 

165 @staticmethod 

166 def _pad_8dot3_name(name: str): 

167 """Pad 8DOT3 name to 11 bytes for header operations. 

168 

169 This is required to pass the correct value to the `FATDirectoryEntry` 

170 constructor as a DIR_Name. 

171 """ 

172 root, ext = os.path.splitext(name) 

173 ext = ext[1:] 

174 name = root.strip().ljust(8) + ext.strip().ljust(3) 

175 return name 

176 

177 @staticmethod 

178 def make_8dot3_name(dir_name: str, 

179 parent_dir_entry) -> str: 

180 """Generate filename based on 8.3 rules out of a long file name. 

181 

182 In 8.3 notation we try to use the first 6 characters and 

183 fill the rest with a tilde, followed by a number (starting 

184 at 1). If that entry is already given, we increment this 

185 number and try again until all possibilities are exhausted 

186 (i.e. A~999999.TXT). 

187 

188 :param dir_name: Long name of directory entry. 

189 :param parent_dir_entry: `FATDirectoryEntry`: Dir entry of parent dir. 

190 :returns: `str`: 8DOT3 compliant filename. 

191 :raises: PyFATException: If parent dir is not a directory 

192 or all name generation possibilities 

193 are exhausted 

194 """ 

195 dirs, files, _ = parent_dir_entry.get_entries() 

196 dir_entries = [e.get_short_name() for e in dirs + files] 

197 

198 extsep = "." 

199 

200 def map_chars(name: bytes) -> bytes: 

201 """Map 8DOT3 valid characters. 

202 

203 :param name: `str`: input name 

204 :returns: `str`: mapped output character 

205 """ 

206 _name: bytes = b'' 

207 for b in struct.unpack(f"{len(name)}c", name): 

208 if b == b' ': 

209 _name += b'' 

210 elif ord(b) in EightDotThree.INVALID_CHARACTERS: 

211 _name += b'_' 

212 else: 

213 _name += b 

214 return _name 

215 

216 dir_name = dir_name.upper() 

217 # Shorten to 8 chars; strip invalid characters 

218 basename = os.path.splitext(dir_name)[0][0:8].strip() 

219 basename = basename.encode(parent_dir_entry._encoding, 

220 errors="replace") 

221 basename = map_chars(basename).decode(parent_dir_entry._encoding) 

222 

223 # Shorten to 3 chars; strip invalid characters 

224 extname = os.path.splitext(dir_name)[1][1:4].strip() 

225 extname = extname.encode(parent_dir_entry._encoding, 

226 errors="replace") 

227 extname = map_chars(extname).decode(parent_dir_entry._encoding) 

228 

229 if len(extname) == 0: 

230 extsep = "" 

231 

232 # Loop until suiting name is found 

233 i = 0 

234 while len(str(i)) + 1 <= 7: 

235 if i > 0: 

236 maxlen = 8 - (1 + len(str(i))) 

237 basename = f"{basename[0:maxlen]}~{i}" 

238 

239 short_name = f"{basename}{extsep}{extname}" 

240 

241 if short_name not in dir_entries: 

242 return short_name 

243 i += 1 

244 

245 raise PyFATException("Cannot generate 8dot3 filename, " 

246 "unable to find suiting short file name.", 

247 errno=errno.EEXIST)