Implement support for EAs in FAT32.

This commit is contained in:
2019-04-28 11:57:54 +01:00
parent b2c008eb02
commit fa2ec74015
9 changed files with 238 additions and 92 deletions

View File

@@ -19,6 +19,7 @@
<ApplicationVersion>$(Version)</ApplicationVersion>
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
<NoWarn>CS0649</NoWarn>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup>
<NrtRevisionFormat>$(Version)-{chash:8} built by {mname} in $(Configuration){!:, modified}</NrtRevisionFormat>

View File

@@ -59,14 +59,6 @@ namespace DiscImageChef.Filesystems.FAT
/// Entry points to self or parent directory
/// </summary>
const byte DIRENT_SUBDIR = 0x2E;
/// <summary>
/// FASTFAT.SYS indicator that extension is lowercase
/// </summary>
const byte FASTFAT_LOWERCASE_EXTENSION = 0x10;
/// <summary>
/// FASTFAT.SYS indicator that basename is lowercase
/// </summary>
const byte FASTFAT_LOWERCASE_BASENAME = 0x08;
const uint FAT32_MASK = 0x0FFFFFFF;
const uint FAT32_END_MASK = 0xFFFFFF8;
const uint FAT32_FORMATTED = 0xFFFFFF6;
@@ -92,6 +84,7 @@ namespace DiscImageChef.Filesystems.FAT
const ushort EAT_MVMT = 0xFFDF;
const ushort EAT_MVST = 0xFFDE;
const ushort EAT_ASN1 = 0xFFDD;
const string FAT32_EA_TAIL = " EA. SF";
readonly (string hash, string name)[] knownBootHashes =
{
@@ -236,5 +229,35 @@ namespace DiscImageChef.Filesystems.FAT
Normal = 0,
Critical = 1
}
[Flags]
enum CaseInfo : byte
{
/// <summary>
/// FASTFAT.SYS indicator that basename is lowercase
/// </summary>
LowerCaseBasename = 0x08,
/// <summary>
/// FASTFAT.SYS indicator that extension is lowercase
/// </summary>
LowerCaseExtension = 0x10,
AllLowerCase = 0x18,
/// <summary>
/// FAT32.IFS &lt; 0.97 indicator for normal EAs present
/// </summary>
NormalEaOld = 0xEA,
/// <summary>
/// FAT32.IFS &lt; 0.97 indicator for critical EAs present
/// </summary>
CriticalEaOld = 0xEC,
/// <summary>
/// FAT32.IFS &gt;= 0.97 indicator for normal EAs present
/// </summary>
NormalEa = 0x40,
/// <summary>
/// FAT32.IFS &gt;= 0.97 indicator for critical EAs present
/// </summary>
CriticalEa = 0x80
}
}
}

View File

