Files
Aaru/Aaru.Filesystems/FAT/Super.cs
Natalia Portillo 4ef3252b24 [Aaru.Filesystems] Introduced constants for module names
Introduces constant fields for respective debug module names, replacing the hardcoded ones.
This is done to standardize the naming convention, reduce redundancy and potentially avoid any typos or name mismatches across the project.
This change makes the code more maintainable and easier to update in case module names need to be changed.
2023-10-03 18:50:19 +01:00

996 lines
38 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Super.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Microsoft FAT filesystem plugin.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2023 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Console;
using Aaru.Helpers;
using FileSystemInfo = Aaru.CommonTypes.Structs.FileSystemInfo;
using Marshal = Aaru.Helpers.Marshal;
using Partition = Aaru.CommonTypes.Partition;
namespace Aaru.Filesystems;
public sealed partial class FAT
{
/// <inheritdoc />
public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
Dictionary<string, string> options, string @namespace)
{
Metadata = new FileSystem();
options ??= GetDefaultOptions();
if(options.TryGetValue("debug", out string debugString))
bool.TryParse(debugString, out _debug);
// Default namespace
@namespace ??= "ecs";
switch(@namespace.ToLowerInvariant())
{
case "dos":
_namespace = Namespace.Dos;
break;
case "nt":
_namespace = Namespace.Nt;
break;
case "os2":
_namespace = Namespace.Os2;
break;
case "ecs":
_namespace = Namespace.Ecs;
break;
case "lfn":
_namespace = Namespace.Lfn;
break;
case "human":
_namespace = Namespace.Human;
break;
default: return ErrorNumber.InvalidArgument;
}
AaruConsole.DebugWriteLine(MODULE_NAME, Localization.Reading_BPB);
uint sectorsPerBpb = imagePlugin.Info.SectorSize < 512 ? 512 / imagePlugin.Info.SectorSize : 1;
ErrorNumber errno = imagePlugin.ReadSectors(0 + partition.Start, sectorsPerBpb, out byte[] bpbSector);
if(errno != ErrorNumber.NoError)
return errno;
BpbKind bpbKind = DetectBpbKind(bpbSector, imagePlugin, partition, out BiosParameterBlockEbpb fakeBpb,
out HumanParameterBlock humanBpb, out AtariParameterBlock atariBpb,
out byte minBootNearJump, out bool andosOemCorrect, out bool bootable);
_fat12 = false;
_fat16 = false;
_fat32 = false;
_useFirstFat = true;
Metadata.Bootable = bootable;
_statfs = new FileSystemInfo
{
FilenameLength = 11,
Files = 0, // Requires traversing all directories
FreeFiles = 0,
PluginId = Id,
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
uint sectorsForRootDirectory = 0;
uint rootDirectoryCluster = 0;
_encoding = encoding ?? (bpbKind == BpbKind.Human ? Encoding.GetEncoding("shift_jis")
: Encoding.GetEncoding("IBM437"));
switch(bpbKind)
{
case BpbKind.DecRainbow:
case BpbKind.Hardcoded:
case BpbKind.Msx:
case BpbKind.Apricot:
_fat12 = true;
break;
case BpbKind.ShortFat32:
case BpbKind.LongFat32:
{
_fat32 = true;
Fat32ParameterBlock fat32Bpb = Marshal.ByteArrayToStructureLittleEndian<Fat32ParameterBlock>(bpbSector);
Fat32ParameterBlockShort shortFat32Bpb =
Marshal.ByteArrayToStructureLittleEndian<Fat32ParameterBlockShort>(bpbSector);
rootDirectoryCluster = fat32Bpb.root_cluster;
// This is to support FAT partitions on hybrid ISO/USB images
if(imagePlugin.Info.MetadataMediaType == MetadataMediaType.OpticalDisc)
{
fat32Bpb.bps *= 4;
fat32Bpb.spc /= 4;
fat32Bpb.big_spfat /= 4;
fat32Bpb.hsectors /= 4;
fat32Bpb.sptrk /= 4;
}
Metadata.Type = fat32Bpb.version != 0 ? FS_TYPE_FAT_PLUS : FS_TYPE_FAT32;
if(fat32Bpb.oem_name != null &&
(fat32Bpb.oem_name[5] != 0x49 || fat32Bpb.oem_name[6] != 0x48 || fat32Bpb.oem_name[7] != 0x43))
Metadata.SystemIdentifier = StringHandlers.CToString(fat32Bpb.oem_name);
_sectorsPerCluster = fat32Bpb.spc;
Metadata.ClusterSize = (uint)(fat32Bpb.bps * fat32Bpb.spc);
_reservedSectors = fat32Bpb.rsectors;
if(fat32Bpb is { big_sectors: 0, signature: 0x28 })
Metadata.Clusters = shortFat32Bpb.huge_sectors / shortFat32Bpb.spc;
else if(fat32Bpb.sectors == 0)
Metadata.Clusters = fat32Bpb.big_sectors / fat32Bpb.spc;
else
Metadata.Clusters = (ulong)(fat32Bpb.sectors / fat32Bpb.spc);
_sectorsPerFat = fat32Bpb.big_spfat;
Metadata.VolumeSerial = $"{fat32Bpb.serial_no:X8}";
_statfs.Id = new FileSystemId
{
IsInt = true,
Serial32 = fat32Bpb.serial_no
};
if((fat32Bpb.flags & 0xF8) == 0x00)
if((fat32Bpb.flags & 0x01) == 0x01)
Metadata.Dirty = true;
if((fat32Bpb.mirror_flags & 0x80) == 0x80)
_useFirstFat = (fat32Bpb.mirror_flags & 0xF) != 1;
if(fat32Bpb.signature == 0x29)
{
Metadata.VolumeName = StringHandlers.SpacePaddedToString(fat32Bpb.volume_label, _encoding);
Metadata.VolumeName = Metadata.VolumeName?.Replace("\0", "");
}
// Check that jumps to a correct boot code position and has boot signature set.
// This will mean that the volume will boot, even if just to say "this is not bootable change disk"......
Metadata.Bootable =
(fat32Bpb.jump[0] == 0xEB && fat32Bpb.jump[1] >= minBootNearJump && fat32Bpb.jump[1] < 0x80) ||
(fat32Bpb.jump[0] == 0xE9 && fat32Bpb.jump.Length >= 3 &&
BitConverter.ToUInt16(fat32Bpb.jump, 1) >= minBootNearJump &&
BitConverter.ToUInt16(fat32Bpb.jump, 1) <= 0x1FC);
sectorsPerRealSector = fat32Bpb.bps / imagePlugin.Info.SectorSize;
_sectorsPerCluster *= sectorsPerRealSector;
// First root directory sector
_firstClusterSector =
((ulong)((fat32Bpb.big_spfat * fat32Bpb.fats_no) + fat32Bpb.rsectors) * sectorsPerRealSector) -
(2 * _sectorsPerCluster);
if(fat32Bpb.fsinfo_sector + partition.Start <= partition.End)
{
errno = imagePlugin.ReadSector(fat32Bpb.fsinfo_sector + partition.Start, out byte[] fsinfoSector);
if(errno != ErrorNumber.NoError)
return errno;
FsInfoSector fsInfo = Marshal.ByteArrayToStructureLittleEndian<FsInfoSector>(fsinfoSector);
if(fsInfo is { signature1: FSINFO_SIGNATURE1, signature2 : FSINFO_SIGNATURE2 }
and { signature3 : FSINFO_SIGNATURE3, free_clusters: < 0xFFFFFFFF })
{
Metadata.FreeClusters = fsInfo.free_clusters;
}
}
break;
}
// Some fields could overflow fake BPB, those will be handled below
case BpbKind.Atari:
{
ushort sum = 0;
for(int i = 0; i < bpbSector.Length; i += 2)
sum += BigEndianBitConverter.ToUInt16(bpbSector, i);
// TODO: Check this
if(sum == 0x1234)
Metadata.Bootable = true;
// BGM changes the bytes per sector instead of changing the sectors per cluster. Why?! WHY!?
uint ratio = fakeBpb.bps / imagePlugin.Info.SectorSize;
fakeBpb.bps = (ushort)imagePlugin.Info.SectorSize;
fakeBpb.spc = (byte)(fakeBpb.spc * ratio);
fakeBpb.rsectors = (ushort)(fakeBpb.rsectors * ratio);
fakeBpb.big_sectors = fakeBpb.sectors * ratio;
fakeBpb.sectors = 0;
fakeBpb.spfat = (ushort)(fakeBpb.spfat * ratio);
fakeBpb.sptrk = (ushort)(fakeBpb.sptrk * ratio);
break;
}
case BpbKind.Human:
// If not debug set Human68k namespace and ShiftJIS codepage as defaults
if(!_debug)
_namespace = Namespace.Human;
Metadata.Bootable = true;
break;
}
ulong firstRootSector = 0;
if(!_fat32)
{
// This is to support FAT partitions on hybrid ISO/USB images
if(imagePlugin.Info.MetadataMediaType == MetadataMediaType.OpticalDisc)
{
fakeBpb.bps *= 4;
fakeBpb.spc /= 4;
fakeBpb.spfat /= 4;
fakeBpb.hsectors /= 4;
fakeBpb.sptrk /= 4;
fakeBpb.rsectors /= 4;
if(fakeBpb.spc == 0)
fakeBpb.spc = 1;
}
ulong clusters;
if(bpbKind != BpbKind.Human)
{
int reservedSectors = fakeBpb.rsectors + (fakeBpb.fats_no * fakeBpb.spfat) +
(fakeBpb.root_ent * 32 / fakeBpb.bps);
if(fakeBpb.sectors == 0)
clusters = (ulong)(fakeBpb.spc == 0 ? fakeBpb.big_sectors - reservedSectors
: (fakeBpb.big_sectors - reservedSectors) / fakeBpb.spc);
else
clusters = (ulong)(fakeBpb.spc == 0 ? fakeBpb.sectors - reservedSectors
: (fakeBpb.sectors - reservedSectors) / fakeBpb.spc);
}
else
clusters = humanBpb.clusters == 0 ? humanBpb.big_clusters : humanBpb.clusters;
// This will walk all the FAT entries and check if they're valid FAT12 or FAT16 entries.
// If the whole table is valid in both senses, it considers the type entry in the BPB.
// BeOS is known to set the type as FAT16 but treat it as FAT12.
if(!_fat12 &&
!_fat16)
{
if(clusters < 4089)
{
ushort[] fat12 = new ushort[clusters + 1];
_reservedSectors = fakeBpb.rsectors;
sectorsPerRealSector = fakeBpb.bps / imagePlugin.Info.SectorSize;
_fatFirstSector = partition.Start + (_reservedSectors * sectorsPerRealSector);
errno = imagePlugin.ReadSectors(_fatFirstSector, fakeBpb.spfat, out byte[] fatBytes);
if(errno != ErrorNumber.NoError)
return errno;
int pos = 0;
for(int i = 0; i + 3 < fatBytes.Length && pos < fat12.Length; i += 3)
{
fat12[pos++] = (ushort)(((fatBytes[i + 1] & 0xF) << 8) + fatBytes[i + 0]);
if(pos >= fat12.Length)
break;
fat12[pos++] = (ushort)(((fatBytes[i + 1] & 0xF0) >> 4) + (fatBytes[i + 2] << 4));
}
bool fat12Valid = fat12[0] >= FAT12_RESERVED && fat12[1] >= FAT12_RESERVED;
if(fat12.Any(entry => entry < FAT12_RESERVED && entry > clusters))
fat12Valid = false;
ushort[] fat16 = MemoryMarshal.Cast<byte, ushort>(fatBytes).ToArray();
bool fat16Valid = fat16[0] >= FAT16_RESERVED && fat16[1] >= 0x3FF0;
if(fat16.Any(entry => entry < FAT16_RESERVED && entry > clusters))
fat16Valid = false;
_fat12 = fat12Valid;
_fat16 = fat16Valid;
// Check BPB type
if(_fat12 == _fat16 &&
fakeBpb.fs_type != null)
{
_fat12 = Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT12 ";
_fat16 = Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT16 ";
}
// Still undecided (fs_type is null or is not FAT1[2|6])
if(_fat12 == _fat16)
{
_fat12 = true;
_fat16 = false;
}
}
else
_fat16 = true;
}
if(_fat12)
Metadata.Type = FS_TYPE_FAT12;
else if(_fat16)
Metadata.Type = FS_TYPE_FAT16;
if(bpbKind == BpbKind.Atari)
{
if(atariBpb.serial_no[0] != 0x49 ||
atariBpb.serial_no[1] != 0x48 ||
atariBpb.serial_no[2] != 0x43)
{
Metadata.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])
};
}
Metadata.SystemIdentifier = StringHandlers.CToString(atariBpb.oem_name);
if(string.IsNullOrEmpty(Metadata.SystemIdentifier))
Metadata.SystemIdentifier = null;
}
else if(fakeBpb.oem_name != null)
{
if(fakeBpb.oem_name[5] != 0x49 ||
fakeBpb.oem_name[6] != 0x48 ||
fakeBpb.oem_name[7] != 0x43)
Metadata.SystemIdentifier = fakeBpb.oem_name[0] switch
{
// Later versions of Windows create a DOS 3 BPB without OEM name on 8 sectors/track floppies
// OEM ID should be ASCII, otherwise ignore it
>= 0x20 and <= 0x7F when fakeBpb.oem_name[1] >= 0x20 && fakeBpb.oem_name[1] <= 0x7F &&
fakeBpb.oem_name[2] >= 0x20 && fakeBpb.oem_name[2] <= 0x7F &&
fakeBpb.oem_name[3] >= 0x20 && fakeBpb.oem_name[3] <= 0x7F &&
fakeBpb.oem_name[4] >= 0x20 && fakeBpb.oem_name[4] <= 0x7F &&
fakeBpb.oem_name[5] >= 0x20 && fakeBpb.oem_name[5] <= 0x7F &&
fakeBpb.oem_name[6] >= 0x20 && fakeBpb.oem_name[6] <= 0x7F &&
fakeBpb.oem_name[7] >= 0x20 &&
fakeBpb.oem_name[7] <= 0x7F =>
StringHandlers.CToString(fakeBpb.oem_name),
< 0x20 when fakeBpb.oem_name[1] >= 0x20 && fakeBpb.oem_name[1] <= 0x7F &&
fakeBpb.oem_name[2] >= 0x20 && fakeBpb.oem_name[2] <= 0x7F &&
fakeBpb.oem_name[3] >= 0x20 && fakeBpb.oem_name[3] <= 0x7F &&
fakeBpb.oem_name[4] >= 0x20 && fakeBpb.oem_name[4] <= 0x7F &&
fakeBpb.oem_name[5] >= 0x20 && fakeBpb.oem_name[5] <= 0x7F &&
fakeBpb.oem_name[6] >= 0x20 && fakeBpb.oem_name[6] <= 0x7F &&
fakeBpb.oem_name[7] >= 0x20 &&
fakeBpb.oem_name[7] <= 0x7F => StringHandlers.CToString(fakeBpb.oem_name, _encoding,
start: 1),
_ => Metadata.SystemIdentifier
};
if(fakeBpb.signature is 0x28 or 0x29)
{
Metadata.VolumeSerial = $"{fakeBpb.serial_no:X8}";
_statfs.Id = new FileSystemId
{
IsInt = true,
Serial32 = fakeBpb.serial_no
};
}
}
Metadata.Clusters = clusters;
_sectorsPerCluster = fakeBpb.spc;
Metadata.ClusterSize = (uint)(fakeBpb.bps * fakeBpb.spc);
_reservedSectors = fakeBpb.rsectors;
_sectorsPerFat = fakeBpb.spfat;
if(fakeBpb.signature is 0x28 or 0x29 || andosOemCorrect)
{
if((fakeBpb.flags & 0xF8) == 0x00)
if((fakeBpb.flags & 0x01) == 0x01)
Metadata.Dirty = true;
if(fakeBpb.signature == 0x29 || andosOemCorrect)
{
Metadata.VolumeName = StringHandlers.SpacePaddedToString(fakeBpb.volume_label, _encoding);
Metadata.VolumeName = Metadata.VolumeName?.Replace("\0", "");
}
}
// Workaround that PCExchange jumps into "FAT16 "...
if(Metadata.SystemIdentifier == "PCX 2.0 ")
fakeBpb.jump[1] += 8;
// Check that jumps to a correct boot code position and has boot signature set.
// This will mean that the volume will boot, even if just to say "this is not bootable change disk"......
if(Metadata.Bootable == false &&
fakeBpb.jump != null)
Metadata.Bootable |=
(fakeBpb.jump[0] == 0xEB && fakeBpb.jump[1] >= minBootNearJump && fakeBpb.jump[1] < 0x80) ||
(fakeBpb.jump[0] == 0xE9 && fakeBpb.jump.Length >= 3 &&
BitConverter.ToUInt16(fakeBpb.jump, 1) >= minBootNearJump &&
BitConverter.ToUInt16(fakeBpb.jump, 1) <= 0x1FC);
// First root directory sector
firstRootSector = ((ulong)((fakeBpb.spfat * fakeBpb.fats_no) + fakeBpb.rsectors) * sectorsPerRealSector) +
partition.Start;
sectorsForRootDirectory = (uint)(fakeBpb.root_ent * 32 / imagePlugin.Info.SectorSize);
sectorsPerRealSector = fakeBpb.bps / imagePlugin.Info.SectorSize;
_sectorsPerCluster *= sectorsPerRealSector;
}
_firstClusterSector += partition.Start;
_image = imagePlugin;
if(_fat32)
_fatEntriesPerSector = imagePlugin.Info.SectorSize / 4;
else if(_fat16)
_fatEntriesPerSector = imagePlugin.Info.SectorSize / 2;
else
_fatEntriesPerSector = imagePlugin.Info.SectorSize * 2 / 3;
_fatFirstSector = partition.Start + (_reservedSectors * sectorsPerRealSector);
_rootDirectoryCache = new Dictionary<string, CompleteDirectoryEntry>();
byte[] rootDirectory;
if(!_fat32)
{
_firstClusterSector = firstRootSector + sectorsForRootDirectory - (_sectorsPerCluster * 2);
errno = imagePlugin.ReadSectors(firstRootSector, sectorsForRootDirectory, out rootDirectory);
if(errno != ErrorNumber.NoError)
return errno;
if(bpbKind == BpbKind.DecRainbow)
{
var rootMs = new MemoryStream();
foreach(ulong rootSector in new ulong[]
{
0x17, 0x19, 0x1B, 0x1D, 0x1E, 0x20
})
{
errno = imagePlugin.ReadSector(rootSector, out byte[] tmp);
if(errno != ErrorNumber.NoError)
return errno;
rootMs.Write(tmp, 0, tmp.Length);
}
rootDirectory = rootMs.ToArray();
}
}
else
{
if(rootDirectoryCluster == 0)
return ErrorNumber.InvalidArgument;
var rootMs = new MemoryStream();
uint[] rootDirectoryClusters = GetClusters(rootDirectoryCluster);
foreach(uint cluster in rootDirectoryClusters)
{
errno = imagePlugin.ReadSectors(_firstClusterSector + (cluster * _sectorsPerCluster),
_sectorsPerCluster, out byte[] buffer);
if(errno != ErrorNumber.NoError)
return errno;
rootMs.Write(buffer, 0, buffer.Length);
}
rootDirectory = rootMs.ToArray();
// OS/2 FAT32.IFS uses LFN instead of .LONGNAME
if(_namespace == Namespace.Os2)
_namespace = Namespace.Lfn;
}
if(rootDirectory is null)
return ErrorNumber.InvalidArgument;
byte[] lastLfnName = null;
byte lastLfnChecksum = 0;
for(int i = 0; i < rootDirectory.Length; i += Marshal.SizeOf<DirectoryEntry>())
{
DirectoryEntry entry =
Marshal.ByteArrayToStructureLittleEndian<DirectoryEntry>(rootDirectory, i,
Marshal.SizeOf<DirectoryEntry>());
if(entry.filename[0] == DIRENT_FINISHED)
break;
if(entry.attributes.HasFlag(FatAttributes.LFN))
{
if(_namespace != Namespace.Lfn &&
_namespace != Namespace.Ecs)
continue;
LfnEntry lfnEntry =
Marshal.ByteArrayToStructureLittleEndian<LfnEntry>(rootDirectory, i, Marshal.SizeOf<LfnEntry>());
int lfnSequence = lfnEntry.sequence & LFN_MASK;
if((lfnEntry.sequence & LFN_ERASED) > 0)
continue;
if((lfnEntry.sequence & LFN_LAST) > 0)
{
lastLfnName = new byte[lfnSequence * 26];
lastLfnChecksum = lfnEntry.checksum;
}
if(lastLfnName is null)
continue;
if(lfnEntry.checksum != lastLfnChecksum)
continue;
lfnSequence--;
Array.Copy(lfnEntry.name1, 0, lastLfnName, lfnSequence * 26, 10);
Array.Copy(lfnEntry.name2, 0, lastLfnName, (lfnSequence * 26) + 10, 12);
Array.Copy(lfnEntry.name3, 0, lastLfnName, (lfnSequence * 26) + 22, 4);
continue;
}
// Not a correct entry
if(entry.filename[0] < DIRENT_MIN &&
entry.filename[0] != DIRENT_E5)
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;
string filename;
if(entry.attributes.HasFlag(FatAttributes.VolumeLabel))
{
byte[] fullname = new byte[11];
Array.Copy(entry.filename, 0, fullname, 0, 8);
Array.Copy(entry.extension, 0, fullname, 8, 3);
string volname = _encoding.GetString(fullname).Trim();
if(!string.IsNullOrEmpty(volname))
Metadata.VolumeName = entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) && _namespace == Namespace.Nt
? volname.ToLower() : volname;
Metadata.VolumeName = Metadata.VolumeName?.Replace("\0", "");
if(entry is { ctime: > 0, cdate: > 0 })
{
Metadata.CreationDate = DateHandlers.DosToDateTime(entry.cdate, entry.ctime);
if(entry.ctime_ms > 0)
Metadata.CreationDate = Metadata.CreationDate?.AddMilliseconds(entry.ctime_ms * 10);
}
if(entry is { mtime: > 0, mdate: > 0 })
{
Metadata.ModificationDate = DateHandlers.DosToDateTime(entry.mdate, entry.mtime);
}
continue;
}
var completeEntry = new CompleteDirectoryEntry
{
Dirent = entry
};
if(_namespace is Namespace.Lfn or Namespace.Ecs &&
lastLfnName != null)
{
byte calculatedLfnChecksum = LfnChecksum(entry.filename, entry.extension);
if(calculatedLfnChecksum == lastLfnChecksum)
{
filename = StringHandlers.CToString(lastLfnName, Encoding.Unicode, true);
completeEntry.Lfn = filename;
lastLfnName = null;
lastLfnChecksum = 0;
}
}
if(entry.filename[0] == DIRENT_E5)
entry.filename[0] = DIRENT_DELETED;
string name = _encoding.GetString(entry.filename).TrimEnd();
string extension = _encoding.GetString(entry.extension).TrimEnd();
if(_namespace == Namespace.Nt)
{
if(entry.caseinfo.HasFlag(CaseInfo.LowerCaseExtension))
extension = extension.ToLower(CultureInfo.CurrentCulture);
if(entry.caseinfo.HasFlag(CaseInfo.LowerCaseBasename))
name = name.ToLower(CultureInfo.CurrentCulture);
}
if(extension != "")
filename = name + "." + extension;
else
filename = name;
if(name == "" &&
extension == "")
{
AaruConsole.DebugWriteLine(MODULE_NAME, Localization.Found_empty_filename_in_root_directory);
if(!_debug ||
entry is { size: > 0, start_cluster: 0 })
continue; // Skip invalid name
// If debug, add it
name = ":{EMPTYNAME}:";
// Try to create a unique filename with an extension from 000 to 999
for(int uniq = 0; uniq < 1000; uniq++)
{
extension = $"{uniq:D03}";
if(!_rootDirectoryCache.ContainsKey($"{name}.{extension}"))
break;
}
// If we couldn't find it, just skip over
if(_rootDirectoryCache.ContainsKey($"{name}.{extension}"))
continue;
}
// Atari ST allows slash AND colon so cannot simply substitute one for the other like in Mac filesystems
filename = filename.Replace('/', '\u2215');
completeEntry.Shortname = filename;
if(_namespace == Namespace.Human)
{
HumanDirectoryEntry humanEntry =
Marshal.ByteArrayToStructureLittleEndian<HumanDirectoryEntry>(rootDirectory, i,
Marshal.SizeOf<HumanDirectoryEntry>());
completeEntry.HumanDirent = humanEntry;
name = StringHandlers.CToString(humanEntry.name1, _encoding).TrimEnd();
extension = StringHandlers.CToString(humanEntry.extension, _encoding).TrimEnd();
string name2 = StringHandlers.CToString(humanEntry.name2, _encoding).TrimEnd();
if(extension != "")
filename = name + name2 + "." + extension;
else
filename = name + name2;
completeEntry.HumanName = filename;
}
if(!_fat32 &&
filename == "EA DATA. SF")
{
_eaDirEntry = entry;
lastLfnName = null;
lastLfnChecksum = 0;
if(_debug)
_rootDirectoryCache[completeEntry.ToString()] = completeEntry;
continue;
}
_rootDirectoryCache[completeEntry.ToString()] = completeEntry;
lastLfnName = null;
lastLfnChecksum = 0;
}
Metadata.VolumeName = Metadata.VolumeName?.Trim();
_statfs.Blocks = Metadata.Clusters;
switch(bpbKind)
{
case BpbKind.ShortFat32:
case BpbKind.LongFat32:
_statfs.Type = Metadata.Type == FS_TYPE_FAT_PLUS ? FS_TYPE_FAT_PLUS : FS_TYPE_FAT32;
break;
default:
_statfs.Type = _fat16 ? FS_TYPE_FAT16 : FS_TYPE_FAT12;
break;
}
_bytesPerCluster = _sectorsPerCluster * imagePlugin.Info.SectorSize;
// The first 2 FAT entries do not count as allocation clusters in FAT12 and FAT16
ushort[] firstFatEntries = new ushort[_statfs.Blocks + 2];
ushort[] secondFatEntries = new ushort[_statfs.Blocks + 2];
bool firstFatValid = true;
bool secondFatValid = true;
if(_fat12)
{
AaruConsole.DebugWriteLine(MODULE_NAME, Localization.Reading_FAT12);
errno = imagePlugin.ReadSectors(_fatFirstSector, _sectorsPerFat, out byte[] fatBytes);
if(errno != ErrorNumber.NoError)
return errno;
int pos = 0;
for(int i = 0; i + 3 < fatBytes.Length && pos < firstFatEntries.Length; i += 3)
{
firstFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF) << 8) + fatBytes[i + 0]);
if(pos >= firstFatEntries.Length)
break;
firstFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF0) >> 4) + (fatBytes[i + 2] << 4));
}
errno = imagePlugin.ReadSectors(_fatFirstSector + _sectorsPerFat, _sectorsPerFat, out fatBytes);
if(errno != ErrorNumber.NoError)
return errno;
_fatEntries = new ushort[_statfs.Blocks + 2];
pos = 0;
for(int i = 0; i + 3 < fatBytes.Length && pos < secondFatEntries.Length; i += 3)
{
secondFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF) << 8) + fatBytes[i + 0]);
if(pos >= secondFatEntries.Length)
break;
secondFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF0) >> 4) + (fatBytes[i + 2] << 4));
}
if(firstFatEntries.Any(entry => entry < FAT12_RESERVED && entry > _statfs.Blocks + 2))
firstFatValid = false;
if(secondFatEntries.Any(entry => entry < FAT12_RESERVED && entry > _statfs.Blocks + 2))
secondFatValid = false;
if(firstFatValid == secondFatValid)
_fatEntries = _useFirstFat ? firstFatEntries : secondFatEntries;
else if(firstFatValid)
_fatEntries = firstFatEntries;
else
_fatEntries = secondFatEntries;
}
else if(_fat16)
{
AaruConsole.DebugWriteLine(MODULE_NAME, Localization.Reading_FAT16);
errno = imagePlugin.ReadSectors(_fatFirstSector, _sectorsPerFat, out byte[] fatBytes);
if(errno != ErrorNumber.NoError)
return errno;
AaruConsole.DebugWriteLine(MODULE_NAME, Localization.Casting_FAT);
firstFatEntries = MemoryMarshal.Cast<byte, ushort>(fatBytes).ToArray();
errno = imagePlugin.ReadSectors(_fatFirstSector + _sectorsPerFat, _sectorsPerFat, out fatBytes);
if(errno != ErrorNumber.NoError)
return errno;
AaruConsole.DebugWriteLine(MODULE_NAME, Localization.Casting_FAT);
secondFatEntries = MemoryMarshal.Cast<byte, ushort>(fatBytes).ToArray();
if(firstFatEntries.Any(entry => entry < FAT16_RESERVED && entry > _statfs.Blocks + 2))
firstFatValid = false;
if(secondFatEntries.Any(entry => entry < FAT16_RESERVED && entry > _statfs.Blocks + 2))
secondFatValid = false;
if(firstFatValid == secondFatValid)
_fatEntries = _useFirstFat ? firstFatEntries : secondFatEntries;
else if(firstFatValid)
_fatEntries = firstFatEntries;
else
_fatEntries = secondFatEntries;
}
// TODO: Check how this affects international filenames
_cultureInfo = new CultureInfo("en-US", false);
_directoryCache = new Dictionary<string, Dictionary<string, CompleteDirectoryEntry>>();
// Check it is really an OS/2 EA file
if(_eaDirEntry.start_cluster != 0)
{
CacheEaData();
ushort eamagic = BitConverter.ToUInt16(_cachedEaData, 0);
if(eamagic != EADATA_MAGIC)
{
_eaDirEntry = new DirectoryEntry();
_cachedEaData = null;
}
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 &&
_namespace is Namespace.Os2 or Namespace.Ecs &&
!_fat32)
{
List<KeyValuePair<string, CompleteDirectoryEntry>> rootFilesWithEas =
_rootDirectoryCache.Where(t => t.Value.Dirent.ea_handle != 0).ToList();
foreach(KeyValuePair<string, CompleteDirectoryEntry> fileWithEa in rootFilesWithEas)
{
Dictionary<string, byte[]> eas = GetEas(fileWithEa.Value.Dirent.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');
fileWithEa.Value.Longname = longname;
_rootDirectoryCache.Remove(fileWithEa.Key);
_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[..^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);
}
}
_mounted = true;
if(string.IsNullOrWhiteSpace(Metadata.VolumeName))
Metadata.VolumeName = null;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber Unmount()
{
if(!_mounted)
return ErrorNumber.AccessDenied;
_mounted = false;
_fatEntries = null;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber StatFs(out FileSystemInfo stat)
{
stat = null;
if(!_mounted)
return ErrorNumber.AccessDenied;
stat = _statfs.ShallowCopy();
return ErrorNumber.NoError;
}
}