public void GetInformation(IMediaImage imagePlugin, Partition partition, out string information, Encoding encoding) { Encoding = encoding ?? Encoding.GetEncoding("IBM437"); information = ""; var sb = new StringBuilder(); XmlFsType = new FileSystemType(); uint sectorsPerBpb = imagePlugin.Info.SectorSize < 512 ? 512 / imagePlugin.Info.SectorSize : 1; byte[] bpbSector = imagePlugin.ReadSectors(0 + partition.Start, sectorsPerBpb); BpbKind bpbKind = DetectBpbKind(bpbSector, imagePlugin, partition, out BiosParameterBlockEbpb fakeBpb, out HumanParameterBlock humanBpb, out AtariParameterBlock atariBpb, out byte minBootNearJump, out bool andosOemCorrect, out bool bootable); bool isFat12 = false; bool isFat16 = false; bool isFat32 = false; ulong rootDirectorySector = 0; string extraInfo = null; string bootChk = null; XmlFsType.Bootable = bootable; // This is needed because for FAT16, GEMDOS increases bytes per sector count instead of using big_sectors field. uint sectorsPerRealSector; // This is needed because some OSes don't put volume label as first entry in the root directory uint sectorsForRootDirectory = 0; switch (bpbKind) { case BpbKind.DecRainbow: case BpbKind.Hardcoded: case BpbKind.Msx: case BpbKind.Apricot: isFat12 = true; break; case BpbKind.ShortFat32: case BpbKind.LongFat32: { isFat32 = true; Fat32ParameterBlock fat32Bpb = Marshal.ByteArrayToStructureLittleEndian <Fat32ParameterBlock>(bpbSector); Fat32ParameterBlockShort shortFat32Bpb = Marshal.ByteArrayToStructureLittleEndian <Fat32ParameterBlockShort>(bpbSector); // This is to support FAT partitions on hybrid ISO/USB images if (imagePlugin.Info.XmlMediaType == XmlMediaType.OpticalDisc) { fat32Bpb.bps *= 4; fat32Bpb.spc /= 4; fat32Bpb.big_spfat /= 4; fat32Bpb.hsectors /= 4; fat32Bpb.sptrk /= 4; } if (fat32Bpb.version != 0) { sb.AppendLine("FAT+"); XmlFsType.Type = "FAT+"; } else { sb.AppendLine("Microsoft FAT32"); XmlFsType.Type = "FAT32"; } if (fat32Bpb.oem_name != null) { if (fat32Bpb.oem_name[5] == 0x49 && fat32Bpb.oem_name[6] == 0x48 && fat32Bpb.oem_name[7] == 0x43) { sb.AppendLine("Volume has been modified by Windows 9x/Me Volume Tracker."); } else { XmlFsType.SystemIdentifier = StringHandlers.CToString(fat32Bpb.oem_name); } } if (!string.IsNullOrEmpty(XmlFsType.SystemIdentifier)) { sb.AppendFormat("OEM Name: {0}", XmlFsType.SystemIdentifier.Trim()).AppendLine(); } sb.AppendFormat("{0} bytes per sector.", fat32Bpb.bps).AppendLine(); sb.AppendFormat("{0} sectors per cluster.", fat32Bpb.spc).AppendLine(); XmlFsType.ClusterSize = (uint)(fat32Bpb.bps * fat32Bpb.spc); sb.AppendFormat("{0} sectors reserved between BPB and FAT.", fat32Bpb.rsectors).AppendLine(); if (fat32Bpb.big_sectors == 0 && fat32Bpb.signature == 0x28) { sb.AppendFormat("{0} sectors on volume ({1} bytes).", shortFat32Bpb.huge_sectors, shortFat32Bpb.huge_sectors * shortFat32Bpb.bps).AppendLine(); XmlFsType.Clusters = shortFat32Bpb.huge_sectors / shortFat32Bpb.spc; } else { sb.AppendFormat("{0} sectors on volume ({1} bytes).", fat32Bpb.big_sectors, fat32Bpb.big_sectors * fat32Bpb.bps).AppendLine(); XmlFsType.Clusters = fat32Bpb.big_sectors / fat32Bpb.spc; } sb.AppendFormat("{0} clusters on volume.", XmlFsType.Clusters).AppendLine(); sb.AppendFormat("Media descriptor: 0x{0:X2}", fat32Bpb.media).AppendLine(); sb.AppendFormat("{0} sectors per FAT.", fat32Bpb.big_spfat).AppendLine(); sb.AppendFormat("{0} sectors per track.", fat32Bpb.sptrk).AppendLine(); sb.AppendFormat("{0} heads.", fat32Bpb.heads).AppendLine(); sb.AppendFormat("{0} hidden sectors before BPB.", fat32Bpb.hsectors).AppendLine(); sb.AppendFormat("Cluster of root directory: {0}", fat32Bpb.root_cluster).AppendLine(); sb.AppendFormat("Sector of FSINFO structure: {0}", fat32Bpb.fsinfo_sector).AppendLine(); sb.AppendFormat("Sector of backup FAT32 parameter block: {0}", fat32Bpb.backup_sector).AppendLine(); sb.AppendFormat("Drive number: 0x{0:X2}", fat32Bpb.drive_no).AppendLine(); sb.AppendFormat("Volume Serial Number: 0x{0:X8}", fat32Bpb.serial_no).AppendLine(); XmlFsType.VolumeSerial = $"{fat32Bpb.serial_no:X8}"; if ((fat32Bpb.flags & 0xF8) == 0x00) { if ((fat32Bpb.flags & 0x01) == 0x01) { sb.AppendLine("Volume should be checked on next mount."); XmlFsType.Dirty = true; } if ((fat32Bpb.flags & 0x02) == 0x02) { sb.AppendLine("Disk surface should be on next mount."); } } if ((fat32Bpb.mirror_flags & 0x80) == 0x80) { sb.AppendFormat("FATs are out of sync. FAT #{0} is in use.", fat32Bpb.mirror_flags & 0xF). AppendLine(); } else { sb.AppendLine("All copies of FAT are the same."); } if ((fat32Bpb.mirror_flags & 0x6F20) == 0x6F20) { sb.AppendLine("DR-DOS will boot this FAT32 using CHS."); } else if ((fat32Bpb.mirror_flags & 0x4F20) == 0x4F20) { sb.AppendLine("DR-DOS will boot this FAT32 using LBA."); } if (fat32Bpb.signature == 0x29) { XmlFsType.VolumeName = Encoding.ASCII.GetString(fat32Bpb.volume_label); sb.AppendFormat("Filesystem type: {0}", Encoding.ASCII.GetString(fat32Bpb.fs_type)). AppendLine(); bootChk = Sha1Context.Data(fat32Bpb.boot_code, out _); } else { bootChk = Sha1Context.Data(shortFat32Bpb.boot_code, out _); } // 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"...... XmlFsType.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; // First root directory sector rootDirectorySector = (ulong)(((fat32Bpb.root_cluster - 2) * fat32Bpb.spc) + (fat32Bpb.big_spfat * fat32Bpb.fats_no) + fat32Bpb.rsectors) * sectorsPerRealSector; sectorsForRootDirectory = 1; if (fat32Bpb.fsinfo_sector + partition.Start <= partition.End) { byte[] fsinfoSector = imagePlugin.ReadSector(fat32Bpb.fsinfo_sector + partition.Start); FsInfoSector fsInfo = Marshal.ByteArrayToStructureLittleEndian <FsInfoSector>(fsinfoSector); if (fsInfo.signature1 == FSINFO_SIGNATURE1 && fsInfo.signature2 == FSINFO_SIGNATURE2 && fsInfo.signature3 == FSINFO_SIGNATURE3) { if (fsInfo.free_clusters < 0xFFFFFFFF) { sb.AppendFormat("{0} free clusters", fsInfo.free_clusters).AppendLine(); XmlFsType.FreeClusters = fsInfo.free_clusters; XmlFsType.FreeClustersSpecified = true; } if (fsInfo.last_cluster > 2 && fsInfo.last_cluster < 0xFFFFFFFF) { sb.AppendFormat("Last allocated cluster {0}", fsInfo.last_cluster).AppendLine(); } } } 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) { XmlFsType.Bootable = true; var atariSb = new StringBuilder(); atariSb.AppendFormat("cmdload will be loaded with value {0:X4}h", BigEndianBitConverter.ToUInt16(bpbSector, 0x01E)).AppendLine(); atariSb.AppendFormat("Boot program will be loaded at address {0:X4}h", atariBpb.ldaaddr). AppendLine(); atariSb.AppendFormat("FAT and directory will be cached at address {0:X4}h", atariBpb.fatbuf). AppendLine(); if (atariBpb.ldmode == 0) { byte[] tmp = new byte[8]; Array.Copy(atariBpb.fname, 0, tmp, 0, 8); string fname = Encoding.ASCII.GetString(tmp).Trim(); tmp = new byte[3]; Array.Copy(atariBpb.fname, 8, tmp, 0, 3); string extension = Encoding.ASCII.GetString(tmp).Trim(); string filename; if (string.IsNullOrEmpty(extension)) { filename = fname; } else { filename = fname + "." + extension; } atariSb.AppendFormat("Boot program resides in file \"{0}\"", filename).AppendLine(); } else { atariSb. AppendFormat("Boot program starts in sector {0} and is {1} sectors long ({2} bytes)", atariBpb.ssect, atariBpb.sectcnt, atariBpb.sectcnt * atariBpb.bps). AppendLine(); } extraInfo = atariSb.ToString(); } break; } case BpbKind.Human: XmlFsType.Bootable = true; break; } if (!isFat32) { // This is to support FAT partitions on hybrid ISO/USB images if (imagePlugin.Info.XmlMediaType == XmlMediaType.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; } } // This assumes no sane implementation will violate cluster size rules // However nothing prevents this to happen // If first file on disk uses only one cluster there is absolutely no way to differentiate between FAT12 and FAT16, // so let's hope implementations use common sense? if (!isFat12 && !isFat16) { ulong clusters; if (fakeBpb.sectors == 0) { clusters = fakeBpb.spc == 0 ? fakeBpb.big_sectors : fakeBpb.big_sectors / fakeBpb.spc; } else { clusters = fakeBpb.spc == 0 ? fakeBpb.sectors : (ulong)fakeBpb.sectors / fakeBpb.spc; } if (clusters < 4089) { isFat12 = true; } else { isFat16 = true; } } if (isFat12) { switch (bpbKind) { case BpbKind.Atari: sb.AppendLine("Atari FAT12"); break; case BpbKind.Apricot: sb.AppendLine("Apricot FAT12"); break; case BpbKind.Human: sb.AppendLine("Human68k FAT12"); break; default: sb.AppendLine("Microsoft FAT12"); break; } XmlFsType.Type = "FAT12"; } else if (isFat16) { sb.AppendLine(bpbKind == BpbKind.Atari ? "Atari FAT16" : bpbKind == BpbKind.Human ? "Human68k FAT16" : "Microsoft FAT16"); XmlFsType.Type = "FAT16"; } if (bpbKind == BpbKind.Atari) { if (atariBpb.serial_no[0] == 0x49 && atariBpb.serial_no[1] == 0x48 && atariBpb.serial_no[2] == 0x43) { sb.AppendLine("Volume has been modified by Windows 9x/Me Volume Tracker."); } else { XmlFsType.VolumeSerial = $"{atariBpb.serial_no[0]:X2}{atariBpb.serial_no[1]:X2}{atariBpb.serial_no[2]:X2}"; } XmlFsType.SystemIdentifier = StringHandlers.CToString(atariBpb.oem_name); if (string.IsNullOrEmpty(XmlFsType.SystemIdentifier)) { XmlFsType.SystemIdentifier = null; } } else if (fakeBpb.oem_name != null) { if (fakeBpb.oem_name[5] == 0x49 && fakeBpb.oem_name[6] == 0x48 && fakeBpb.oem_name[7] == 0x43) { sb.AppendLine("Volume has been modified by Windows 9x/Me Volume Tracker."); } else { // 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 if (fakeBpb.oem_name[0] >= 0x20 && fakeBpb.oem_name[0] <= 0x7F && 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) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name); } else if (fakeBpb.oem_name[0] < 0x20 && 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) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name, Encoding, start: 1); } } if (fakeBpb.signature == 0x28 || fakeBpb.signature == 0x29) { XmlFsType.VolumeSerial = $"{fakeBpb.serial_no:X8}"; } } if (XmlFsType.SystemIdentifier != null) { sb.AppendFormat("OEM Name: {0}", XmlFsType.SystemIdentifier.Trim()).AppendLine(); } sb.AppendFormat("{0} bytes per sector.", fakeBpb.bps).AppendLine(); if (bpbKind != BpbKind.Human) { if (fakeBpb.sectors == 0) { sb.AppendFormat("{0} sectors on volume ({1} bytes).", fakeBpb.big_sectors, fakeBpb.big_sectors * fakeBpb.bps).AppendLine(); XmlFsType.Clusters = fakeBpb.spc == 0 ? fakeBpb.big_sectors : fakeBpb.big_sectors / fakeBpb.spc; } else { sb.AppendFormat("{0} sectors on volume ({1} bytes).", fakeBpb.sectors, fakeBpb.sectors * fakeBpb.bps).AppendLine(); XmlFsType.Clusters = (ulong)(fakeBpb.spc == 0 ? fakeBpb.sectors : fakeBpb.sectors / fakeBpb.spc); } } else { XmlFsType.Clusters = humanBpb.clusters == 0 ? humanBpb.big_clusters : humanBpb.clusters; sb.AppendFormat("{0} sectors on volume ({1} bytes).", (XmlFsType.Clusters * humanBpb.bpc) / imagePlugin.Info.SectorSize, XmlFsType.Clusters * humanBpb.bpc).AppendLine(); } sb.AppendFormat("{0} sectors per cluster.", fakeBpb.spc).AppendLine(); sb.AppendFormat("{0} clusters on volume.", XmlFsType.Clusters).AppendLine(); XmlFsType.ClusterSize = (uint)(fakeBpb.bps * fakeBpb.spc); sb.AppendFormat("{0} sectors reserved between BPB and FAT.", fakeBpb.rsectors).AppendLine(); sb.AppendFormat("{0} FATs.", fakeBpb.fats_no).AppendLine(); sb.AppendFormat("{0} entries on root directory.", fakeBpb.root_ent).AppendLine(); if (fakeBpb.media > 0) { sb.AppendFormat("Media descriptor: 0x{0:X2}", fakeBpb.media).AppendLine(); } sb.AppendFormat("{0} sectors per FAT.", fakeBpb.spfat).AppendLine(); if (fakeBpb.sptrk > 0 && fakeBpb.sptrk < 64 && fakeBpb.heads > 0 && fakeBpb.heads < 256) { sb.AppendFormat("{0} sectors per track.", fakeBpb.sptrk).AppendLine(); sb.AppendFormat("{0} heads.", fakeBpb.heads).AppendLine(); } if (fakeBpb.hsectors <= partition.Start) { sb.AppendFormat("{0} hidden sectors before BPB.", fakeBpb.hsectors).AppendLine(); } if (fakeBpb.signature == 0x28 || fakeBpb.signature == 0x29 || andosOemCorrect) { sb.AppendFormat("Drive number: 0x{0:X2}", fakeBpb.drive_no).AppendLine(); if (XmlFsType.VolumeSerial != null) { sb.AppendFormat("Volume Serial Number: {0}", XmlFsType.VolumeSerial).AppendLine(); } if ((fakeBpb.flags & 0xF8) == 0x00) { if ((fakeBpb.flags & 0x01) == 0x01) { sb.AppendLine("Volume should be checked on next mount."); XmlFsType.Dirty = true; } if ((fakeBpb.flags & 0x02) == 0x02) { sb.AppendLine("Disk surface should be on next mount."); } } if (fakeBpb.signature == 0x29 || andosOemCorrect) { XmlFsType.VolumeName = Encoding.ASCII.GetString(fakeBpb.volume_label); sb.AppendFormat("Filesystem type: {0}", Encoding.ASCII.GetString(fakeBpb.fs_type)).AppendLine(); } } else if (bpbKind == BpbKind.Atari && XmlFsType.VolumeSerial != null) { sb.AppendFormat("Volume Serial Number: {0}", XmlFsType.VolumeSerial).AppendLine(); } bootChk = Sha1Context.Data(fakeBpb.boot_code, out _); // Workaround that PCExchange jumps into "FAT16 "... if (XmlFsType.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 (XmlFsType.Bootable == false && fakeBpb.jump != null) { XmlFsType.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); } sectorsPerRealSector = fakeBpb.bps / imagePlugin.Info.SectorSize; // First root directory sector rootDirectorySector = (ulong)((fakeBpb.spfat * fakeBpb.fats_no) + fakeBpb.rsectors) * sectorsPerRealSector; sectorsForRootDirectory = (uint)((fakeBpb.root_ent * 32) / imagePlugin.Info.SectorSize); } if (extraInfo != null) { sb.Append(extraInfo); } if (rootDirectorySector + partition.Start < partition.End && imagePlugin.Info.XmlMediaType != XmlMediaType.OpticalDisc) { byte[] rootDirectory = imagePlugin.ReadSectors(rootDirectorySector + partition.Start, sectorsForRootDirectory); if (bpbKind == BpbKind.DecRainbow) { var rootMs = new MemoryStream(); foreach (byte[] tmp in from ulong rootSector in new[] { 0x17, 0x19, 0x1B, 0x1D, 0x1E, 0x20 } select imagePlugin.ReadSector(rootSector)) { rootMs.Write(tmp, 0, tmp.Length); } rootDirectory = rootMs.ToArray(); } for (int i = 0; i < rootDirectory.Length; i += 32) { // Not a correct entry if (rootDirectory[i] < DIRENT_MIN && rootDirectory[i] != DIRENT_E5) { continue; } // Deleted or subdirectory entry if (rootDirectory[i] == DIRENT_SUBDIR || rootDirectory[i] == DIRENT_DELETED) { continue; } // Not a volume label if (rootDirectory[i + 0x0B] != 0x08 && rootDirectory[i + 0x0B] != 0x28) { continue; } DirectoryEntry entry = Marshal.ByteArrayToStructureLittleEndian <DirectoryEntry>(rootDirectory, i, 32); 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)) { XmlFsType.VolumeName = entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) ? volname.ToLower() : volname; } if (entry.ctime > 0 && entry.cdate > 0) { XmlFsType.CreationDate = DateHandlers.DosToDateTime(entry.cdate, entry.ctime); if (entry.ctime_ms > 0) { XmlFsType.CreationDate = XmlFsType.CreationDate.AddMilliseconds(entry.ctime_ms * 10); } XmlFsType.CreationDateSpecified = true; sb.AppendFormat("Volume created on {0}", XmlFsType.CreationDate).AppendLine(); } if (entry.mtime > 0 && entry.mdate > 0) { XmlFsType.ModificationDate = DateHandlers.DosToDateTime(entry.mdate, entry.mtime); XmlFsType.ModificationDateSpecified = true; sb.AppendFormat("Volume last modified on {0}", XmlFsType.ModificationDate).AppendLine(); } if (entry.adate > 0) { sb.AppendFormat("Volume last accessed on {0:d}", DateHandlers.DosToDateTime(entry.adate, 0)). AppendLine(); } break; } } if (!string.IsNullOrEmpty(XmlFsType.VolumeName)) { sb.AppendFormat("Volume label: {0}", XmlFsType.VolumeName).AppendLine(); } if (XmlFsType.Bootable) { // Intel short jump if (bpbSector[0] == 0xEB && bpbSector[1] < 0x80) { int sigSize = bpbSector[510] == 0x55 && bpbSector[511] == 0xAA ? 2 : 0; byte[] bootCode = new byte[512 - sigSize - bpbSector[1] - 2]; Array.Copy(bpbSector, bpbSector[1] + 2, bootCode, 0, bootCode.Length); Sha1Context.Data(bootCode, out _); } // Intel big jump else if (bpbSector[0] == 0xE9 && BitConverter.ToUInt16(bpbSector, 1) < 0x1FC) { int sigSize = bpbSector[510] == 0x55 && bpbSector[511] == 0xAA ? 2 : 0; byte[] bootCode = new byte[512 - sigSize - BitConverter.ToUInt16(bpbSector, 1) - 3]; Array.Copy(bpbSector, BitConverter.ToUInt16(bpbSector, 1) + 3, bootCode, 0, bootCode.Length); Sha1Context.Data(bootCode, out _); } sb.AppendLine("Volume is bootable"); sb.AppendFormat("Boot code's SHA1: {0}", bootChk).AppendLine(); string bootName = knownBootHashes.FirstOrDefault(t => t.hash == bootChk).name; if (string.IsNullOrWhiteSpace(bootName)) { sb.AppendLine("Unknown boot code."); } else { sb.AppendFormat("Boot code corresponds to {0}", bootName).AppendLine(); } } information = sb.ToString(); }
/// <inheritdoc /> public Errno Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding, Dictionary <string, string> options, string @namespace) { XmlFsType = new FileSystemType(); 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(Errno.InvalidArgument); } AaruConsole.DebugWriteLine("FAT plugin", "Reading BPB"); uint sectorsPerBpb = imagePlugin.Info.SectorSize < 512 ? 512 / imagePlugin.Info.SectorSize : 1; byte[] bpbSector = imagePlugin.ReadSectors(0 + partition.Start, sectorsPerBpb); 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; XmlFsType.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.XmlMediaType == XmlMediaType.OpticalDisc) { fat32Bpb.bps *= 4; fat32Bpb.spc /= 4; fat32Bpb.big_spfat /= 4; fat32Bpb.hsectors /= 4; fat32Bpb.sptrk /= 4; } XmlFsType.Type = fat32Bpb.version != 0 ? "FAT+" : "FAT32"; if (fat32Bpb.oem_name != null && (fat32Bpb.oem_name[5] != 0x49 || fat32Bpb.oem_name[6] != 0x48 || fat32Bpb.oem_name[7] != 0x43)) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fat32Bpb.oem_name); } _sectorsPerCluster = fat32Bpb.spc; XmlFsType.ClusterSize = (uint)(fat32Bpb.bps * fat32Bpb.spc); _reservedSectors = fat32Bpb.rsectors; if (fat32Bpb.big_sectors == 0 && fat32Bpb.signature == 0x28) { XmlFsType.Clusters = shortFat32Bpb.huge_sectors / shortFat32Bpb.spc; } else if (fat32Bpb.sectors == 0) { XmlFsType.Clusters = fat32Bpb.big_sectors / fat32Bpb.spc; } else { XmlFsType.Clusters = (ulong)(fat32Bpb.sectors / fat32Bpb.spc); } _sectorsPerFat = fat32Bpb.big_spfat; XmlFsType.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) { XmlFsType.Dirty = true; } } if ((fat32Bpb.mirror_flags & 0x80) == 0x80) { _useFirstFat = (fat32Bpb.mirror_flags & 0xF) != 1; } if (fat32Bpb.signature == 0x29) { XmlFsType.VolumeName = StringHandlers.SpacePaddedToString(fat32Bpb.volume_label, Encoding); XmlFsType.VolumeName = XmlFsType.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"...... XmlFsType.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) { byte[] fsinfoSector = imagePlugin.ReadSector(fat32Bpb.fsinfo_sector + partition.Start); FsInfoSector fsInfo = Marshal.ByteArrayToStructureLittleEndian <FsInfoSector>(fsinfoSector); if (fsInfo.signature1 == FSINFO_SIGNATURE1 && fsInfo.signature2 == FSINFO_SIGNATURE2 && fsInfo.signature3 == FSINFO_SIGNATURE3) { if (fsInfo.free_clusters < 0xFFFFFFFF) { XmlFsType.FreeClusters = fsInfo.free_clusters; XmlFsType.FreeClustersSpecified = true; } } } 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) { XmlFsType.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; } XmlFsType.Bootable = true; break; } ulong firstRootSector = 0; if (!_fat32) { // This is to support FAT partitions on hybrid ISO/USB images if (imagePlugin.Info.XmlMediaType == XmlMediaType.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]; _reservedSectors = fakeBpb.rsectors; sectorsPerRealSector = fakeBpb.bps / imagePlugin.Info.SectorSize; _fatFirstSector = partition.Start + (_reservedSectors * sectorsPerRealSector); byte[] fatBytes = imagePlugin.ReadSectors(_fatFirstSector, fakeBpb.spfat); 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; foreach (ushort entry in fat12) { if (entry >= FAT12_RESERVED || entry <= clusters) { continue; } fat12Valid = false; break; } ushort[] fat16 = MemoryMarshal.Cast <byte, ushort>(fatBytes).ToArray(); bool fat16Valid = fat16[0] >= FAT16_RESERVED && fat16[1] >= 0x3FF0; foreach (ushort entry in fat16) { if (entry >= FAT16_RESERVED || entry <= clusters) { continue; } fat16Valid = false; break; } _fat12 = fat12Valid; _fat16 = fat16Valid; // Check BPB type if (_fat12 == _fat16) { _fat12 = Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT12 "; _fat16 = Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT16 "; } } else { _fat16 = true; } } if (_fat12) { XmlFsType.Type = "FAT12"; } else if (_fat16) { XmlFsType.Type = "FAT16"; } if (bpbKind == BpbKind.Atari) { if (atariBpb.serial_no[0] != 0x49 || atariBpb.serial_no[1] != 0x48 || atariBpb.serial_no[2] != 0x43) { 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]) }; } XmlFsType.SystemIdentifier = StringHandlers.CToString(atariBpb.oem_name); if (string.IsNullOrEmpty(XmlFsType.SystemIdentifier)) { XmlFsType.SystemIdentifier = null; } } else if (fakeBpb.oem_name != null) { if (fakeBpb.oem_name[5] != 0x49 || fakeBpb.oem_name[6] != 0x48 || fakeBpb.oem_name[7] != 0x43) { // 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 if (fakeBpb.oem_name[0] >= 0x20 && fakeBpb.oem_name[0] <= 0x7F && 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) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name); } else if (fakeBpb.oem_name[0] < 0x20 && 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) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name, Encoding, start: 1); } } if (fakeBpb.signature == 0x28 || fakeBpb.signature == 0x29) { XmlFsType.VolumeSerial = $"{fakeBpb.serial_no:X8}"; _statfs.Id = new FileSystemId { IsInt = true, Serial32 = fakeBpb.serial_no }; } } XmlFsType.Clusters = clusters; _sectorsPerCluster = fakeBpb.spc; XmlFsType.ClusterSize = (uint)(fakeBpb.bps * fakeBpb.spc); _reservedSectors = fakeBpb.rsectors; _sectorsPerFat = fakeBpb.spfat; if (fakeBpb.signature == 0x28 || fakeBpb.signature == 0x29 || andosOemCorrect) { if ((fakeBpb.flags & 0xF8) == 0x00) { if ((fakeBpb.flags & 0x01) == 0x01) { XmlFsType.Dirty = true; } } if (fakeBpb.signature == 0x29 || andosOemCorrect) { XmlFsType.VolumeName = StringHandlers.SpacePaddedToString(fakeBpb.volume_label, Encoding); XmlFsType.VolumeName = XmlFsType.VolumeName?.Replace("\0", ""); } } // Workaround that PCExchange jumps into "FAT16 "... if (XmlFsType.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 (XmlFsType.Bootable == false && fakeBpb.jump != null) { XmlFsType.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); rootDirectory = imagePlugin.ReadSectors(firstRootSector, sectorsForRootDirectory); if (bpbKind == BpbKind.DecRainbow) { var rootMs = new MemoryStream(); foreach (byte[] tmp in from ulong rootSector in new[] { 0x17, 0x19, 0x1B, 0x1D, 0x1E, 0x20 } select imagePlugin.ReadSector(rootSector)) { rootMs.Write(tmp, 0, tmp.Length); } rootDirectory = rootMs.ToArray(); } } else { if (rootDirectoryCluster == 0) { return(Errno.InvalidArgument); } var rootMs = new MemoryStream(); uint[] rootDirectoryClusters = GetClusters(rootDirectoryCluster); foreach (byte[] buffer in rootDirectoryClusters.Select(cluster => imagePlugin. ReadSectors(_firstClusterSector + (cluster * _sectorsPerCluster), _sectorsPerCluster))) { 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(Errno.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)) { XmlFsType.VolumeName = entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) && _namespace == Namespace.Nt ? volname.ToLower() : volname; } XmlFsType.VolumeName = XmlFsType.VolumeName?.Replace("\0", ""); if (entry.ctime > 0 && entry.cdate > 0) { XmlFsType.CreationDate = DateHandlers.DosToDateTime(entry.cdate, entry.ctime); if (entry.ctime_ms > 0) { XmlFsType.CreationDate = XmlFsType.CreationDate.AddMilliseconds(entry.ctime_ms * 10); } XmlFsType.CreationDateSpecified = true; } if (entry.mtime > 0 && entry.mdate > 0) { XmlFsType.ModificationDate = DateHandlers.DosToDateTime(entry.mdate, entry.mtime); XmlFsType.ModificationDateSpecified = true; } continue; } var completeEntry = new CompleteDirectoryEntry { Dirent = entry }; if ((_namespace == Namespace.Lfn || _namespace == 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("FAT filesystem", "Found empty filename in root directory"); if (!_debug || (entry.size > 0 && entry.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; } XmlFsType.VolumeName = XmlFsType.VolumeName?.Trim(); _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")}"; break; 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")}"; break; case BpbKind.ShortFat32: case BpbKind.LongFat32: _statfs.Type = XmlFsType.Type == "FAT+" ? "FAT+" : "Microsoft FAT32"; break; case BpbKind.Andos: _statfs.Type = $"ANDOS FAT{(_fat16 ? "16" : "12")}"; break; case BpbKind.Apricot: _statfs.Type = $"Apricot FAT{(_fat16 ? "16" : "12")}"; break; case BpbKind.DecRainbow: _statfs.Type = $"DEC FAT{(_fat16 ? "16" : "12")}"; break; case BpbKind.Human: _statfs.Type = $"Human68k FAT{(_fat16 ? "16" : "12")}"; break; default: throw new ArgumentOutOfRangeException(); } _bytesPerCluster = _sectorsPerCluster * imagePlugin.Info.SectorSize; ushort[] _firstFatEntries = new ushort[_statfs.Blocks]; ushort[] _secondFatEntries = new ushort[_statfs.Blocks]; bool _firstFatValid = true; bool _secondFatValid = true; if (_fat12) { AaruConsole.DebugWriteLine("FAT plugin", "Reading FAT12"); byte[] fatBytes = imagePlugin.ReadSectors(_fatFirstSector, _sectorsPerFat); 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)); } fatBytes = imagePlugin.ReadSectors(_fatFirstSector + _sectorsPerFat, _sectorsPerFat); _fatEntries = new ushort[_statfs.Blocks]; 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)); } foreach (ushort entry in _firstFatEntries) { if (entry >= FAT12_RESERVED || entry <= _statfs.Blocks) { continue; } _firstFatValid = false; break; } foreach (ushort entry in _secondFatEntries) { if (entry >= FAT12_RESERVED || entry <= _statfs.Blocks) { continue; } _secondFatValid = false; break; } if (_firstFatValid == _secondFatValid) { _fatEntries = _useFirstFat ? _firstFatEntries : _secondFatEntries; } else if (_firstFatValid) { _fatEntries = _firstFatEntries; } else { _fatEntries = _secondFatEntries; } } else if (_fat16) { AaruConsole.DebugWriteLine("FAT plugin", "Reading FAT16"); byte[] fatBytes = imagePlugin.ReadSectors(_fatFirstSector, _sectorsPerFat); AaruConsole.DebugWriteLine("FAT plugin", "Casting FAT"); _firstFatEntries = MemoryMarshal.Cast <byte, ushort>(fatBytes).ToArray(); fatBytes = imagePlugin.ReadSectors(_fatFirstSector + _sectorsPerFat, _sectorsPerFat); AaruConsole.DebugWriteLine("FAT plugin", "Casting FAT"); _secondFatEntries = MemoryMarshal.Cast <byte, ushort>(fatBytes).ToArray(); foreach (ushort entry in _firstFatEntries) { if (entry >= FAT16_RESERVED || entry <= _statfs.Blocks) { continue; } _firstFatValid = false; break; } foreach (ushort entry in _secondFatEntries) { if (entry >= FAT16_RESERVED || entry <= _statfs.Blocks) { continue; } _secondFatValid = false; break; } 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 == Namespace.Os2 || _namespace == 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))
static BpbKind DetectBpbKind(byte[] bpbSector, IMediaImage imagePlugin, Partition partition, out BiosParameterBlockEbpb fakeBpb, out HumanParameterBlock humanBpb, out AtariParameterBlock atariBpb, out byte minBootNearJump, out bool andosOemCorrect, out bool bootable) { fakeBpb = new BiosParameterBlockEbpb(); minBootNearJump = 0; andosOemCorrect = false; bootable = false; humanBpb = Marshal.ByteArrayToStructureBigEndian <HumanParameterBlock>(bpbSector); atariBpb = Marshal.ByteArrayToStructureLittleEndian <AtariParameterBlock>(bpbSector); ulong expectedClusters = humanBpb.bpc > 0 ? partition.Size / humanBpb.bpc : 0; // Check clusters for Human68k are correct bool humanClustersCorrect = humanBpb.clusters == 0 ? humanBpb.big_clusters == expectedClusters : humanBpb.clusters == expectedClusters; // Check OEM for Human68k is correct bool humanOemCorrect = bpbSector[2] >= 0x20 && bpbSector[3] >= 0x20 && bpbSector[4] >= 0x20 && bpbSector[5] >= 0x20 && bpbSector[6] >= 0x20 && bpbSector[7] >= 0x20 && bpbSector[8] >= 0x20 && bpbSector[9] >= 0x20 && bpbSector[10] >= 0x20 && bpbSector[11] >= 0x20 && bpbSector[12] >= 0x20 && bpbSector[13] >= 0x20 && bpbSector[14] >= 0x20 && bpbSector[15] >= 0x20 && bpbSector[16] >= 0x20 && bpbSector[17] >= 0x20; // Check correct branch for Human68k bool humanBranchCorrect = bpbSector[0] == 0x60 && bpbSector[1] >= 0x1C && bpbSector[1] < 0xFE; AaruConsole.DebugWriteLine("FAT plugin", "humanClustersCorrect = {0}", humanClustersCorrect); AaruConsole.DebugWriteLine("FAT plugin", "humanOemCorrect = {0}", humanOemCorrect); AaruConsole.DebugWriteLine("FAT plugin", "humanBranchCorrect = {0}", humanBranchCorrect); // If all Human68k checks are correct, it is a Human68k FAT16 bool useHumanBpb = humanClustersCorrect && humanOemCorrect && humanBranchCorrect && expectedClusters > 0; if (useHumanBpb) { AaruConsole.DebugWriteLine("FAT plugin", "Using Human68k BPB"); fakeBpb.jump = humanBpb.jump; fakeBpb.oem_name = humanBpb.oem_name; fakeBpb.bps = (ushort)imagePlugin.Info.SectorSize; fakeBpb.spc = (byte)(humanBpb.bpc / fakeBpb.bps); fakeBpb.fats_no = 2; fakeBpb.root_ent = humanBpb.root_ent; fakeBpb.media = humanBpb.media; fakeBpb.spfat = (ushort)(humanBpb.cpfat * fakeBpb.spc); fakeBpb.boot_code = humanBpb.boot_code; fakeBpb.sectors = humanBpb.clusters; fakeBpb.big_sectors = humanBpb.big_clusters; fakeBpb.rsectors = 1; return(BpbKind.Human); } var msxBpb = new MsxParameterBlock(); var dos2Bpb = new BiosParameterBlock2(); var dos30Bpb = new BiosParameterBlock30(); var dos32Bpb = new BiosParameterBlock32(); var dos33Bpb = new BiosParameterBlock33(); var shortEbpb = new BiosParameterBlockShortEbpb(); var ebpb = new BiosParameterBlockEbpb(); var apricotBpb = new ApricotLabel(); bool useAtariBpb = false; bool useMsxBpb = false; bool useDos2Bpb = false; bool useDos3Bpb = false; bool useDos32Bpb = false; bool useDos33Bpb = false; bool userShortExtendedBpb = false; bool useExtendedBpb = false; bool useShortFat32 = false; bool useLongFat32 = false; bool useApricotBpb = false; bool useDecRainbowBpb = false; if (imagePlugin.Info.SectorSize >= 256) { msxBpb = Marshal.ByteArrayToStructureLittleEndian <MsxParameterBlock>(bpbSector); dos2Bpb = Marshal.ByteArrayToStructureLittleEndian <BiosParameterBlock2>(bpbSector); dos30Bpb = Marshal.ByteArrayToStructureLittleEndian <BiosParameterBlock30>(bpbSector); dos32Bpb = Marshal.ByteArrayToStructureLittleEndian <BiosParameterBlock32>(bpbSector); dos33Bpb = Marshal.ByteArrayToStructureLittleEndian <BiosParameterBlock33>(bpbSector); shortEbpb = Marshal.ByteArrayToStructureLittleEndian <BiosParameterBlockShortEbpb>(bpbSector); ebpb = Marshal.ByteArrayToStructureLittleEndian <BiosParameterBlockEbpb>(bpbSector); Fat32ParameterBlockShort shortFat32Bpb = Marshal.ByteArrayToStructureLittleEndian <Fat32ParameterBlockShort>(bpbSector); Fat32ParameterBlock fat32Bpb = Marshal.ByteArrayToStructureLittleEndian <Fat32ParameterBlock>(bpbSector); apricotBpb = Marshal.ByteArrayToStructureLittleEndian <ApricotLabel>(bpbSector); int bitsInBpsMsx = CountBits.Count(msxBpb.bps); int bitsInBpsDos33 = CountBits.Count(dos33Bpb.bps); int bitsInBpsDos40 = CountBits.Count(ebpb.bps); int bitsInBpsFat32Short = CountBits.Count(shortFat32Bpb.bps); int bitsInBpsFat32 = CountBits.Count(fat32Bpb.bps); int bitsInBpsApricot = CountBits.Count(apricotBpb.mainBPB.bps); bool correctSpcMsx = msxBpb.spc == 1 || msxBpb.spc == 2 || msxBpb.spc == 4 || msxBpb.spc == 8 || msxBpb.spc == 16 || msxBpb.spc == 32 || msxBpb.spc == 64; bool correctSpcDos33 = dos33Bpb.spc == 1 || dos33Bpb.spc == 2 || dos33Bpb.spc == 4 || dos33Bpb.spc == 8 || dos33Bpb.spc == 16 || dos33Bpb.spc == 32 || dos33Bpb.spc == 64; bool correctSpcDos40 = ebpb.spc == 1 || ebpb.spc == 2 || ebpb.spc == 4 || ebpb.spc == 8 || ebpb.spc == 16 || ebpb.spc == 32 || ebpb.spc == 64; bool correctSpcFat32Short = shortFat32Bpb.spc == 1 || shortFat32Bpb.spc == 2 || shortFat32Bpb.spc == 4 || shortFat32Bpb.spc == 8 || shortFat32Bpb.spc == 16 || shortFat32Bpb.spc == 32 || shortFat32Bpb.spc == 64; bool correctSpcFat32 = fat32Bpb.spc == 1 || fat32Bpb.spc == 2 || fat32Bpb.spc == 4 || fat32Bpb.spc == 8 || fat32Bpb.spc == 16 || fat32Bpb.spc == 32 || fat32Bpb.spc == 64; bool correctSpcApricot = apricotBpb.mainBPB.spc == 1 || apricotBpb.mainBPB.spc == 2 || apricotBpb.mainBPB.spc == 4 || apricotBpb.mainBPB.spc == 8 || apricotBpb.mainBPB.spc == 16 || apricotBpb.mainBPB.spc == 32 || apricotBpb.mainBPB.spc == 64; // This is to support FAT partitions on hybrid ISO/USB images if (imagePlugin.Info.XmlMediaType == XmlMediaType.OpticalDisc) { atariBpb.sectors /= 4; msxBpb.sectors /= 4; dos2Bpb.sectors /= 4; dos30Bpb.sectors /= 4; dos32Bpb.sectors /= 4; dos33Bpb.sectors /= 4; dos33Bpb.big_sectors /= 4; shortEbpb.sectors /= 4; shortEbpb.big_sectors /= 4; ebpb.sectors /= 4; ebpb.big_sectors /= 4; shortFat32Bpb.sectors /= 4; shortFat32Bpb.big_sectors /= 4; shortFat32Bpb.huge_sectors /= 4; fat32Bpb.sectors /= 4; fat32Bpb.big_sectors /= 4; apricotBpb.mainBPB.sectors /= 4; } andosOemCorrect = dos33Bpb.oem_name[0] < 0x20 && dos33Bpb.oem_name[1] >= 0x20 && dos33Bpb.oem_name[2] >= 0x20 && dos33Bpb.oem_name[3] >= 0x20 && dos33Bpb.oem_name[4] >= 0x20 && dos33Bpb.oem_name[5] >= 0x20 && dos33Bpb.oem_name[6] >= 0x20 && dos33Bpb.oem_name[7] >= 0x20; if (bitsInBpsFat32 == 1 && correctSpcFat32 && fat32Bpb.fats_no <= 2 && fat32Bpb.sectors == 0 && fat32Bpb.spfat == 0 && fat32Bpb.signature == 0x29 && Encoding.ASCII.GetString(fat32Bpb.fs_type) == "FAT32 ") { AaruConsole.DebugWriteLine("FAT plugin", "Using FAT32 BPB"); minBootNearJump = 0x58; return(BpbKind.LongFat32); } if (bitsInBpsFat32Short == 1 && correctSpcFat32Short && shortFat32Bpb.fats_no <= 2 && shortFat32Bpb.sectors == 0 && shortFat32Bpb.spfat == 0 && shortFat32Bpb.signature == 0x28) { AaruConsole.DebugWriteLine("FAT plugin", "Using short FAT32 BPB"); minBootNearJump = 0x57; return(BpbKind.ShortFat32); } if (bitsInBpsMsx == 1 && correctSpcMsx && msxBpb.fats_no <= 2 && msxBpb.root_ent > 0 && msxBpb.sectors <= (partition.End - partition.Start) + 1 && msxBpb.spfat > 0 && Encoding.ASCII.GetString(msxBpb.vol_id) == "VOL_ID") { AaruConsole.DebugWriteLine("FAT plugin", "Using MSX BPB"); useMsxBpb = true; } else if (bitsInBpsApricot == 1 && correctSpcApricot && apricotBpb.mainBPB.fats_no <= 2 && apricotBpb.mainBPB.root_ent > 0 && apricotBpb.mainBPB.sectors <= (partition.End - partition.Start) + 1 && apricotBpb.mainBPB.spfat > 0 && apricotBpb.partitionCount == 0) { AaruConsole.DebugWriteLine("FAT plugin", "Using Apricot BPB"); useApricotBpb = true; } else if (bitsInBpsDos40 == 1 && correctSpcDos40 && ebpb.fats_no <= 2 && ebpb.root_ent > 0 && ebpb.spfat > 0 && (ebpb.signature == 0x28 || ebpb.signature == 0x29 || andosOemCorrect)) { if (ebpb.sectors == 0) { if (ebpb.big_sectors <= (partition.End - partition.Start) + 1) { if (ebpb.signature == 0x29 || andosOemCorrect) { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 4.0 BPB"); useExtendedBpb = true; minBootNearJump = 0x3C; } else { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 3.4 BPB"); userShortExtendedBpb = true; minBootNearJump = 0x29; } } } else if (ebpb.sectors <= (partition.End - partition.Start) + 1) { if (ebpb.signature == 0x29 || andosOemCorrect) { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 4.0 BPB"); useExtendedBpb = true; minBootNearJump = 0x3C; } else { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 3.4 BPB"); userShortExtendedBpb = true; minBootNearJump = 0x29; } } } else if (bitsInBpsDos33 == 1 && correctSpcDos33 && dos33Bpb.rsectors < partition.End - partition.Start && dos33Bpb.fats_no <= 2 && dos33Bpb.root_ent > 0 && dos33Bpb.spfat > 0) { if (dos33Bpb.sectors == 0 && dos33Bpb.hsectors <= partition.Start && dos33Bpb.big_sectors > 0 && dos33Bpb.big_sectors <= (partition.End - partition.Start) + 1) { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 3.3 BPB"); useDos33Bpb = true; minBootNearJump = 0x22; } else if (dos33Bpb.big_sectors == 0 && dos33Bpb.hsectors <= partition.Start && dos33Bpb.sectors > 0 && dos33Bpb.sectors <= (partition.End - partition.Start) + 1) { if (atariBpb.jump[0] == 0x60 || (atariBpb.jump[0] == 0xE9 && atariBpb.jump[1] == 0x00 && Encoding.ASCII.GetString(dos33Bpb.oem_name) != "NEXT ")) { AaruConsole.DebugWriteLine("FAT plugin", "Using Atari BPB"); useAtariBpb = true; } else { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 3.3 BPB"); useDos33Bpb = true; minBootNearJump = 0x22; } } else { if (dos32Bpb.hsectors <= partition.Start && dos32Bpb.hsectors + dos32Bpb.sectors == dos32Bpb.total_sectors) { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 3.2 BPB"); useDos32Bpb = true; minBootNearJump = 0x1E; } else if (dos30Bpb.sptrk > 0 && dos30Bpb.sptrk < 64 && dos30Bpb.heads > 0 && dos30Bpb.heads < 256) { if (atariBpb.jump[0] == 0x60 || (atariBpb.jump[0] == 0xE9 && atariBpb.jump[1] == 0x00 && Encoding.ASCII.GetString(dos33Bpb.oem_name) != "NEXT ")) { AaruConsole.DebugWriteLine("FAT plugin", "Using Atari BPB"); useAtariBpb = true; } else { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 3.0 BPB"); useDos3Bpb = true; minBootNearJump = 0x1C; } } else { if (atariBpb.jump[0] == 0x60 || (atariBpb.jump[0] == 0xE9 && atariBpb.jump[1] == 0x00 && Encoding.ASCII.GetString(dos33Bpb.oem_name) != "NEXT ")) { AaruConsole.DebugWriteLine("FAT plugin", "Using Atari BPB"); useAtariBpb = true; } else { AaruConsole.DebugWriteLine("FAT plugin", "Using DOS 2.0 BPB"); useDos2Bpb = true; minBootNearJump = 0x16; } } } } } // DEC Rainbow, lacks a BPB but has a very concrete structure... if (imagePlugin.Info.Sectors == 800 && imagePlugin.Info.SectorSize == 512 && !useAtariBpb && !useMsxBpb && !useDos2Bpb && !useDos3Bpb && !useDos32Bpb && !useDos33Bpb && !userShortExtendedBpb && !useExtendedBpb && !useShortFat32 && !useLongFat32 && !useApricotBpb) { // DEC Rainbow boots up with a Z80, first byte should be DI (disable interrupts) byte z80Di = bpbSector[0]; // First FAT1 sector resides at LBA 0x14 byte[] fat1Sector0 = imagePlugin.ReadSector(0x14); // First FAT2 sector resides at LBA 0x1A byte[] fat2Sector0 = imagePlugin.ReadSector(0x1A); bool equalFatIds = fat1Sector0[0] == fat2Sector0[0] && fat1Sector0[1] == fat2Sector0[1]; // Volume is software interleaved 2:1 var rootMs = new MemoryStream(); foreach (byte[] tmp in from ulong rootSector in new[] { 0x17, 0x19, 0x1B, 0x1D, 0x1E, 0x20 } select imagePlugin.ReadSector(rootSector)) { rootMs.Write(tmp, 0, tmp.Length); } byte[] rootDir = rootMs.ToArray(); bool validRootDir = true; // Iterate all root directory for (int e = 0; e < 96 * 32; e += 32) { for (int c = 0; c < 11; c++) { if ((rootDir[c + e] < 0x20 && rootDir[c + e] != 0x00 && rootDir[c + e] != 0x05) || rootDir[c + e] == 0xFF || rootDir[c + e] == 0x2E) { validRootDir = false; break; } } if (!validRootDir) { break; } } if (z80Di == 0xF3 && equalFatIds && (fat1Sector0[0] & 0xF0) == 0xF0 && fat1Sector0[1] == 0xFF && validRootDir) { useDecRainbowBpb = true; AaruConsole.DebugWriteLine("FAT plugin", "Using DEC Rainbow hardcoded BPB."); fakeBpb.bps = 512; fakeBpb.spc = 1; fakeBpb.rsectors = 20; fakeBpb.fats_no = 2; fakeBpb.root_ent = 96; fakeBpb.sectors = 800; fakeBpb.media = 0xFA; fakeBpb.sptrk = 10; fakeBpb.heads = 1; fakeBpb.hsectors = 0; fakeBpb.spfat = 3; bootable = true; fakeBpb.boot_code = bpbSector; return(BpbKind.DecRainbow); } } if (!useAtariBpb && !useMsxBpb && !useDos2Bpb && !useDos3Bpb && !useDos32Bpb && !useDos33Bpb && !useHumanBpb && !userShortExtendedBpb && !useExtendedBpb && !useShortFat32 && !useLongFat32 && !useApricotBpb && !useDecRainbowBpb) { byte[] fatSector = imagePlugin.ReadSector(1 + partition.Start); switch (fatSector[0]) { case 0xE5: if (imagePlugin.Info.Sectors == 2002 && imagePlugin.Info.SectorSize == 128) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB."); fakeBpb.bps = 128; fakeBpb.spc = 4; fakeBpb.rsectors = 1; fakeBpb.fats_no = 2; fakeBpb.root_ent = 64; fakeBpb.sectors = 2002; fakeBpb.media = 0xE5; fakeBpb.sptrk = 26; fakeBpb.heads = 1; fakeBpb.hsectors = 0; fakeBpb.spfat = 1; } break; case 0xFD: if (imagePlugin.Info.Sectors == 4004 && imagePlugin.Info.SectorSize == 128) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB."); fakeBpb.bps = 128; fakeBpb.spc = 4; fakeBpb.rsectors = 4; fakeBpb.fats_no = 2; fakeBpb.root_ent = 68; fakeBpb.sectors = 4004; fakeBpb.media = 0xFD; fakeBpb.sptrk = 26; fakeBpb.heads = 2; fakeBpb.hsectors = 0; fakeBpb.spfat = 6; } else if (imagePlugin.Info.Sectors == 2002 && imagePlugin.Info.SectorSize == 128) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB."); fakeBpb.bps = 128; fakeBpb.spc = 4; fakeBpb.rsectors = 4; fakeBpb.fats_no = 2; fakeBpb.root_ent = 68; fakeBpb.sectors = 2002; fakeBpb.media = 0xFD; fakeBpb.sptrk = 26; fakeBpb.heads = 1; fakeBpb.hsectors = 0; fakeBpb.spfat = 6; } break; case 0xFE: if (imagePlugin.Info.Sectors == 320 && imagePlugin.Info.SectorSize == 512) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB for 5.25\" SSDD."); fakeBpb.bps = 512; fakeBpb.spc = 1; fakeBpb.rsectors = 1; fakeBpb.fats_no = 2; fakeBpb.root_ent = 64; fakeBpb.sectors = 320; fakeBpb.media = 0xFE; fakeBpb.sptrk = 8; fakeBpb.heads = 1; fakeBpb.hsectors = 0; fakeBpb.spfat = 1; } else if (imagePlugin.Info.Sectors == 2002 && imagePlugin.Info.SectorSize == 128) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB."); fakeBpb.bps = 128; fakeBpb.spc = 4; fakeBpb.rsectors = 1; fakeBpb.fats_no = 2; fakeBpb.root_ent = 68; fakeBpb.sectors = 2002; fakeBpb.media = 0xFE; fakeBpb.sptrk = 26; fakeBpb.heads = 1; fakeBpb.hsectors = 0; fakeBpb.spfat = 6; } else if (imagePlugin.Info.Sectors == 1232 && imagePlugin.Info.SectorSize == 1024) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB."); fakeBpb.bps = 1024; fakeBpb.spc = 1; fakeBpb.rsectors = 1; fakeBpb.fats_no = 2; fakeBpb.root_ent = 192; fakeBpb.sectors = 1232; fakeBpb.media = 0xFE; fakeBpb.sptrk = 8; fakeBpb.heads = 2; fakeBpb.hsectors = 0; fakeBpb.spfat = 2; } else if (imagePlugin.Info.Sectors == 616 && imagePlugin.Info.SectorSize == 1024) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB."); fakeBpb.bps = 1024; fakeBpb.spc = 1; fakeBpb.rsectors = 1; fakeBpb.fats_no = 2; fakeBpb.root_ent = 6192; fakeBpb.sectors = 616; fakeBpb.media = 0xFE; fakeBpb.sptrk = 8; fakeBpb.heads = 2; fakeBpb.hsectors = 0; } else if (imagePlugin.Info.Sectors == 720 && imagePlugin.Info.SectorSize == 128) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB."); fakeBpb.bps = 128; fakeBpb.spc = 2; fakeBpb.rsectors = 54; fakeBpb.fats_no = 2; fakeBpb.root_ent = 64; fakeBpb.sectors = 720; fakeBpb.media = 0xFE; fakeBpb.sptrk = 18; fakeBpb.heads = 1; fakeBpb.hsectors = 0; fakeBpb.spfat = 4; } else if (imagePlugin.Info.Sectors == 640 && imagePlugin.Info.SectorSize == 512) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB for 5.25\" DSDD."); fakeBpb.bps = 512; fakeBpb.spc = 2; fakeBpb.rsectors = 1; fakeBpb.fats_no = 2; fakeBpb.root_ent = 112; fakeBpb.sectors = 640; fakeBpb.media = 0xFF; fakeBpb.sptrk = 8; fakeBpb.heads = 2; fakeBpb.hsectors = 0; fakeBpb.spfat = 1; } break; case 0xFF: if (imagePlugin.Info.Sectors == 640 && imagePlugin.Info.SectorSize == 512) { AaruConsole.DebugWriteLine("FAT plugin", "Using hardcoded BPB for 5.25\" DSDD."); fakeBpb.bps = 512; fakeBpb.spc = 2; fakeBpb.rsectors = 1; fakeBpb.fats_no = 2; fakeBpb.root_ent = 112; fakeBpb.sectors = 640; fakeBpb.media = 0xFF; fakeBpb.sptrk = 8; fakeBpb.heads = 2; fakeBpb.hsectors = 0; fakeBpb.spfat = 1; } break; } // This assumes a bootable sector will jump somewhere or disable interrupts in x86 code bootable |= bpbSector[0] == 0xFA || (bpbSector[0] == 0xEB && bpbSector[1] <= 0x7F) || (bpbSector[0] == 0xE9 && BitConverter.ToUInt16(bpbSector, 1) <= 0x1FC); fakeBpb.boot_code = bpbSector; return(BpbKind.Hardcoded); } if (useExtendedBpb) { fakeBpb = ebpb; return(BpbKind.Extended); } if (userShortExtendedBpb) { fakeBpb.jump = shortEbpb.jump; fakeBpb.oem_name = shortEbpb.oem_name; fakeBpb.bps = shortEbpb.bps; fakeBpb.spc = shortEbpb.spc; fakeBpb.rsectors = shortEbpb.rsectors; fakeBpb.fats_no = shortEbpb.fats_no; fakeBpb.root_ent = shortEbpb.root_ent; fakeBpb.sectors = shortEbpb.sectors; fakeBpb.media = shortEbpb.media; fakeBpb.spfat = shortEbpb.spfat; fakeBpb.sptrk = shortEbpb.sptrk; fakeBpb.heads = shortEbpb.heads; fakeBpb.hsectors = shortEbpb.hsectors; fakeBpb.big_sectors = shortEbpb.big_sectors; fakeBpb.drive_no = shortEbpb.drive_no; fakeBpb.flags = shortEbpb.flags; fakeBpb.signature = shortEbpb.signature; fakeBpb.serial_no = shortEbpb.serial_no; fakeBpb.boot_code = shortEbpb.boot_code; fakeBpb.boot_signature = shortEbpb.boot_signature; return(BpbKind.ShortExtended); } if (useDos33Bpb) { fakeBpb.jump = dos33Bpb.jump; fakeBpb.oem_name = dos33Bpb.oem_name; fakeBpb.bps = dos33Bpb.bps; fakeBpb.spc = dos33Bpb.spc; fakeBpb.rsectors = dos33Bpb.rsectors; fakeBpb.fats_no = dos33Bpb.fats_no; fakeBpb.root_ent = dos33Bpb.root_ent; fakeBpb.sectors = dos33Bpb.sectors; fakeBpb.media = dos33Bpb.media; fakeBpb.spfat = dos33Bpb.spfat; fakeBpb.sptrk = dos33Bpb.sptrk; fakeBpb.heads = dos33Bpb.heads; fakeBpb.hsectors = dos33Bpb.hsectors; fakeBpb.big_sectors = dos33Bpb.big_sectors; fakeBpb.boot_code = dos33Bpb.boot_code; fakeBpb.boot_signature = dos33Bpb.boot_signature; return(BpbKind.Dos33); } if (useDos32Bpb) { fakeBpb.jump = dos32Bpb.jump; fakeBpb.oem_name = dos32Bpb.oem_name; fakeBpb.bps = dos32Bpb.bps; fakeBpb.spc = dos32Bpb.spc; fakeBpb.rsectors = dos32Bpb.rsectors; fakeBpb.fats_no = dos32Bpb.fats_no; fakeBpb.root_ent = dos32Bpb.root_ent; fakeBpb.sectors = dos32Bpb.sectors; fakeBpb.media = dos32Bpb.media; fakeBpb.spfat = dos32Bpb.spfat; fakeBpb.sptrk = dos32Bpb.sptrk; fakeBpb.heads = dos32Bpb.heads; fakeBpb.hsectors = dos32Bpb.hsectors; fakeBpb.boot_code = dos32Bpb.boot_code; fakeBpb.boot_signature = dos32Bpb.boot_signature; return(BpbKind.Dos32); } if (useDos3Bpb) { fakeBpb.jump = dos30Bpb.jump; fakeBpb.oem_name = dos30Bpb.oem_name; fakeBpb.bps = dos30Bpb.bps; fakeBpb.spc = dos30Bpb.spc; fakeBpb.rsectors = dos30Bpb.rsectors; fakeBpb.fats_no = dos30Bpb.fats_no; fakeBpb.root_ent = dos30Bpb.root_ent; fakeBpb.sectors = dos30Bpb.sectors; fakeBpb.media = dos30Bpb.media; fakeBpb.spfat = dos30Bpb.spfat; fakeBpb.sptrk = dos30Bpb.sptrk; fakeBpb.heads = dos30Bpb.heads; fakeBpb.hsectors = dos30Bpb.hsectors; fakeBpb.boot_code = dos30Bpb.boot_code; fakeBpb.boot_signature = dos30Bpb.boot_signature; return(BpbKind.Dos3); } if (useDos2Bpb) { fakeBpb.jump = dos2Bpb.jump; fakeBpb.oem_name = dos2Bpb.oem_name; fakeBpb.bps = dos2Bpb.bps; fakeBpb.spc = dos2Bpb.spc; fakeBpb.rsectors = dos2Bpb.rsectors; fakeBpb.fats_no = dos2Bpb.fats_no; fakeBpb.root_ent = dos2Bpb.root_ent; fakeBpb.sectors = dos2Bpb.sectors; fakeBpb.media = dos2Bpb.media; fakeBpb.spfat = dos2Bpb.spfat; fakeBpb.boot_code = dos2Bpb.boot_code; fakeBpb.boot_signature = dos2Bpb.boot_signature; return(BpbKind.Dos2); } if (useMsxBpb) { fakeBpb.jump = msxBpb.jump; fakeBpb.oem_name = msxBpb.oem_name; fakeBpb.bps = msxBpb.bps; fakeBpb.spc = msxBpb.spc; fakeBpb.rsectors = msxBpb.rsectors; fakeBpb.fats_no = msxBpb.fats_no; fakeBpb.root_ent = msxBpb.root_ent; fakeBpb.sectors = msxBpb.sectors; fakeBpb.media = msxBpb.media; fakeBpb.spfat = msxBpb.spfat; fakeBpb.sptrk = msxBpb.sptrk; fakeBpb.heads = msxBpb.heads; fakeBpb.hsectors = msxBpb.hsectors; fakeBpb.boot_code = msxBpb.boot_code; fakeBpb.boot_signature = msxBpb.boot_signature; fakeBpb.serial_no = msxBpb.serial_no; // TODO: Is there any way to check this? bootable = true; return(BpbKind.Msx); } if (useAtariBpb) { fakeBpb.jump = atariBpb.jump; fakeBpb.oem_name = atariBpb.oem_name; fakeBpb.bps = atariBpb.bps; fakeBpb.spc = atariBpb.spc; fakeBpb.rsectors = atariBpb.rsectors; fakeBpb.fats_no = atariBpb.fats_no; fakeBpb.root_ent = atariBpb.root_ent; fakeBpb.sectors = atariBpb.sectors; fakeBpb.media = atariBpb.media; fakeBpb.spfat = atariBpb.spfat; fakeBpb.sptrk = atariBpb.sptrk; fakeBpb.heads = atariBpb.heads; fakeBpb.boot_code = atariBpb.boot_code; return(BpbKind.Atari); } if (useApricotBpb) { fakeBpb.bps = apricotBpb.mainBPB.bps; fakeBpb.spc = apricotBpb.mainBPB.spc; fakeBpb.rsectors = apricotBpb.mainBPB.rsectors; fakeBpb.fats_no = apricotBpb.mainBPB.fats_no; fakeBpb.root_ent = apricotBpb.mainBPB.root_ent; fakeBpb.sectors = apricotBpb.mainBPB.sectors; fakeBpb.media = apricotBpb.mainBPB.media; fakeBpb.spfat = apricotBpb.mainBPB.spfat; fakeBpb.sptrk = apricotBpb.spt; bootable = apricotBpb.bootType > 0; if (apricotBpb.bootLocation > 0 && apricotBpb.bootLocation + apricotBpb.bootSize < imagePlugin.Info.Sectors) { fakeBpb.boot_code = imagePlugin.ReadSectors(apricotBpb.bootLocation, (uint)(apricotBpb.sectorSize * apricotBpb.bootSize) / imagePlugin.Info.SectorSize); } return(BpbKind.Apricot); } return(BpbKind.None); }
/// <summary> /// Mounts an Apple Lisa filesystem /// </summary> public Errno Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding, Dictionary <string, string> options, string @namespace) { XmlFsType = new FileSystemType(); if (options == null) { options = GetDefaultOptions(); } if (options.TryGetValue("debug", out string debugString)) { bool.TryParse(debugString, out debug); } // Default namespace if (@namespace is null) { @namespace = "ecs"; } switch (@namespace.ToLowerInvariant()) { case "dos": this.@namespace = Namespace.Dos; break; 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; case "human": this.@namespace = Namespace.Human; break; default: return(Errno.InvalidArgument); } DicConsole.DebugWriteLine("FAT plugin", "Reading BPB"); uint sectorsPerBpb = imagePlugin.Info.SectorSize < 512 ? 512 / imagePlugin.Info.SectorSize : 1; byte[] bpbSector = imagePlugin.ReadSectors(0 + partition.Start, sectorsPerBpb); 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; XmlFsType.Bootable = bootable; statfs = new FileSystemInfo { Blocks = XmlFsType.Clusters, 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; 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.XmlMediaType == XmlMediaType.OpticalDisc) { fat32Bpb.bps *= 4; fat32Bpb.spc /= 4; fat32Bpb.big_spfat /= 4; fat32Bpb.hsectors /= 4; fat32Bpb.sptrk /= 4; } XmlFsType.Type = fat32Bpb.version != 0 ? "FAT+" : "FAT32"; if (fat32Bpb.oem_name != null && (fat32Bpb.oem_name[5] != 0x49 || fat32Bpb.oem_name[6] != 0x48 || fat32Bpb.oem_name[7] != 0x43)) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fat32Bpb.oem_name); } sectorsPerCluster = fat32Bpb.spc; XmlFsType.ClusterSize = (uint)(fat32Bpb.bps * fat32Bpb.spc); reservedSectors = fat32Bpb.rsectors; if (fat32Bpb.big_sectors == 0 && fat32Bpb.signature == 0x28) { XmlFsType.Clusters = shortFat32Bpb.huge_sectors / shortFat32Bpb.spc; } else { XmlFsType.Clusters = fat32Bpb.big_sectors / fat32Bpb.spc; } sectorsPerFat = fat32Bpb.big_spfat; XmlFsType.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) { XmlFsType.Dirty = true; } } if ((fat32Bpb.mirror_flags & 0x80) == 0x80) { useFirstFat = (fat32Bpb.mirror_flags & 0xF) != 1; } if (fat32Bpb.signature == 0x29) { XmlFsType.VolumeName = Encoding.ASCII.GetString(fat32Bpb.volume_label); } // 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"...... XmlFsType.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) { byte[] fsinfoSector = imagePlugin.ReadSector(fat32Bpb.fsinfo_sector + partition.Start); FsInfoSector fsInfo = Marshal.ByteArrayToStructureLittleEndian <FsInfoSector>(fsinfoSector); if (fsInfo.signature1 == FSINFO_SIGNATURE1 && fsInfo.signature2 == FSINFO_SIGNATURE2 && fsInfo.signature3 == FSINFO_SIGNATURE3) { if (fsInfo.free_clusters < 0xFFFFFFFF) { XmlFsType.FreeClusters = fsInfo.free_clusters; XmlFsType.FreeClustersSpecified = true; } } } 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) { XmlFsType.Bootable = true; } break; } case BpbKind.Human: // If not debug set Human68k namespace and ShiftJIS codepage as defaults if (!debug) { this.@namespace = Namespace.Human; encoding = Encoding.GetEncoding("shift_jis"); } XmlFsType.Bootable = true; break; } Encoding = encoding ?? (bpbKind == BpbKind.Human ? Encoding.GetEncoding("shift_jis") : Encoding.GetEncoding("IBM437")); ulong firstRootSector = 0; if (!fat32) { // This is to support FAT partitions on hybrid ISO/USB images if (imagePlugin.Info.XmlMediaType == XmlMediaType.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; } } // This assumes no sane implementation will violate cluster size rules // However nothing prevents this to happen // If first file on disk uses only one cluster there is absolutely no way to differentiate between FAT12 and FAT16, // so let's hope implementations use common sense? if (!fat12 && !fat16) { ulong clusters; if (fakeBpb.sectors == 0) { clusters = fakeBpb.spc == 0 ? fakeBpb.big_sectors : fakeBpb.big_sectors / fakeBpb.spc; } else { clusters = fakeBpb.spc == 0 ? fakeBpb.sectors : (ulong)fakeBpb.sectors / fakeBpb.spc; } if (clusters < 4089) { fat12 = true; } else { fat16 = true; } } if (fat12) { XmlFsType.Type = "FAT12"; } else if (fat16) { XmlFsType.Type = "FAT16"; } if (bpbKind == BpbKind.Atari) { if (atariBpb.serial_no[0] != 0x49 || atariBpb.serial_no[1] != 0x48 || atariBpb.serial_no[2] != 0x43) { 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]) }; } XmlFsType.SystemIdentifier = StringHandlers.CToString(atariBpb.oem_name); if (string.IsNullOrEmpty(XmlFsType.SystemIdentifier)) { XmlFsType.SystemIdentifier = null; } } else if (fakeBpb.oem_name != null) { if (fakeBpb.oem_name[5] != 0x49 || fakeBpb.oem_name[6] != 0x48 || fakeBpb.oem_name[7] != 0x43) { // 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 if (fakeBpb.oem_name[0] >= 0x20 && fakeBpb.oem_name[0] <= 0x7F && 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) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name); } else if (fakeBpb.oem_name[0] < 0x20 && 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) { XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name, Encoding, start: 1); } } if (fakeBpb.signature == 0x28 || fakeBpb.signature == 0x29) { XmlFsType.VolumeSerial = $"{fakeBpb.serial_no:X8}"; statfs.Id = new FileSystemId { IsInt = true, Serial32 = fakeBpb.serial_no }; } } if (bpbKind != BpbKind.Human) { if (fakeBpb.sectors == 0) { XmlFsType.Clusters = fakeBpb.spc == 0 ? fakeBpb.big_sectors : fakeBpb.big_sectors / fakeBpb.spc; } else { XmlFsType.Clusters = (ulong)(fakeBpb.spc == 0 ? fakeBpb.sectors : fakeBpb.sectors / fakeBpb.spc); } } else { XmlFsType.Clusters = humanBpb.clusters == 0 ? humanBpb.big_clusters : humanBpb.clusters; } sectorsPerCluster = fakeBpb.spc; XmlFsType.ClusterSize = (uint)(fakeBpb.bps * fakeBpb.spc); reservedSectors = fakeBpb.rsectors; sectorsPerFat = fakeBpb.spfat; if (fakeBpb.signature == 0x28 || fakeBpb.signature == 0x29 || andosOemCorrect) { if ((fakeBpb.flags & 0xF8) == 0x00) { if ((fakeBpb.flags & 0x01) == 0x01) { XmlFsType.Dirty = true; } } if (fakeBpb.signature == 0x29 || andosOemCorrect) { XmlFsType.VolumeName = Encoding.ASCII.GetString(fakeBpb.volume_label); } } // Workaround that PCExchange jumps into "FAT16 "... if (XmlFsType.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 (XmlFsType.Bootable == false && fakeBpb.jump != null) { XmlFsType.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 = null; if (!fat32) { firstClusterSector = firstRootSector + sectorsForRootDirectory - sectorsPerCluster * 2; rootDirectory = imagePlugin.ReadSectors(firstRootSector, sectorsForRootDirectory); if (bpbKind == BpbKind.DecRainbow) { MemoryStream rootMs = new MemoryStream(); foreach (byte[] tmp in from ulong rootSector in new[] { 0x17, 0x19, 0x1B, 0x1D, 0x1E, 0x20 } select imagePlugin.ReadSector(rootSector)) { rootMs.Write(tmp, 0, tmp.Length); } rootDirectory = rootMs.ToArray(); } } else { if (rootDirectoryCluster == 0) { return(Errno.InvalidArgument); } MemoryStream rootMs = new MemoryStream(); uint[] rootDirectoryClusters = GetClusters(rootDirectoryCluster); foreach (uint cluster in rootDirectoryClusters) { byte[] buffer = imagePlugin.ReadSectors(firstClusterSector + cluster * sectorsPerCluster, sectorsPerCluster); rootMs.Write(buffer, 0, buffer.Length); } 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; 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 (this.@namespace != Namespace.Lfn && this.@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)) { XmlFsType.VolumeName = entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) && this.@namespace == Namespace.Nt ? volname.ToLower() : volname; } if (entry.ctime > 0 && entry.cdate > 0) { XmlFsType.CreationDate = DateHandlers.DosToDateTime(entry.cdate, entry.ctime); if (entry.ctime_ms > 0) { XmlFsType.CreationDate = XmlFsType.CreationDate.AddMilliseconds(entry.ctime_ms * 10); } XmlFsType.CreationDateSpecified = true; } if (entry.mtime > 0 && entry.mdate > 0) { XmlFsType.ModificationDate = DateHandlers.DosToDateTime(entry.mdate, entry.mtime); XmlFsType.ModificationDateSpecified = true; } 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); 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 (this.@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; } completeEntry.Shortname = filename; if (this.@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; } XmlFsType.VolumeName = XmlFsType.VolumeName?.Trim(); 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")}"; break; 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")}"; break; case BpbKind.ShortFat32: case BpbKind.LongFat32: statfs.Type = XmlFsType.Type == "FAT+" ? "FAT+" : "Microsoft FAT32"; break; case BpbKind.Andos: statfs.Type = $"ANDOS FAT{(fat16 ? "16" : "12")}"; break; case BpbKind.Apricot: statfs.Type = $"Apricot FAT{(fat16 ? "16" : "12")}"; break; case BpbKind.DecRainbow: statfs.Type = $"DEC FAT{(fat16 ? "16" : "12")}"; break; case BpbKind.Human: statfs.Type = $"Human68k FAT{(fat16 ? "16" : "12")}"; break; default: throw new ArgumentOutOfRangeException(); } bytesPerCluster = sectorsPerCluster * imagePlugin.Info.SectorSize; if (fat12) { byte[] fatBytes = imagePlugin.ReadSectors(fatFirstSector + (useFirstFat ? 0 : sectorsPerFat), sectorsPerFat); fatEntries = new ushort[statfs.Blocks]; int pos = 0; for (int i = 0; i + 3 < fatBytes.Length && pos < fatEntries.Length; i += 3) { fatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF) << 8) + fatBytes[i + 0]); fatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF0) >> 4) + (fatBytes[i + 2] << 4)); } } else if (fat16) { DicConsole.DebugWriteLine("FAT plugin", "Reading FAT16"); byte[] fatBytes = imagePlugin.ReadSectors(fatFirstSector + (useFirstFat ? 0 : sectorsPerFat), sectorsPerFat); DicConsole.DebugWriteLine("FAT plugin", "Casting FAT"); fatEntries = MemoryMarshal.Cast <byte, ushort>(fatBytes).ToArray(); } // 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 && (this.@namespace == Namespace.Os2 || this.@namespace == Namespace.Ecs)) { 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))