@@ -73,7 +73,7 @@ namespace DiscImageChef.Filesystems.FAT
? path.Substring(1).ToLower(cultureInfo)
: path.ToLower(cultureInfo);
if(directoryCache.TryGetValue(cutPath, out Dictionary<string, DirectoryEntry> currentDirectory))
if(directoryCache.TryGetValue(cutPath, out Dictionary<string, CompleteDirectoryEntry> currentDirectory))
{
contents = currentDirectory.Keys.ToList();
return Errno.NoError;
@@ -81,12 +81,12 @@ namespace DiscImageChef.Filesystems.FAT
string[] pieces = cutPath.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
KeyValuePair<string, DirectoryEntry> entry =
KeyValuePair<string, CompleteDirectoryEntry> 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;
if(!entry.Value.Dirent.attributes.HasFlag(FatAttributes.Subdirectory)) return Errno.NotDirectory;
string currentPath = pieces[0];
@@ -98,12 +98,12 @@ namespace DiscImageChef.Filesystems.FAT
if(string.IsNullOrEmpty(entry.Key)) return Errno.NoSuchFile;
if(!entry.Value.attributes.HasFlag(FatAttributes.Subdirectory)) return Errno.NotDirectory;
if(!entry.Value.Dirent.attributes.HasFlag(FatAttributes.Subdirectory)) return Errno.NotDirectory;
currentPath = p == 0 ? pieces[0] : $"{currentPath}/{pieces[p]}";
uint currentCluster = entry.Value.start_cluster;
uint currentCluster = entry.Value.Dirent.start_cluster;
if(fat32) currentCluster += (uint)(entry.Value.ea_handle << 16);
if(fat32) currentCluster += (uint)(entry.Value.Dirent.ea_handle << 16);
if(directoryCache.TryGetValue(currentPath, out currentDirectory)) continue;
@@ -121,7 +121,7 @@ namespace DiscImageChef.Filesystems.FAT
Array.Copy(buffer, 0, directoryBuffer, i * bytesPerCluster, bytesPerCluster);
}
currentDirectory = new Dictionary<string, DirectoryEntry>();
currentDirectory = new Dictionary<string, CompleteDirectoryEntry>();
byte[] lastLfnName = null;
byte lastLfnChecksum = 0;
List<string> LFNs = new List<string>();
@@ -136,7 +136,7 @@ namespace DiscImageChef.Filesystems.FAT
if(dirent.attributes.HasFlag(FatAttributes.LFN))
{
if(@namespace != Namespace.Lfn) continue;
if(@namespace != Namespace.Lfn && @namespace != Namespace.Ecs) continue;
LfnEntry lfnEntry =
Marshal.ByteArrayToStructureLittleEndian<LfnEntry>(directoryBuffer, pos,
@@ -180,7 +180,9 @@ namespace DiscImageChef.Filesystems.FAT
if(dirent.attributes.HasFlag(FatAttributes.VolumeLabel)) continue;
if(@namespace == Namespace.Lfn && lastLfnName != null)
CompleteDirectoryEntry completeEntry = new CompleteDirectoryEntry {Dirent = dirent};
if((@namespace == Namespace.Lfn || @namespace == Namespace.Ecs) && lastLfnName != null)
{
byte calculatedLfnChecksum = LfnChecksum(dirent.filename, dirent.extension);
@@ -188,11 +190,9 @@ namespace DiscImageChef.Filesystems.FAT
{
filename = StringHandlers.CToString(lastLfnName, Encoding.Unicode, true);
LFNs.Add(filename);
currentDirectory[filename] = dirent;
completeEntry.Lfn = filename;
lastLfnName = null;
lastLfnChecksum = 0;
continue;
}
}
@@ -203,10 +203,10 @@ namespace DiscImageChef.Filesystems.FAT
if(@namespace == Namespace.Nt)
{
if((dirent.caseinfo & FASTFAT_LOWERCASE_EXTENSION) > 0)
if(dirent.caseinfo.HasFlag(CaseInfo.LowerCaseExtension))
extension = extension.ToLower(CultureInfo.CurrentCulture);
if((dirent.caseinfo & FASTFAT_LOWERCASE_BASENAME) > 0)
if(dirent.caseinfo.HasFlag(CaseInfo.LowerCaseBasename))
name = name.ToLower(CultureInfo.CurrentCulture);
}
@@ -216,21 +216,19 @@ namespace DiscImageChef.Filesystems.FAT
// 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;
completeEntry.Shortname = filename;
currentDirectory[completeEntry.ToString()] = completeEntry;
}
// 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();
List<KeyValuePair<string, CompleteDirectoryEntry>> filesWithEas =
currentDirectory.Where(t => t.Value.Dirent.ea_handle != 0).ToList();
foreach(KeyValuePair<string, DirectoryEntry> fileWithEa in filesWithEas)
foreach(KeyValuePair<string, CompleteDirectoryEntry> 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);
Dictionary<string, byte[]> eas = GetEas(fileWithEa.Value.Dirent.ea_handle);
if(eas is null) continue;
@@ -253,8 +251,36 @@ namespace DiscImageChef.Filesystems.FAT
// Forward slash is allowed in .LONGNAME, so change it to visually similar division slash
longname = longname.Replace('/', '\u2215');
fileWithEa.Value.Longname = longname;
currentDirectory.Remove(fileWithEa.Key);
currentDirectory[longname] = fileWithEa.Value;
currentDirectory[fileWithEa.Value.ToString()] = fileWithEa.Value;
}
}
// Check FAT32.IFS EAs
if(fat32 || debug)
{
List<KeyValuePair<string, CompleteDirectoryEntry>> fat32EaSidecars =
currentDirectory.Where(t => t.Key.EndsWith(FAT32_EA_TAIL, true, cultureInfo)).ToList();
foreach(KeyValuePair<string, CompleteDirectoryEntry> sidecar in fat32EaSidecars)
{
// No real file this sidecar accompanies
if(!currentDirectory
.TryGetValue(sidecar.Key.Substring(0, sidecar.Key.Length - FAT32_EA_TAIL.Length),
out CompleteDirectoryEntry fileWithEa)) continue;
// If not in debug mode we will consider the lack of EA bitflags to mean the EAs are corrupted or not real
if(!debug)
if(!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.NormalEaOld) &&
!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.CriticalEa) &&
!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.NormalEa) &&
!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.CriticalEa))
continue;
fileWithEa.Fat32Ea = sidecar.Value.Dirent;
if(!debug) currentDirectory.Remove(sidecar.Key);
}
}

