diff --git a/DiscImageChef.Filesystems/FAT/Dir.cs b/DiscImageChef.Filesystems/FAT/Dir.cs index 8a89c9d5b..815f9b8ba 100644 --- a/DiscImageChef.Filesystems/FAT/Dir.cs +++ b/DiscImageChef.Filesystems/FAT/Dir.cs @@ -32,7 +32,10 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using DiscImageChef.CommonTypes.Structs; +using DiscImageChef.Helpers; namespace DiscImageChef.Filesystems.FAT { @@ -59,7 +62,118 @@ namespace DiscImageChef.Filesystems.FAT contents = null; if(!mounted) return Errno.AccessDenied; - throw new NotImplementedException(); + if(string.IsNullOrWhiteSpace(path) || path == "/") + { + contents = rootDirectoryCache.Keys.ToList(); + return Errno.NoError; + } + + string cutPath = path.StartsWith("/", StringComparison.Ordinal) + ? path.Substring(1).ToLower(cultureInfo) + : path.ToLower(cultureInfo); + + if(directoryCache.TryGetValue(cutPath, out Dictionary currentDirectory)) + { + contents = currentDirectory.Keys.ToList(); + return Errno.NoError; + } + + string[] pieces = cutPath.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); + + KeyValuePair entry = + rootDirectoryCache.FirstOrDefault(t => t.Key.ToLower(cultureInfo) == pieces[0]); + + if(string.IsNullOrEmpty(entry.Key)) return Errno.NoSuchFile; + + if(!entry.Value.attributes.HasFlag(FatAttributes.Subdirectory)) return Errno.NotDirectory; + + string currentPath = pieces[0]; + + currentDirectory = rootDirectoryCache; + + for(int p = 0; p < pieces.Length; p++) + { + entry = currentDirectory.FirstOrDefault(t => t.Key.ToLower(cultureInfo) == pieces[p]); + + if(string.IsNullOrEmpty(entry.Key)) return Errno.NoSuchFile; + + if(!entry.Value.attributes.HasFlag(FatAttributes.Subdirectory)) return Errno.NotDirectory; + + currentPath = p == 0 ? pieces[0] : $"{currentPath}/{pieces[p]}"; + uint currentCluster = entry.Value.start_cluster; + + if(directoryCache.TryGetValue(currentPath, out currentDirectory)) continue; + + uint[] clusters = GetClusters(currentCluster); + + if(clusters is null) return Errno.InvalidArgument; + + byte[] directoryBuffer = new byte[bytesPerCluster * clusters.Length]; + + for(int i = 0; i < clusters.Length; i++) + { + byte[] buffer = image.ReadSectors(firstClusterSector + (clusters[i] - 2) * sectorsPerCluster, + sectorsPerCluster); + Array.Copy(buffer, 0, directoryBuffer, i * bytesPerCluster, bytesPerCluster); + } + + currentDirectory = new Dictionary(); + + int pos = 0; + while(pos < directoryBuffer.Length) + { + DirectoryEntry dirent = + Marshal.ByteArrayToStructureLittleEndian(directoryBuffer, pos, + Marshal.SizeOf()); + + pos += Marshal.SizeOf(); + + if(dirent.filename[0] == DIRENT_FINISHED) break; + + // Not a correct entry + if(dirent.filename[0] < DIRENT_MIN && dirent.filename[0] != DIRENT_E5) continue; + + // Self + if(Encoding.GetString(dirent.filename).TrimEnd() == ".") continue; + + // Parent + if(Encoding.GetString(dirent.filename).TrimEnd() == "..") continue; + + // Deleted + if(dirent.filename[0] == DIRENT_DELETED) continue; + + // TODO: LFN namespace + if(dirent.attributes.HasFlag(FatAttributes.LFN)) continue; + + string filename; + + if(dirent.attributes.HasFlag(FatAttributes.VolumeLabel)) continue; + + if(dirent.filename[0] == DIRENT_E5) dirent.filename[0] = DIRENT_DELETED; + + string name = Encoding.GetString(dirent.filename).TrimEnd(); + string extension = Encoding.GetString(dirent.extension).TrimEnd(); + + if((dirent.caseinfo & FASTFAT_LOWERCASE_EXTENSION) > 0) + extension = extension.ToLower(CultureInfo.CurrentCulture); + + if((dirent.caseinfo & FASTFAT_LOWERCASE_BASENAME) > 0) + name = name.ToLower(CultureInfo.CurrentCulture); + + if(extension != "") filename = name + "." + extension; + else filename = name; + + // Using array accessor ensures that repeated entries just get substituted. + // Repeated entries are not allowed but some bad implementations (e.g. FAT32.IFS)allow to create them + // when using spaces + currentDirectory[filename] = dirent; + } + + directoryCache.Add(currentPath, currentDirectory); + } + + contents = currentDirectory?.Keys.ToList(); + return Errno.NoError; } } } \ No newline at end of file diff --git a/DiscImageChef.Filesystems/FAT/FAT.cs b/DiscImageChef.Filesystems/FAT/FAT.cs index cea99a33f..04891d636 100644 --- a/DiscImageChef.Filesystems/FAT/FAT.cs +++ b/DiscImageChef.Filesystems/FAT/FAT.cs @@ -32,6 +32,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using DiscImageChef.CommonTypes.Interfaces; using DiscImageChef.CommonTypes.Structs; @@ -43,19 +44,22 @@ namespace DiscImageChef.Filesystems.FAT // X68K uses cdate/adate from direntry for extending filename public partial class FAT : IReadOnlyFilesystem { - bool debug; - bool fat12; - bool fat16; - bool fat32; - ulong fatFirstSector; - ulong firstClusterSector; - bool mounted; - uint reservedSectors; - Dictionary rootDirectoryCache; - uint sectorsPerCluster; - uint sectorsPerFat; - bool useFirstFat; - FileSystemInfo statfs; + uint bytesPerCluster; + CultureInfo cultureInfo; + bool debug; + Dictionary> directoryCache; + bool fat12; + bool fat16; + bool fat32; + ulong fatFirstSector; + ulong firstClusterSector; + bool mounted; + uint reservedSectors; + Dictionary rootDirectoryCache; + uint sectorsPerCluster; + uint sectorsPerFat; + FileSystemInfo statfs; + bool useFirstFat; public FileSystemType XmlFsType { get; private set; } diff --git a/DiscImageChef.Filesystems/FAT/Super.cs b/DiscImageChef.Filesystems/FAT/Super.cs index d42a364b9..cb95d5023 100644 --- a/DiscImageChef.Filesystems/FAT/Super.cs +++ b/DiscImageChef.Filesystems/FAT/Super.cs @@ -94,7 +94,6 @@ namespace DiscImageChef.Filesystems.FAT FreeBlocks = 0 // Requires traversing the FAT }; - // This is needed because for FAT16, GEMDOS increases bytes per sector count instead of using big_sectors field. uint sectorsPerRealSector = 1; // This is needed because some OSes don't put volume label as first entry in the root directory @@ -145,7 +144,7 @@ namespace DiscImageChef.Filesystems.FAT sectorsPerFat = fat32Bpb.big_spfat; XmlFsType.VolumeSerial = $"{fat32Bpb.serial_no:X8}"; - statfs.Id = new FileSystemId {IsInt = true, Serial32 = fat32Bpb.serial_no}; + statfs.Id = new FileSystemId {IsInt = true, Serial32 = fat32Bpb.serial_no}; if((fat32Bpb.flags & 0xF8) == 0x00) if((fat32Bpb.flags & 0x01) == 0x01) @@ -250,7 +249,12 @@ namespace DiscImageChef.Filesystems.FAT { XmlFsType.VolumeSerial = $"{atariBpb.serial_no[0]:X2}{atariBpb.serial_no[1]:X2}{atariBpb.serial_no[2]:X2}"; - statfs.Id = new FileSystemId {IsInt = true, Serial32 = (uint)((atariBpb.serial_no[0] << 16) + (atariBpb.serial_no[1] << 8) + atariBpb.serial_no[2])}; + statfs.Id = new FileSystemId + { + IsInt = true, + Serial32 = (uint)((atariBpb.serial_no[0] << 16) + (atariBpb.serial_no[1] << 8) + + atariBpb.serial_no[2]) + }; } XmlFsType.SystemIdentifier = StringHandlers.CToString(atariBpb.oem_name); @@ -283,7 +287,7 @@ namespace DiscImageChef.Filesystems.FAT if(fakeBpb.signature == 0x28 || fakeBpb.signature == 0x29) { XmlFsType.VolumeSerial = $"{fakeBpb.serial_no:X8}"; - statfs.Id = new FileSystemId {IsInt = true, Serial32 = fakeBpb.serial_no}; + statfs.Id = new FileSystemId {IsInt = true, Serial32 = fakeBpb.serial_no}; } } @@ -377,17 +381,25 @@ namespace DiscImageChef.Filesystems.FAT if(rootDirectory is null) return Errno.InvalidArgument; - for(int i = 0; i < rootDirectory.Length; i += 32) + for(int i = 0; i < rootDirectory.Length; i += Marshal.SizeOf()) { - DirectoryEntry entry = Marshal.ByteArrayToStructureLittleEndian(rootDirectory, i, 32); + DirectoryEntry entry = + Marshal.ByteArrayToStructureLittleEndian(rootDirectory, i, + Marshal.SizeOf()); if(entry.filename[0] == DIRENT_FINISHED) break; // Not a correct entry if(entry.filename[0] < DIRENT_MIN && entry.filename[0] != DIRENT_E5) continue; - // Deleted or subdirectory entry - if(entry.filename[0] == DIRENT_SUBDIR || entry.filename[0] == DIRENT_DELETED) continue; + // Self + if(Encoding.GetString(entry.filename).TrimEnd() == ".") continue; + + // Parent + if(Encoding.GetString(entry.filename).TrimEnd() == "..") continue; + + // Deleted + if(entry.filename[0] == DIRENT_DELETED) continue; // TODO: LFN namespace if(entry.attributes.HasFlag(FatAttributes.LFN)) continue; @@ -422,8 +434,8 @@ namespace DiscImageChef.Filesystems.FAT if(entry.filename[0] == DIRENT_E5) entry.filename[0] = DIRENT_DELETED; - string name = Encoding.GetString(entry.filename).Trim(); - string extension = Encoding.GetString(entry.extension).Trim(); + string name = Encoding.GetString(entry.filename).TrimEnd(); + string extension = Encoding.GetString(entry.extension).TrimEnd(); if((entry.caseinfo & FASTFAT_LOWERCASE_EXTENSION) > 0) extension = extension.ToLower(CultureInfo.CurrentCulture); @@ -437,40 +449,53 @@ namespace DiscImageChef.Filesystems.FAT } XmlFsType.VolumeName = XmlFsType.VolumeName?.Trim(); - mounted = true; - - statfs.Blocks = XmlFsType.Clusters; + statfs.Blocks = XmlFsType.Clusters; switch(bpbKind) { case BpbKind.Hardcoded: statfs.Type = $"Microsoft FAT{(fat16 ? "16" : "12")}"; break; - case BpbKind.Atari: statfs.Type = $"Atari FAT{(fat16 ? "16" : "12")}"; + case BpbKind.Atari: + statfs.Type = $"Atari FAT{(fat16 ? "16" : "12")}"; break; - case BpbKind.Msx: statfs.Type = $"MSX FAT{(fat16 ? "16" : "12")}"; + case BpbKind.Msx: + statfs.Type = $"MSX FAT{(fat16 ? "16" : "12")}"; break; case BpbKind.Dos2: case BpbKind.Dos3: case BpbKind.Dos32: case BpbKind.Dos33: case BpbKind.ShortExtended: - case BpbKind.Extended: statfs.Type = $"Microsoft FAT{(fat16 ? "16" : "12")}"; + case BpbKind.Extended: + statfs.Type = $"Microsoft FAT{(fat16 ? "16" : "12")}"; break; case BpbKind.ShortFat32: - case BpbKind.LongFat32: statfs.Type = XmlFsType.Type == "FAT+" ? "FAT+" : "Microsoft FAT32"; + case BpbKind.LongFat32: + statfs.Type = XmlFsType.Type == "FAT+" ? "FAT+" : "Microsoft FAT32"; break; - case BpbKind.Andos: statfs.Type = $"ANDOS FAT{(fat16 ? "16" : "12")}"; + case BpbKind.Andos: + statfs.Type = $"ANDOS FAT{(fat16 ? "16" : "12")}"; break; - case BpbKind.Apricot: statfs.Type = $"Apricot FAT{(fat16 ? "16" : "12")}"; + case BpbKind.Apricot: + statfs.Type = $"Apricot FAT{(fat16 ? "16" : "12")}"; break; - case BpbKind.DecRainbow: statfs.Type = $"DEC FAT{(fat16 ? "16" : "12")}"; + case BpbKind.DecRainbow: + statfs.Type = $"DEC FAT{(fat16 ? "16" : "12")}"; break; - case BpbKind.Human: statfs.Type = $"Human FAT{(fat16 ? "16" : "12")}"; + case BpbKind.Human: + statfs.Type = $"Human FAT{(fat16 ? "16" : "12")}"; break; default: throw new ArgumentOutOfRangeException(); } + bytesPerCluster = sectorsPerCluster * imagePlugin.Info.SectorSize; + + // TODO: Check how this affects international filenames + cultureInfo = new CultureInfo("en-US", false); + directoryCache = new Dictionary>(); + + mounted = true; return Errno.NoError; }