diff --git a/DiscImageChef.Filesystems/FAT/Consts.cs b/DiscImageChef.Filesystems/FAT/Consts.cs index f4e332178..beae51463 100644 --- a/DiscImageChef.Filesystems/FAT/Consts.cs +++ b/DiscImageChef.Filesystems/FAT/Consts.cs @@ -83,6 +83,15 @@ namespace DiscImageChef.Filesystems.FAT const ushort EADATA_MAGIC = 0x4445; const ushort EASCTR_MAGIC = 0x4145; const ushort EA_UNUSED = 0xFFFF; + const ushort EAT_BINARY = 0xFFFE; + const ushort EAT_ASCII = 0xFFFD; + const ushort EAT_BITMAP = 0xFFFB; + const ushort EAT_METAFILE = 0xFFFA; + const ushort EAT_ICON = 0xFFF9; + const ushort EAT_EA = 0xFFEE; + const ushort EAT_MVMT = 0xFFDF; + const ushort EAT_MVST = 0xFFDE; + const ushort EAT_ASN1 = 0xFFDD; readonly (string hash, string name)[] knownBootHashes = { @@ -216,7 +225,9 @@ namespace DiscImageChef.Filesystems.FAT { Dos, Nt, - Lfn + Lfn, + Os2, + Ecs } [Flags] diff --git a/DiscImageChef.Filesystems/FAT/Dir.cs b/DiscImageChef.Filesystems/FAT/Dir.cs index e3a866fa6..93d630e7a 100644 --- a/DiscImageChef.Filesystems/FAT/Dir.cs +++ b/DiscImageChef.Filesystems/FAT/Dir.cs @@ -122,8 +122,9 @@ namespace DiscImageChef.Filesystems.FAT } currentDirectory = new Dictionary(); - byte[] lastLfnName = null; - byte lastLfnChecksum = 0; + byte[] lastLfnName = null; + byte lastLfnChecksum = 0; + List LFNs = new List(); for(int pos = 0; pos < directoryBuffer.Length; pos += Marshal.SizeOf()) { @@ -187,6 +188,7 @@ namespace DiscImageChef.Filesystems.FAT { filename = StringHandlers.CToString(lastLfnName, Encoding.Unicode, true); + LFNs.Add(filename); currentDirectory[filename] = dirent; lastLfnName = null; lastLfnChecksum = 0; @@ -217,6 +219,45 @@ namespace DiscImageChef.Filesystems.FAT currentDirectory[filename] = dirent; } + // Check OS/2 .LONGNAME + if(eaCache != null && (@namespace == Namespace.Os2 || @namespace == Namespace.Ecs)) + { + List> filesWithEas = + currentDirectory.Where(t => t.Value.ea_handle != 0).ToList(); + + foreach(KeyValuePair fileWithEa in filesWithEas) + { + // This ensures LFN takes preference when eCS is in use + if(LFNs.Contains(fileWithEa.Key)) continue; + + Dictionary eas = GetEas(fileWithEa.Value.ea_handle); + + if(eas is null) continue; + + if(!eas.TryGetValue("com.microsoft.os2.longname", out byte[] longnameEa)) continue; + + if(BitConverter.ToUInt16(longnameEa, 0) != EAT_ASCII) continue; + + ushort longnameSize = BitConverter.ToUInt16(longnameEa, 2); + + if(longnameSize + 4 > longnameEa.Length) continue; + + byte[] longnameBytes = new byte[longnameSize]; + + Array.Copy(longnameEa, 4, longnameBytes, 0, longnameSize); + + string longname = StringHandlers.CToString(longnameBytes, Encoding); + + if(string.IsNullOrWhiteSpace(longname)) continue; + + // Forward slash is allowed in .LONGNAME, so change it to visually similar division slash + longname = longname.Replace('/', '\u2215'); + + currentDirectory.Remove(fileWithEa.Key); + currentDirectory[longname] = fileWithEa.Value; + } + } + directoryCache.Add(currentPath, currentDirectory); } diff --git a/DiscImageChef.Filesystems/FAT/FAT.cs b/DiscImageChef.Filesystems/FAT/FAT.cs index b1fa15e76..f1271db3a 100644 --- a/DiscImageChef.Filesystems/FAT/FAT.cs +++ b/DiscImageChef.Filesystems/FAT/FAT.cs @@ -80,7 +80,9 @@ namespace DiscImageChef.Filesystems.FAT { {"dos", "DOS (8.3 all uppercase)"}, {"nt", "Windows NT (8.3 mixed case)"}, - {"lfn", "Long file names (default)"} + {"os2", "OS/2 .LONGNAME extended attribute"}, + {"ecs", "Use LFN when available with fallback to .LONGNAME (default)"}, + {"lfn", "Long file names"} }; static Dictionary GetDefaultOptions() => diff --git a/DiscImageChef.Filesystems/FAT/Super.cs b/DiscImageChef.Filesystems/FAT/Super.cs index 29d77aef3..5952abb81 100644 --- a/DiscImageChef.Filesystems/FAT/Super.cs +++ b/DiscImageChef.Filesystems/FAT/Super.cs @@ -66,7 +66,7 @@ namespace DiscImageChef.Filesystems.FAT if(options.TryGetValue("debug", out string debugString)) bool.TryParse(debugString, out debug); // Default namespace - if(@namespace is null) @namespace = "lfn"; + if(@namespace is null) @namespace = "ecs"; switch(@namespace.ToLowerInvariant()) { @@ -76,6 +76,12 @@ namespace DiscImageChef.Filesystems.FAT case "nt": this.@namespace = Namespace.Nt; break; + case "os2": + this.@namespace = Namespace.Os2; + break; + case "ecs": + this.@namespace = Namespace.Ecs; + break; case "lfn": this.@namespace = Namespace.Lfn; break; @@ -399,8 +405,9 @@ namespace DiscImageChef.Filesystems.FAT if(rootDirectory is null) return Errno.InvalidArgument; - byte[] lastLfnName = null; - byte lastLfnChecksum = 0; + byte[] lastLfnName = null; + byte lastLfnChecksum = 0; + List LFNs = new List(); for(int i = 0; i < rootDirectory.Length; i += Marshal.SizeOf()) { @@ -412,7 +419,7 @@ namespace DiscImageChef.Filesystems.FAT if(entry.attributes.HasFlag(FatAttributes.LFN)) { - if(this.@namespace != Namespace.Lfn) continue; + if(this.@namespace != Namespace.Lfn && this.@namespace != Namespace.Ecs) continue; LfnEntry lfnEntry = Marshal.ByteArrayToStructureLittleEndian(rootDirectory, i, @@ -482,7 +489,7 @@ namespace DiscImageChef.Filesystems.FAT continue; } - if(this.@namespace == Namespace.Lfn && lastLfnName != null) + if((this.@namespace == Namespace.Lfn || this.@namespace == Namespace.Ecs) && lastLfnName != null) { byte calculatedLfnChecksum = LfnChecksum(entry.filename, entry.extension); @@ -490,6 +497,7 @@ namespace DiscImageChef.Filesystems.FAT { filename = StringHandlers.CToString(lastLfnName, Encoding.Unicode, true); + LFNs.Add(filename); rootDirectoryCache[filename] = entry; lastLfnName = null; lastLfnChecksum = 0; @@ -616,6 +624,45 @@ namespace DiscImageChef.Filesystems.FAT else eaCache = new Dictionary>(); } + // Check OS/2 .LONGNAME + if(eaCache != null && (this.@namespace == Namespace.Os2 || this.@namespace == Namespace.Ecs)) + { + List> rootFilesWithEas = + rootDirectoryCache.Where(t => t.Value.ea_handle != 0).ToList(); + + foreach(KeyValuePair fileWithEa in rootFilesWithEas) + { + // This ensures LFN takes preference when eCS is in use + if(LFNs.Contains(fileWithEa.Key)) continue; + + Dictionary eas = GetEas(fileWithEa.Value.ea_handle); + + if(eas is null) continue; + + if(!eas.TryGetValue("com.microsoft.os2.longname", out byte[] longnameEa)) continue; + + if(BitConverter.ToUInt16(longnameEa, 0) != EAT_ASCII) continue; + + ushort longnameSize = BitConverter.ToUInt16(longnameEa, 2); + + if(longnameSize + 4 > longnameEa.Length) continue; + + byte[] longnameBytes = new byte[longnameSize]; + + Array.Copy(longnameEa, 4, longnameBytes, 0, longnameSize); + + string longname = StringHandlers.CToString(longnameBytes, Encoding); + + if(string.IsNullOrWhiteSpace(longname)) continue; + + // Forward slash is allowed in .LONGNAME, so change it to visually similar division slash + longname = longname.Replace('/', '\u2215'); + + rootDirectoryCache.Remove(fileWithEa.Key); + rootDirectoryCache[longname] = fileWithEa.Value; + } + } + mounted = true; return Errno.NoError; } diff --git a/DiscImageChef.Filesystems/FAT/Xattr.cs b/DiscImageChef.Filesystems/FAT/Xattr.cs index 38e8f3861..033f3cbbd 100644 --- a/DiscImageChef.Filesystems/FAT/Xattr.cs +++ b/DiscImageChef.Filesystems/FAT/Xattr.cs @@ -74,58 +74,9 @@ namespace DiscImageChef.Filesystems.FAT if(entry.ea_handle == 0) return Errno.NoError; - int aIndex = entry.ea_handle >> 7; - // First 0x20 bytes are the magic number and unused words - ushort a = BitConverter.ToUInt16(cachedEaData, aIndex * 2 + 0x20); + eas = GetEas(entry.ea_handle); - ushort b = BitConverter.ToUInt16(cachedEaData, entry.ea_handle * 2 + 0x200); - - uint eaCluster = (uint)(a + b); - - if(b == EA_UNUSED) return Errno.NoError; - - EaHeader header = - Marshal.ByteArrayToStructureLittleEndian(cachedEaData, (int)(eaCluster * bytesPerCluster), - Marshal.SizeOf()); - - if(header.magic != 0x4145) return Errno.NoError; - - uint eaLen = BitConverter.ToUInt32(cachedEaData, - (int)(eaCluster * bytesPerCluster) + Marshal.SizeOf()); - - byte[] eaData = new byte[eaLen]; - Array.Copy(cachedEaData, (int)(eaCluster * bytesPerCluster) + Marshal.SizeOf(), eaData, 0, eaLen); - - eas = new Dictionary(); - - if(true) eas.Add("com.microsoft.os2.fea", eaData); - - int pos = 4; - while(pos < eaData.Length) - { - byte fEA = eaData[pos++]; - byte cbName = eaData[pos++]; - ushort cbValue = BitConverter.ToUInt16(eaData, pos); - pos += 2; - - string name = Encoding.ASCII.GetString(eaData, pos, cbName); - pos += cbName; - pos++; - byte[] data = new byte[cbValue]; - - Array.Copy(eaData, pos, data, 0, cbValue); - pos += cbValue; - - // OS/2 System Attributes - if(name[0] == '.') - { - // This is WorkPlace System information so it's IBM - if(name == ".CLASSINFO") name = "com.ibm.os2.classinfo"; - else name = "com.microsoft.os2" + name.ToLower(); - } - - eas.Add(name, data); - } + if(eas is null) return Errno.NoError; eaCache.Add(path.ToLower(cultureInfo), eas); xattrs = eas.Keys.ToList(); @@ -162,6 +113,64 @@ namespace DiscImageChef.Filesystems.FAT return Errno.NoError; } + Dictionary GetEas(ushort eaHandle) + { + int aIndex = eaHandle >> 7; + // First 0x20 bytes are the magic number and unused words + ushort a = BitConverter.ToUInt16(cachedEaData, aIndex * 2 + 0x20); + + ushort b = BitConverter.ToUInt16(cachedEaData, eaHandle * 2 + 0x200); + + uint eaCluster = (uint)(a + b); + + if(b == EA_UNUSED) return null; + + EaHeader header = + Marshal.ByteArrayToStructureLittleEndian(cachedEaData, (int)(eaCluster * bytesPerCluster), + Marshal.SizeOf()); + + if(header.magic != 0x4145) return null; + + uint eaLen = BitConverter.ToUInt32(cachedEaData, + (int)(eaCluster * bytesPerCluster) + Marshal.SizeOf()); + + byte[] eaData = new byte[eaLen]; + Array.Copy(cachedEaData, (int)(eaCluster * bytesPerCluster) + Marshal.SizeOf(), eaData, 0, eaLen); + + Dictionary eas = new Dictionary(); + + if(debug) eas.Add("com.microsoft.os2.fea", eaData); + + int pos = 4; + while(pos < eaData.Length) + { + byte fEA = eaData[pos++]; + byte cbName = eaData[pos++]; + ushort cbValue = BitConverter.ToUInt16(eaData, pos); + pos += 2; + + string name = Encoding.ASCII.GetString(eaData, pos, cbName); + pos += cbName; + pos++; + byte[] data = new byte[cbValue]; + + Array.Copy(eaData, pos, data, 0, cbValue); + pos += cbValue; + + // OS/2 System Attributes + if(name[0] == '.') + { + // This is WorkPlace System information so it's IBM + if(name == ".CLASSINFO") name = "com.ibm.os2.classinfo"; + else name = "com.microsoft.os2" + name.ToLower(); + } + + eas.Add(name, data); + } + + return eas; + } + void CacheEaData() { if(eaDirEntry.start_cluster == 0) return;