View File

@@ -48,7 +48,7 @@ namespace DiscImageChef.Filesystems.FAT
byte[] cachedEaData;
CultureInfo cultureInfo;
bool debug;
Dictionary<string, Dictionary<string, DirectoryEntry>> directoryCache;
Dictionary<string, Dictionary<string, CompleteDirectoryEntry>> directoryCache;
DirectoryEntry eaDirEntry;
bool fat12;
bool fat16;
@@ -59,7 +59,7 @@ namespace DiscImageChef.Filesystems.FAT
bool mounted;
Namespace @namespace;
uint reservedSectors;
Dictionary<string, DirectoryEntry> rootDirectoryCache;
Dictionary<string, CompleteDirectoryEntry> rootDirectoryCache;
uint sectorsPerCluster;
uint sectorsPerFat;
FileSystemInfo statfs;

View File

@@ -122,9 +122,11 @@ namespace DiscImageChef.Filesystems.FAT
stat = null;
if(!mounted) return Errno.AccessDenied;
Errno err = GetFileEntry(path, out DirectoryEntry entry);
Errno err = GetFileEntry(path, out CompleteDirectoryEntry completeEntry);
if(err != Errno.NoError) return err;
DirectoryEntry entry = completeEntry.Dirent;
stat = new FileEntryInfo
{
Attributes = new FileAttributes(),
@@ -207,9 +209,9 @@ namespace DiscImageChef.Filesystems.FAT
return clusters.ToArray();
}
Errno GetFileEntry(string path, out DirectoryEntry entry)
Errno GetFileEntry(string path, out CompleteDirectoryEntry entry)
{
entry = new DirectoryEntry();
entry = null;
string cutPath =
path.StartsWith("/") ? path.Substring(1).ToLower(cultureInfo) : path.ToLower(cultureInfo);
@@ -223,12 +225,12 @@ namespace DiscImageChef.Filesystems.FAT
if(err != Errno.NoError) return err;
Dictionary<string, DirectoryEntry> parent;
Dictionary<string, CompleteDirectoryEntry> parent;
if(pieces.Length == 1) parent = rootDirectoryCache;
else if(!directoryCache.TryGetValue(parentPath, out parent)) return Errno.InvalidArgument;
KeyValuePair<string, DirectoryEntry> dirent =
KeyValuePair<string, CompleteDirectoryEntry> dirent =
parent.FirstOrDefault(t => t.Key.ToLower(cultureInfo) == pieces[pieces.Length - 1]);
if(string.IsNullOrEmpty(dirent.Key)) return Errno.NoSuchFile;

View File

@@ -791,7 +791,8 @@ namespace DiscImageChef.Filesystems.FAT
Array.Copy(entry.extension, 0, fullname, 8, 3);
string volname = Encoding.GetString(fullname).Trim();
if(!string.IsNullOrEmpty(volname))
XmlFsType.VolumeName = (entry.caseinfo & 0x18) > 0 ? volname.ToLower() : volname;
XmlFsType.VolumeName =
entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) ? volname.ToLower() : volname;
if(entry.ctime > 0 && entry.cdate > 0)
{

View File

@@ -855,7 +855,7 @@ namespace DiscImageChef.Filesystems.FAT
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public readonly byte[] extension;
public readonly FatAttributes attributes;
public readonly byte caseinfo;
public readonly CaseInfo caseinfo;
public readonly byte ctime_ms;
public readonly ushort ctime;
public readonly ushort cdate;
@@ -894,5 +894,22 @@ namespace DiscImageChef.Filesystems.FAT
public readonly uint unknown;
public readonly ushort zero;
}
class CompleteDirectoryEntry
{
public DirectoryEntry Dirent;
public DirectoryEntry Fat32Ea;
public string Lfn;
public string Longname;
public string Shortname;
public override string ToString()
{
// This ensures LFN takes preference when eCS is in use
if(!string.IsNullOrEmpty(Lfn)) return Lfn;
return !string.IsNullOrEmpty(Longname) ? Longname : Shortname;
}
}
}
}

View File

