Implement OS/2 and eCS namespaces using .LONGNAME EA in FAT12 and FAT16.

This commit is contained in:
2019-04-27 22:21:13 +01:00
parent fe4bcd63f2
commit b2c008eb02
5 changed files with 170 additions and 60 deletions

View File

@@ -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]

View File

@@ -122,8 +122,9 @@ namespace DiscImageChef.Filesystems.FAT
}
currentDirectory = new Dictionary<string, DirectoryEntry>();
byte[] lastLfnName = null;
byte lastLfnChecksum = 0;
byte[] lastLfnName = null;
byte lastLfnChecksum = 0;
List<string> LFNs = new List<string>();
for(int pos = 0; pos < directoryBuffer.Length; pos += Marshal.SizeOf<DirectoryEntry>())
{
@@ -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<KeyValuePair<string, DirectoryEntry>> filesWithEas =
currentDirectory.Where(t => t.Value.ea_handle != 0).ToList();
foreach(KeyValuePair<string, DirectoryEntry> fileWithEa in filesWithEas)
{
// This ensures LFN takes preference when eCS is in use
if(LFNs.Contains(fileWithEa.Key)) continue;
Dictionary<string, byte[]> 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);
}

View File

@@ -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<string, string> GetDefaultOptions() =>

View File

@@ -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<string> LFNs = new List<string>();
for(int i = 0; i < rootDirectory.Length; i += Marshal.SizeOf<DirectoryEntry>())
{
@@ -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<LfnEntry>(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<string, Dictionary<string, byte[]>>();
}
// Check OS/2 .LONGNAME
if(eaCache != null && (this.@namespace == Namespace.Os2 || this.@namespace == Namespace.Ecs))
{
List<KeyValuePair<string, DirectoryEntry>> rootFilesWithEas =
rootDirectoryCache.Where(t => t.Value.ea_handle != 0).ToList();
foreach(KeyValuePair<string, DirectoryEntry> fileWithEa in rootFilesWithEas)
{
// This ensures LFN takes preference when eCS is in use
if(LFNs.Contains(fileWithEa.Key)) continue;
Dictionary<string, byte[]> 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;
}

View File

@@ -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<EaHeader>(cachedEaData, (int)(eaCluster * bytesPerCluster),
Marshal.SizeOf<EaHeader>());
if(header.magic != 0x4145) return Errno.NoError;
uint eaLen = BitConverter.ToUInt32(cachedEaData,
(int)(eaCluster * bytesPerCluster) + Marshal.SizeOf<EaHeader>());
byte[] eaData = new byte[eaLen];
Array.Copy(cachedEaData, (int)(eaCluster * bytesPerCluster) + Marshal.SizeOf<EaHeader>(), eaData, 0, eaLen);
eas = new Dictionary<string, byte[]>();
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<string, byte[]> 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<EaHeader>(cachedEaData, (int)(eaCluster * bytesPerCluster),
Marshal.SizeOf<EaHeader>());
if(header.magic != 0x4145) return null;
uint eaLen = BitConverter.ToUInt32(cachedEaData,
(int)(eaCluster * bytesPerCluster) + Marshal.SizeOf<EaHeader>());
byte[] eaData = new byte[eaLen];
Array.Copy(cachedEaData, (int)(eaCluster * bytesPerCluster) + Marshal.SizeOf<EaHeader>(), eaData, 0, eaLen);
Dictionary<string, byte[]> eas = new Dictionary<string, byte[]>();
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;