1# -*- coding: utf-8 -*-
2
3"""Directory entry operations with PyFAT."""
4import posixpath
5import struct
6import warnings
7
8from time import timezone
9
10from pyfatfs.DosDateTime import DosDateTime
11from pyfatfs.EightDotThree import EightDotThree
12from pyfatfs._exceptions import PyFATException, NotAnLFNEntryException, \
13 BrokenLFNEntryException
14from pyfatfs import FAT_OEM_ENCODING, FAT_LFN_ENCODING
15
16import errno
17
18
19class FATDirectoryEntry:
20 """Represents directory entries in FAT (files & directories)."""
21
22 #: Marks a directory entry as empty
23 FREE_DIR_ENTRY_MARK = 0xE5
24 #: Marks all directory entries after this one as empty
25 LAST_DIR_ENTRY_MARK = 0x00
26
27 #: Bit set in DIR_Attr if entry is read-only
28 ATTR_READ_ONLY = 0x01
29 #: Bit set in DIR_Attr if entry is hidden
30 ATTR_HIDDEN = 0x02
31 #: Bit set in DIR_Attr if entry is a system file
32 ATTR_SYSTEM = 0x04
33 #: Bit set in DIR_Attr if entry is a volume id descriptor
34 ATTR_VOLUME_ID = 0x8
35 #: Bit set in DIR_Attr if entry is a directory
36 ATTR_DIRECTORY = 0x10
37 #: Bit set in DIR_Attr if entry is an archive
38 ATTR_ARCHIVE = 0x20
39 #: Bits set in DIR_Attr if entry is an LFN entry
40 ATTR_LONG_NAME = ATTR_READ_ONLY | ATTR_HIDDEN | \
41 ATTR_SYSTEM | ATTR_VOLUME_ID
42 #: Bitmask to check if entry is an LFN entry
43 ATTR_LONG_NAME_MASK = ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | \
44 ATTR_VOLUME_ID | ATTR_DIRECTORY | ATTR_ARCHIVE
45
46 #: Directory entry header layout in struct formatted string
47 FAT_DIRECTORY_LAYOUT = "<11sBBBHHHHHHHL"
48 #: Size of a directory entry header in bytes
49 FAT_DIRECTORY_HEADER_SIZE = struct.calcsize(FAT_DIRECTORY_LAYOUT)
50 #: Maximum allowed file size, dictated by size of DIR_FileSize
51 MAX_FILE_SIZE = 0xFFFFFFFF
52 #: Directory entry headers
53 FAT_DIRECTORY_VARS = ["DIR_Name", "DIR_Attr", "DIR_NTRes",
54 "DIR_CrtTimeTenth", "DIR_CrtTime",
55 "DIR_CrtDate", "DIR_LstAccessDate",
56 "DIR_FstClusHI", "DIR_WrtTime",
57 "DIR_WrtDate", "DIR_FstClusLO",
58 "DIR_FileSize"]
59
60 def __init__(self,
61 DIR_Name: EightDotThree, DIR_Attr: int,
62 DIR_NTRes: int, DIR_CrtTimeTenth: int,
63 DIR_CrtTime: int, DIR_CrtDate: int, DIR_LstAccessDate: int,
64 DIR_FstClusHI: int, DIR_WrtTime: int, DIR_WrtDate: int,
65 DIR_FstClusLO: int, DIR_FileSize: int,
66 encoding: str = FAT_OEM_ENCODING,
67 fs: "pyfatfs.PyFat.PyFat" = None, # noqa: F821
68 lazy_load: bool = False, lfn_entry=None):
69 """FAT directory entry constructor.
70
71 :param DIR_Name: `EightDotThree` class instance
72 :param DIR_Attr: Attributes of directory
73 :param DIR_NTRes: Reserved attributes of directory entry
74 :param DIR_CrtTimeTenth: Milliseconds at file creation
75 :param DIR_CrtTime: Creation timestamp of entry
76 :param DIR_CrtDate: Creation date of entry
77 :param DIR_LstAccessDate: Last access date of entry
78 :param DIR_FstClusHI: High cluster value of entry data
79 :param DIR_WrtTime: Modification timestamp of entry
80 :param DIR_WrtDate: Modification date of entry
81 :param DIR_FstClusLO: Low cluster value of entry data
82 :param DIR_FileSize: File size in bytes
83 :param encoding: Encoding of filename
84 :param lfn_entry: FATLongDirectoryEntry instance or None
85 """
86 self.__filesize = 0
87
88 self.name: EightDotThree = DIR_Name
89 self.attr = int(DIR_Attr)
90 self.ntres = int(DIR_NTRes)
91 self.crttimetenth = int(DIR_CrtTimeTenth)
92 self.crttime = int(DIR_CrtTime)
93 self.crtdate = int(DIR_CrtDate)
94 self.lstaccessdate = int(DIR_LstAccessDate)
95 self.fstclushi = int(DIR_FstClusHI)
96 self.wrttime = int(DIR_WrtTime)
97 self.wrtdate = int(DIR_WrtDate)
98 self.fstcluslo = int(DIR_FstClusLO)
99 self.filesize = int(DIR_FileSize)
100
101 self.__lazy_load = lazy_load
102 self.__fs = fs
103
104 self._parent = None
105
106 # Handle LFN entries
107 self.lfn_entry = None
108 try:
109 self.set_lfn_entry(lfn_entry)
110 except BrokenLFNEntryException:
111 warnings.warn("Broken LFN entry detected, omitting "
112 "long file name.")
113
114 self.__dirs = []
115 self.__encoding = encoding
116
117 @property
118 def _encoding(self):
119 return self.__encoding
120
121 @property
122 def filesize(self):
123 """Size of the file in bytes.
124
125 :getter: Get the currently set filesize in bytes
126 :setter: Set new filesize. FAT chain must be extended
127 separately. Raises `PyFATException` with
128 `errno=E2BIG` if filesize is larger than
129 `FATDirectoryEntry.MAX_FILE_SIZE`.
130 :type: int
131 """
132 return self.__filesize
133
134 @filesize.setter
135 def filesize(self, size: int):
136 if size > self.MAX_FILE_SIZE:
137 raise PyFATException(f"Specified file size {size} too large "
138 f"for FAT-based filesystems.",
139 errno=errno.E2BIG)
140
141 self.__filesize = size
142
143 @staticmethod
144 def new(name: EightDotThree, tz: timezone, encoding: str,
145 attr: int = 0, ntres: int = 0, cluster: int = 0,
146 filesize: int = 0) -> "FATDirectoryEntry":
147 """Create a new directory entry with sane defaults.
148
149 :param name: ``EightDotThree``: SFN of new dentry
150 :param tz: ``timezone``: Timezone value to use for new timestamp
151 :param encoding: ``str``: Encoding for SFN
152 :param attr: ``int``: Directory attributes
153 :param ntres: ``int``: Reserved NT directory attributes
154 :param cluster: ``int``: Cluster number of dentry
155 :param filesize: ``int``: Size of file referenced by dentry
156 :returns: ``FATDirectoryEntry`` instance
157 """
158 dt = DosDateTime.now(tz=tz)
159 dentry = FATDirectoryEntry(
160 DIR_Name=name,
161 DIR_Attr=attr,
162 DIR_NTRes=ntres,
163 DIR_CrtTimeTenth=0,
164 DIR_CrtTime=dt.serialize_time(),
165 DIR_CrtDate=dt.serialize_date(),
166 DIR_LstAccessDate=dt.serialize_date(),
167 DIR_FstClusHI=0x00,
168 DIR_WrtTime=dt.serialize_time(),
169 DIR_WrtDate=dt.serialize_date(),
170 DIR_FstClusLO=0x00,
171 DIR_FileSize=filesize,
172 encoding=encoding
173 )
174 dentry.set_cluster(cluster)
175 return dentry
176
177 def get_ctime(self) -> DosDateTime:
178 """Get dentry creation time."""
179 return self.__combine_dosdatetime(self.crtdate, self.crttime)
180
181 def get_mtime(self) -> DosDateTime:
182 """Get dentry modification time."""
183 return self.__combine_dosdatetime(self.wrtdate, self.wrttime)
184
185 def get_atime(self) -> DosDateTime:
186 """Get dentry access time."""
187 return DosDateTime.deserialize_date(self.lstaccessdate)
188
189 @staticmethod
190 def __combine_dosdatetime(dt, tm) -> DosDateTime:
191 dt = DosDateTime.deserialize_date(dt)
192 return dt.combine(dt, DosDateTime.deserialize_time(tm))
193
194 def get_checksum(self) -> int:
195 """Get calculated checksum of this directory entry.
196
197 :returns: Checksum as int
198 """
199 return self.name.checksum()
200
201 def set_lfn_entry(self, lfn_entry):
202 """Set LFN entry for current directory entry.
203
204 :param: lfn_entry: Can be either of type `FATLongDirectoryEntry`
205 or `None`.
206 """
207 if not isinstance(lfn_entry, FATLongDirectoryEntry):
208 return
209
210 # Verify LFN entries checksums
211 chksum = self.get_checksum()
212 for entry in lfn_entry.lfn_entries:
213 entry_chksum = lfn_entry.lfn_entries[entry]["LDIR_Chksum"]
214 if entry_chksum != chksum:
215 raise BrokenLFNEntryException(f'Checksum verification for '
216 f'LFN entry of directory '
217 f'"{self.get_short_name()}" '
218 f'failed')
219 self.lfn_entry = lfn_entry
220
221 def get_entry_size(self):
222 """Get size of directory entry.
223
224 :returns: Entry size in bytes as int
225 """
226 if self.is_directory():
227 self.__populate_dirs()
228
229 sz = self.FAT_DIRECTORY_HEADER_SIZE
230 if isinstance(self.lfn_entry, FATLongDirectoryEntry):
231 sz *= len(self.lfn_entry.lfn_entries)
232 sz += self.FAT_DIRECTORY_HEADER_SIZE * len(self.__dirs)+1
233
234 return sz
235
236 def get_size(self):
237 """Get filesize or directory entry size.
238
239 :returns: Filesize or directory entry size in bytes as int
240 """
241 import warnings
242 warnings.warn(f"{self.__class__}.get_size is deprecated, this "
243 f"method will be removed in PyFatFS 2.0; please "
244 f"use the filesize property instead!",
245 DeprecationWarning)
246 return self.filesize
247
248 def set_size(self, size: int):
249 """Set filesize.
250
251 :param size: `int`: File size in bytes
252 """
253 import warnings
254 warnings.warn(f"{self.__class__}.set_size is deprecated, this "
255 f"method will be removed in PyFatFS 2.0; please "
256 f"use the filesize property instead!",
257 DeprecationWarning)
258 self.filesize = size
259
260 def get_cluster(self):
261 """Get cluster address of directory entry.
262
263 :returns: Cluster address of entry
264 """
265 return self.fstcluslo + (self.fstclushi << 16)
266
267 def set_cluster(self, first_cluster):
268 """Set low and high cluster address in directory headers."""
269 self.fstcluslo = (first_cluster >> (16 * 0) & 0xFFFF)
270 self.fstclushi = (first_cluster >> (16 * 1) & 0xFFFF)
271
272 def __bytes__(self):
273 """Represent directory entry as bytes.
274
275 Note: Also represents accompanying LFN entries
276
277 :returns: Entry & LFN entry as bytes-object
278 """
279 entry = b''
280 if isinstance(self.lfn_entry, FATLongDirectoryEntry):
281 entry += bytes(self.lfn_entry)
282
283 entry += struct.pack(self.FAT_DIRECTORY_LAYOUT,
284 self.name.name,
285 self.attr, self.ntres, self.crttimetenth,
286 self.crttime, self.crtdate, self.lstaccessdate,
287 self.fstclushi, self.wrttime, self.wrtdate,
288 self.fstcluslo, self.filesize)
289
290 return entry
291
292 def _add_parent(self, cls):
293 """Add parent directory link to current directory entry.
294
295 raises: PyFATException
296 """
297 if self._parent is not None:
298 raise PyFATException("Trying to add multiple parents to current "
299 "directory!", errno=errno.ETOOMANYREFS)
300
301 if not isinstance(cls, FATDirectoryEntry):
302 raise PyFATException("Trying to add a non-FAT directory entry "
303 "as parent directory!", errno=errno.EBADE)
304
305 self._parent = cls
306
307 def _get_parent_dir(self, sd):
308 """Build path name for recursive directory entries."""
309 name = self.__repr__()
310 if self.__repr__() == "/":
311 name = ""
312 sd += [name]
313
314 if self._parent is None:
315 return sd
316
317 return self._parent._get_parent_dir(sd)
318
319 def get_full_path(self):
320 """Iterate all parents up and join them by "/"."""
321 parent_dirs = [self.__repr__()]
322
323 if self._parent is None:
324 return "/"
325
326 return posixpath.join(*list(reversed(
327 self._parent._get_parent_dir(parent_dirs))))
328
329 def get_parent_dir(self):
330 """Get the parent directory entry."""
331 if self._parent is None:
332 raise PyFATException("Cannot query parent directory of "
333 "root directory", errno=errno.ENOENT)
334
335 return self._parent
336
337 def is_special(self):
338 """Determine if dir entry is a dot or dotdot entry.
339
340 :returns: Boolean value whether or not entry is
341 a dot or dotdot entry
342 """
343 return self.get_short_name() in [".", ".."]
344
345 def is_read_only(self):
346 """Determine if dir entry has read-only attribute set.
347
348 :returns: Boolean value indicating read-only attribute is set
349 """
350 return (self.ATTR_READ_ONLY & self.attr) > 0
351
352 def is_hidden(self):
353 """Determine if dir entry has the hidden attribute set.
354
355 :returns: Boolean value indicating hidden attribute is set
356 """
357 return (self.ATTR_HIDDEN & self.attr) > 0
358
359 def is_system(self):
360 """Determine if dir entry has the system file attribute set.
361
362 :returns: Boolean value indicating system attribute is set
363 """
364 return (self.ATTR_SYSTEM & self.attr) > 0
365
366 def is_volume_id(self):
367 """Determine if dir entry has the volume ID attribute set.
368
369 :returns: Boolean value indicating volume ID attribute is set
370 """
371 return (self.ATTR_VOLUME_ID & self.attr) > 0
372
373 def _verify_is_directory(self):
374 """Verify that current entry is a directory.
375
376 raises: PyFATException: If current entry is not a directory.
377 """
378 if not self.is_directory():
379 raise PyFATException("Cannot get entries of this entry, as "
380 "it is not a directory.",
381 errno=errno.ENOTDIR)
382
383 def is_directory(self):
384 """Determine if dir entry has directory attribute set.
385
386 :returns: Boolean value indicating directory attribute is set
387 """
388 return (self.ATTR_DIRECTORY & self.attr) > 0
389
390 def is_archive(self):
391 """Determine if dir entry has archive attribute set.
392
393 :returns: Boolean value indicating archive attribute is set
394 """
395 return (self.ATTR_ARCHIVE & self.attr) > 0
396
397 def is_empty(self):
398 """Determine if directory does not contain any directories."""
399 self._verify_is_directory()
400 self.__populate_dirs()
401
402 for d in self.__dirs:
403 if d.is_special():
404 continue
405 return False
406
407 return True
408
409 def __populate_dirs(self):
410 if self.__lazy_load is False:
411 return
412
413 clus = self.get_cluster()
414 self.__dirs = self.__fs.parse_dir_entries_in_cluster_chain(clus)
415 for dir_entry in self.__dirs:
416 dir_entry._add_parent(self)
417 self.__lazy_load = False
418
419 def _get_entries_raw(self):
420 """Get a full list of entries in current directory."""
421 self._verify_is_directory()
422 self.__populate_dirs()
423
424 return self.__dirs
425
426 def get_entries(self):
427 """Get entries of directory.
428
429 :raises: PyFatException: If entry is not a directory
430 :returns: tuple: root (current path, full),
431 dirs (all dirs), files (all files)
432 """
433 dirs = []
434 files = []
435 specials = []
436
437 for d in self._get_entries_raw():
438 if d.is_special() or d.is_volume_id():
439 # Volume IDs and dot/dotdot entries
440 specials += [d]
441 elif d.is_directory():
442 # Directories
443 dirs += [d]
444 else:
445 # Everything else must be a file
446 files += [d]
447
448 return dirs, files, specials
449
450 def _search_entry(self, name: str):
451 """Find given dir entry by walking current dir.
452
453 :param name: Name of entry to search for
454 :raises: PyFATException: If entry cannot be found
455 :returns: FATDirectoryEntry: Found entry
456 """
457 dirs, files, _ = self.get_entries()
458 for entry in dirs+files:
459 try:
460 if entry.get_long_name() == name:
461 return entry
462 except NotAnLFNEntryException:
463 pass
464 if entry.get_short_name() == name:
465 return entry
466
467 raise PyFATException(f'Cannot find entry {name}',
468 errno=errno.ENOENT)
469
470 def get_entry(self, path: str):
471 """Get sub-entry if current entry is a directory.
472
473 :param path: Relative path of entry to get
474 :raises: PyFATException: If entry cannot be found
475 :returns: FATDirectoryEntry: Found entry
476 """
477 entry = self
478 for segment in filter(None, path.split("/")):
479 entry._verify_is_directory()
480 entry = entry._search_entry(segment)
481 return entry
482
483 def walk(self):
484 """Walk all directory entries recursively.
485
486 :returns: tuple: root (current path, full),
487 dirs (all dirs), files (all files)
488 """
489 self._verify_is_directory()
490 self.__populate_dirs()
491
492 root = self.get_full_path()
493 dirs, files, _ = self.get_entries()
494
495 yield root, dirs, files
496 for d in self.__dirs:
497 if d.is_special():
498 # Ignore dot and dotdot
499 continue
500
501 if not d.is_directory():
502 continue
503
504 yield from d.walk()
505
506 def add_subdirectory(self, dir_entry, recursive: bool = True):
507 """Register a subdirectory in current directory entry.
508
509 :param dir_entry: FATDirectoryEntry
510 :raises: PyFATException: If current entry is not a directory or
511 given directory entry already has a parent
512 directory set
513 """
514 # Check if current dir entry is even a directory!
515 self._verify_is_directory()
516 self.__populate_dirs()
517
518 dir_entry._add_parent(self)
519 self.__dirs += [dir_entry]
520
521 def mark_empty(self):
522 """Mark this directory entry as empty."""
523 # Also mark LFN entries as empty
524 try:
525 self.lfn_entry.mark_empty()
526 except AttributeError:
527 pass
528
529 self.name.name[0] = self.FREE_DIR_ENTRY_MARK
530
531 def remove_dir_entry(self, name):
532 """Remove given dir_entry from dir list.
533
534 **NOTE:** This will also remove special entries such
535 as ».«, »..« and volume labels!
536 """
537 # Iterate all entries
538 for dir_entry in self._get_entries_raw():
539 sn = dir_entry.get_short_name()
540 try:
541 ln = dir_entry.get_long_name()
542 except NotAnLFNEntryException:
543 ln = None
544 if name in [sn, ln]:
545 self.__dirs.remove(dir_entry)
546 return
547
548 raise PyFATException(f"Cannot remove '{name}', no such "
549 f"file or directory!", errno=errno.ENOENT)
550
551 def __repr__(self):
552 """String-represent directory entry by (preferably) LFN.
553
554 :returns: str: Long file name if existing, 8DOT3 otherwise
555 """
556 try:
557 return self.get_long_name()
558 except NotAnLFNEntryException:
559 return self.get_short_name()
560
561 def get_short_name(self):
562 """Get short name of directory entry.
563
564 :returns: str: Name of directory entry
565 """
566 return self.name.get_unpadded_filename()
567
568 def get_long_name(self):
569 """Get long name of directory entry.
570
571 :raises: NotAnLFNEntryException: If entry has no long file name
572 :returns: str: Long file name of directory entry
573 """
574 if self.lfn_entry is None:
575 raise NotAnLFNEntryException("No LFN entry found for this "
576 "dir entry.")
577
578 return str(self.lfn_entry)
579
580
581class FATLongDirectoryEntry(object):
582 """Represents long file name (LFN) entries."""
583
584 #: LFN entry header layout in struct formatted string
585 FAT_LONG_DIRECTORY_LAYOUT = "<B10sBBB12sH4s"
586 #: LFN header fields when extracted with `FAT_LONG_DIRECTORY_LAYOUT`
587 FAT_LONG_DIRECTORY_VARS = ["LDIR_Ord", "LDIR_Name1", "LDIR_Attr",
588 "LDIR_Type", "LDIR_Chksum", "LDIR_Name2",
589 "LDIR_FstClusLO", "LDIR_Name3"]
590 #: Ordinance of last LFN entry in a chain
591 LAST_LONG_ENTRY = 0x40
592 #: Length for long file name in bytes per entry
593 LFN_ENTRY_LENGTH = 26
594
595 def __init__(self):
596 """Initialize empty LFN directory entry object."""
597 self.lfn_entries = {}
598
599 def get_entries(self, reverse: bool = False):
600 """Get LFS entries in correct order (based on `LDIR_Ord`).
601
602 :param reverse: `bool`: Returns LFN entries in reversed order.
603 This is required for byte representation.
604 """
605 for _, e in sorted(self.lfn_entries.items(),
606 key=lambda x: x[1]["LDIR_Ord"],
607 reverse=reverse):
608 yield e
609
610 def mark_empty(self):
611 """Mark LFN entry as empty."""
612 free_dir_entry_mark = FATDirectoryEntry.FREE_DIR_ENTRY_MARK
613 for k in self.lfn_entries.keys():
614 self.lfn_entries[k]["LDIR_Ord"] = free_dir_entry_mark
615
616 def __bytes__(self):
617 """Represent LFN entries as bytes."""
618 entries_bytes = b""
619 for e in self.get_entries(reverse=True):
620 entries_bytes += struct.pack(self.FAT_LONG_DIRECTORY_LAYOUT,
621 e["LDIR_Ord"], e["LDIR_Name1"],
622 e["LDIR_Attr"], e["LDIR_Type"],
623 e["LDIR_Chksum"], e["LDIR_Name2"],
624 e["LDIR_FstClusLO"], e["LDIR_Name3"])
625 return entries_bytes
626
627 def __str__(self):
628 """Remove padding from LFN entry and decode it.
629
630 :returns: `str` decoded string of filename
631 """
632 name = b''
633
634 for e in self.get_entries():
635 for h in ["LDIR_Name1", "LDIR_Name2", "LDIR_Name3"]:
636 name += e[h]
637
638 while name.endswith(b'\xFF\xFF'):
639 name = name[:-2]
640
641 name = name.decode(FAT_LFN_ENCODING)
642
643 if name.endswith('\0'):
644 name = name[:-1]
645
646 return name
647
648 @staticmethod
649 def is_lfn_entry(LDIR_Ord, LDIR_Attr):
650 """Verify that entry is an LFN entry.
651
652 :param LDIR_Ord: First byte of the directory header, ordinance
653 :param LDIR_Attr: Attributes segment of directory header
654 :returns: `True` if entry is a valid LFN entry
655 """
656 lfn_attr = FATDirectoryEntry.ATTR_LONG_NAME
657 lfn_attr_mask = FATDirectoryEntry.ATTR_LONG_NAME_MASK
658 is_attr_set = (LDIR_Attr & lfn_attr_mask) == lfn_attr
659
660 return is_attr_set and \
661 LDIR_Ord != FATDirectoryEntry.FREE_DIR_ENTRY_MARK
662
663 def add_lfn_entry(self, LDIR_Ord, LDIR_Name1, LDIR_Attr, LDIR_Type,
664 LDIR_Chksum, LDIR_Name2, LDIR_FstClusLO, LDIR_Name3):
665 """Add LFN entry to this instances chain.
666
667 :param LDIR_Ord: Ordinance of LFN entry
668 :param LDIR_Name1: First name field of LFN entry
669 :param LDIR_Attr: Attributes of LFN entry
670 :param LDIR_Type: Type of LFN entry
671 :param LDIR_Chksum: Checksum value of following 8dot3 entry
672 :param LDIR_Name2: Second name field of LFN entry
673 :param LDIR_FstClusLO: Cluster address of LFN entry. Always zero.
674 :param LDIR_Name3: Third name field of LFN entry
675 """
676 # Check if attribute matches
677 if not self.is_lfn_entry(LDIR_Ord, LDIR_Attr):
678 raise NotAnLFNEntryException("Given LFN entry is not a long "
679 "file name entry or attribute "
680 "not set correctly!")
681
682 # Check if FstClusLO is 0, as required by the spec
683 if LDIR_FstClusLO != 0:
684 raise PyFATException("Given LFN entry has an invalid first "
685 "cluster ID, don't know what to do.",
686 errno=errno.EFAULT)
687
688 # Check if item with same index has already been added
689 if LDIR_Ord in self.lfn_entries.keys():
690 raise PyFATException("Given LFN entry part with index \'{}\'"
691 "has already been added to LFN "
692 "entry list.".format(LDIR_Ord))
693
694 mapped_entries = dict(zip(self.FAT_LONG_DIRECTORY_VARS,
695 (LDIR_Ord, LDIR_Name1, LDIR_Attr, LDIR_Type,
696 LDIR_Chksum, LDIR_Name2, LDIR_FstClusLO,
697 LDIR_Name3)))
698
699 self.lfn_entries[LDIR_Ord] = mapped_entries
700
701 def is_lfn_entry_complete(self):
702 """Verify that LFN object forms a complete chain.
703
704 :returns: `True` if `LAST_LONG_ENTRY` is found
705 """
706 for k in self.lfn_entries.keys():
707 if (int(k) & self.LAST_LONG_ENTRY) == self.LAST_LONG_ENTRY:
708 return True
709
710 return False
711
712
713def make_lfn_entry(dir_name: str,
714 short_name: EightDotThree):
715 """Generate a `FATLongDirectoryEntry` instance from directory name.
716
717 :param dir_name: Long name of directory
718 :param short_name: `EightDotThree` class instance
719 :raises: `PyFATException` if entry name does not require an LFN
720 entry or the name exceeds the FAT limitation of 255 characters
721 """
722 lfn_entry = FATLongDirectoryEntry()
723 #: Length in bytes of an LFN entry
724 lfn_entry_length = 26
725 dir_name_str = dir_name
726 dir_name = dir_name.encode(FAT_LFN_ENCODING)
727 dir_name_modulus = len(dir_name) % lfn_entry_length
728
729 if EightDotThree.is_8dot3_conform(dir_name_str,
730 encoding=short_name.encoding):
731 raise PyFATException("Directory entry is already 8.3 conform, "
732 "no need to create an LFN entry.",
733 errno=errno.EINVAL)
734
735 if len(dir_name) > 255:
736 raise PyFATException("Long file name exceeds 255 "
737 "characters, not supported.",
738 errno=errno.ENAMETOOLONG)
739
740 checksum = short_name.checksum()
741
742 if dir_name_modulus != 0:
743 # Null-terminate string if required
744 dir_name += '\0'.encode(FAT_LFN_ENCODING)
745
746 # Fill the rest with 0xFF if it doesn't fit evenly
747 new_sz = lfn_entry_length - len(dir_name)
748 new_sz %= lfn_entry_length
749 new_sz += len(dir_name)
750 dir_name += b'\xFF' * (new_sz - len(dir_name))
751
752 # Generate linked LFN entries
753 lfn_entries = len(dir_name) // lfn_entry_length
754 for i in range(lfn_entries):
755 if i == lfn_entries-1:
756 lfn_entry_ord = 0x40 | i+1
757 else:
758 lfn_entry_ord = i+1
759
760 n = i*lfn_entry_length
761 dirname1 = dir_name[n:n+10]
762 n += 10
763 dirname2 = dir_name[n:n+12]
764 n += 12
765 dirname3 = dir_name[n:n+4]
766
767 lfn_entry.add_lfn_entry(LDIR_Ord=lfn_entry_ord,
768 LDIR_Name1=dirname1,
769 LDIR_Attr=FATDirectoryEntry.ATTR_LONG_NAME,
770 LDIR_Type=0x00,
771 LDIR_Chksum=checksum,
772 LDIR_Name2=dirname2,
773 LDIR_FstClusLO=0,
774 LDIR_Name3=dirname3)
775 return lfn_entry