@@ -368,7 +368,7 @@ namespace DiscImageChef.Filesystems.FAT
else fatEntriesPerSector = imagePlugin.Info.SectorSize * 2 / 3;
fatFirstSector = partition.Start + reservedSectors * sectorsPerRealSector;
rootDirectoryCache = new Dictionary<string, DirectoryEntry>();
rootDirectoryCache = new Dictionary<string, CompleteDirectoryEntry>();
byte[] rootDirectory = null;
if(!fat32)
@@ -401,13 +401,15 @@ namespace DiscImageChef.Filesystems.FAT
}
rootDirectory = rootMs.ToArray();
// OS/2 FAT32.IFS uses LFN instead of .LONGNAME
if(this.@namespace == Namespace.Os2) this.@namespace = Namespace.Os2;
}
if(rootDirectory is null) return Errno.InvalidArgument;
byte[] lastLfnName = null;
byte lastLfnChecksum = 0;
List<string> LFNs = new List<string>();
for(int i = 0; i < rootDirectory.Length; i += Marshal.SizeOf<DirectoryEntry>())
{
@@ -468,7 +470,8 @@ namespace DiscImageChef.Filesystems.FAT
Array.Copy(entry.extension, 0, fullname, 8, 3);
string volname = Encoding.GetString(fullname).Trim();
if(!string.IsNullOrEmpty(volname))
XmlFsType.VolumeName = (entry.caseinfo & 0x18) > 0 && this.@namespace == Namespace.Nt
XmlFsType.VolumeName =
entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) && this.@namespace == Namespace.Nt
? volname.ToLower()
: volname;
@@ -489,6 +492,8 @@ namespace DiscImageChef.Filesystems.FAT
continue;
}
CompleteDirectoryEntry completeEntry = new CompleteDirectoryEntry {Dirent = entry};
if((this.@namespace == Namespace.Lfn || this.@namespace == Namespace.Ecs) && lastLfnName != null)
{
byte calculatedLfnChecksum = LfnChecksum(entry.filename, entry.extension);
@@ -497,11 +502,9 @@ namespace DiscImageChef.Filesystems.FAT
{
filename = StringHandlers.CToString(lastLfnName, Encoding.Unicode, true);
LFNs.Add(filename);
rootDirectoryCache[filename] = entry;
completeEntry.Lfn = filename;
lastLfnName = null;
lastLfnChecksum = 0;
continue;
}
}
@@ -512,28 +515,30 @@ namespace DiscImageChef.Filesystems.FAT
if(this.@namespace == Namespace.Nt)
{
if((entry.caseinfo & FASTFAT_LOWERCASE_EXTENSION) > 0)
if(entry.caseinfo.HasFlag(CaseInfo.LowerCaseExtension))
extension = extension.ToLower(CultureInfo.CurrentCulture);
if((entry.caseinfo & FASTFAT_LOWERCASE_BASENAME) > 0)
if(entry.caseinfo.HasFlag(CaseInfo.LowerCaseBasename))
name = name.ToLower(CultureInfo.CurrentCulture);
}
if(extension != "") filename = name + "." + extension;
else filename = name;
completeEntry.Shortname = filename;
if(!fat32 && filename == "EA DATA. SF")
{
eaDirEntry = entry;
lastLfnName = null;
lastLfnChecksum = 0;
if(debug) rootDirectoryCache[filename] = entry;
if(debug) rootDirectoryCache[completeEntry.ToString()] = completeEntry;
continue;
}
rootDirectoryCache[filename] = entry;
rootDirectoryCache[completeEntry.ToString()] = completeEntry;
lastLfnName = null;
lastLfnChecksum = 0;
}
@@ -608,7 +613,7 @@ namespace DiscImageChef.Filesystems.FAT
// TODO: Check how this affects international filenames
cultureInfo = new CultureInfo("en-US", false);
directoryCache = new Dictionary<string, Dictionary<string, DirectoryEntry>>();
directoryCache = new Dictionary<string, Dictionary<string, CompleteDirectoryEntry>>();
// Check it is really an OS/2 EA file
if(eaDirEntry.start_cluster != 0)
@@ -623,19 +628,17 @@ namespace DiscImageChef.Filesystems.FAT
}
else eaCache = new Dictionary<string, Dictionary<string, byte[]>>();
}
else if(fat32) 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();
List<KeyValuePair<string, CompleteDirectoryEntry>> rootFilesWithEas =
rootDirectoryCache.Where(t => t.Value.Dirent.ea_handle != 0).ToList();
foreach(KeyValuePair<string, DirectoryEntry> fileWithEa in rootFilesWithEas)
foreach(KeyValuePair<string, CompleteDirectoryEntry> 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);
Dictionary<string, byte[]> eas = GetEas(fileWithEa.Value.Dirent.ea_handle);
if(eas is null) continue;
@@ -658,8 +661,41 @@ namespace DiscImageChef.Filesystems.FAT
// Forward slash is allowed in .LONGNAME, so change it to visually similar division slash
longname = longname.Replace('/', '\u2215');
fileWithEa.Value.Longname = longname;
rootDirectoryCache.Remove(fileWithEa.Key);
rootDirectoryCache[longname] = fileWithEa.Value;
rootDirectoryCache[fileWithEa.Value.ToString()] = fileWithEa.Value;
}
}
// Check FAT32.IFS EAs
if(fat32 || debug)
{
List<KeyValuePair<string, CompleteDirectoryEntry>> fat32EaSidecars = rootDirectoryCache
.Where(t =>
t.Key
.EndsWith(FAT32_EA_TAIL,
true,
cultureInfo))
.ToList();
foreach(KeyValuePair<string, CompleteDirectoryEntry> sidecar in fat32EaSidecars)
{
// No real file this sidecar accompanies
if(!rootDirectoryCache
.TryGetValue(sidecar.Key.Substring(0, sidecar.Key.Length - FAT32_EA_TAIL.Length),
out CompleteDirectoryEntry fileWithEa)) continue;
// If not in debug mode we will consider the lack of EA bitflags to mean the EAs are corrupted or not real
if(!debug)
if(!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.NormalEaOld) &&
!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.CriticalEa) &&
!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.NormalEa) &&
!fileWithEa.Dirent.caseinfo.HasFlag(CaseInfo.CriticalEa))
continue;
fileWithEa.Fat32Ea = sidecar.Value.Dirent;
if(!debug) rootDirectoryCache.Remove(sidecar.Key);
}
}

View File

@@ -56,7 +56,7 @@ namespace DiscImageChef.Filesystems.FAT
if(!mounted) return Errno.AccessDenied;
// No other xattr recognized yet
if(cachedEaData is null) return Errno.NotSupported;
if(cachedEaData is null && !fat32) return Errno.NotSupported;
if(path[0] == '/') path = path.Substring(1);
@@ -66,15 +66,24 @@ namespace DiscImageChef.Filesystems.FAT
return Errno.NoError;
}
Errno err = GetFileEntry(path, out DirectoryEntry entry);
Errno err = GetFileEntry(path, out CompleteDirectoryEntry entry);
if(err != Errno.NoError) return err;
if(err != Errno.NoError || entry is null) return err;
xattrs = new List<string>();
if(entry.ea_handle == 0) return Errno.NoError;
if(!fat32)
{
if(entry.Dirent.ea_handle == 0) return Errno.NoError;
eas = GetEas(entry.ea_handle);
eas = GetEas(entry.Dirent.ea_handle);
}
else
{
if(entry.Fat32Ea.start_cluster == 0) return Errno.NoError;
eas = GetEas(entry.Fat32Ea);
}
if(eas is null) return Errno.NoError;
@@ -113,6 +122,30 @@ namespace DiscImageChef.Filesystems.FAT
return Errno.NoError;
}
Dictionary<string, byte[]> GetEas(DirectoryEntry entryFat32Ea)
{
MemoryStream eaMs = new MemoryStream();
uint[] rootDirectoryClusters = GetClusters(entryFat32Ea.start_cluster);
foreach(uint cluster in rootDirectoryClusters)
{
byte[] buffer = image.ReadSectors(firstClusterSector + (cluster - 2) * sectorsPerCluster,
sectorsPerCluster);
eaMs.Write(buffer, 0, buffer.Length);
}
byte[] full = eaMs.ToArray();
ushort size = BitConverter.ToUInt16(full, 0);
byte[] eas = new byte[size];
Array.Copy(full, 0, eas, 0, size);
full = null;
eaMs.Close();
return GetEas(eas);
}
Dictionary<string, byte[]> GetEas(ushort eaHandle)
{
int aIndex = eaHandle >> 7;
@@ -137,6 +170,13 @@ namespace DiscImageChef.Filesystems.FAT
byte[] eaData = new byte[eaLen];
Array.Copy(cachedEaData, (int)(eaCluster * bytesPerCluster) + Marshal.SizeOf<EaHeader>(), eaData, 0, eaLen);
return GetEas(eaData);
}
Dictionary<string, byte[]> GetEas(byte[] eaData)
{
if(eaData is null || eaData.Length < 4) return null;
Dictionary<string, byte[]> eas = new Dictionary<string, byte[]>();
if(debug) eas.Add("com.microsoft.os2.fea", eaData);