// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : Info.cs // Author(s) : Natalia Portillo // // 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 . // // ---------------------------------------------------------------------------- // Copyright © 2011-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using Aaru.Checksums; using Aaru.CommonTypes.AaruMetadata; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.Helpers; using Aaru.Logging; using Marshal = Aaru.Helpers.Marshal; using Partition = Aaru.CommonTypes.Partition; namespace Aaru.Filesystems; public sealed partial class FAT { #region IReadOnlyFilesystem Members /// [SuppressMessage("ReSharper", "JoinDeclarationAndInitializer")] public bool Identify(IMediaImage imagePlugin, Partition partition) { if(2 + partition.Start >= partition.End) return false; ushort bps; byte spc; byte numberOfFats; ushort reservedSecs; ushort rootEntries; ushort sectors; byte mediaDescriptor; ushort fatSectors; uint bigSectors; byte bpbSignature; byte fat32Signature; ulong hugeSectors; byte[] fat32Id = new byte[8]; byte[] msxId = new byte[6]; byte fatId; byte[] dosOem = new byte[8]; byte[] atariOem = new byte[6]; ushort bootable = 0; 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 false; errno = imagePlugin.ReadSector(sectorsPerBpb + partition.Start, out byte[] fatSector); if(errno != ErrorNumber.NoError) return false; HumanParameterBlock humanBpb = Marshal.ByteArrayToStructureBigEndian(bpbSector); ulong expectedClusters = humanBpb.bpc > 0 ? partition.Size / humanBpb.bpc : 0; AaruLogging.Debug(MODULE_NAME, "Human bpc = {0}", humanBpb.bpc); AaruLogging.Debug(MODULE_NAME, "Human clusters = {0}", humanBpb.clusters); AaruLogging.Debug(MODULE_NAME, "Human big_clusters = {0}", humanBpb.big_clusters); AaruLogging.Debug(MODULE_NAME, "Human expected clusters = {0}", expectedClusters); // 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] >= 0x20 && bpbSector[1] < 0xFE; AaruLogging.Debug(MODULE_NAME, "humanClustersCorrect = {0}", humanClustersCorrect); AaruLogging.Debug(MODULE_NAME, "humanOemCorrect = {0}", humanOemCorrect); AaruLogging.Debug(MODULE_NAME, "humanBranchCorrect = {0}", humanBranchCorrect); // If all Human68k checks are correct, it is a Human68k FAT16 if(humanClustersCorrect && humanOemCorrect && humanBranchCorrect && expectedClusters > 0) return true; Array.Copy(bpbSector, 0x02, atariOem, 0, 6); Array.Copy(bpbSector, 0x03, dosOem, 0, 8); bps = BitConverter.ToUInt16(bpbSector, 0x00B); spc = bpbSector[0x00D]; reservedSecs = BitConverter.ToUInt16(bpbSector, 0x00E); numberOfFats = bpbSector[0x010]; rootEntries = BitConverter.ToUInt16(bpbSector, 0x011); sectors = BitConverter.ToUInt16(bpbSector, 0x013); mediaDescriptor = bpbSector[0x015]; fatSectors = BitConverter.ToUInt16(bpbSector, 0x016); Array.Copy(bpbSector, 0x052, msxId, 0, 6); bigSectors = BitConverter.ToUInt32(bpbSector, 0x020); bpbSignature = bpbSector[0x026]; fat32Signature = bpbSector[0x042]; Array.Copy(bpbSector, 0x052, fat32Id, 0, 8); hugeSectors = BitConverter.ToUInt64(bpbSector, 0x052); fatId = fatSector[0]; int bitsInBps = CountBits.Count(bps); if(imagePlugin.Info.SectorSize >= 512) bootable = BitConverter.ToUInt16(bpbSector, 0x1FE); bool correctSpc = spc is 1 or 2 or 4 or 8 or 16 or 32 or 64; string msxString = Encoding.ASCII.GetString(msxId); string fat32String = Encoding.ASCII.GetString(fat32Id); bool atariOemCorrect = atariOem[0] >= 0x20 && atariOem[1] >= 0x20 && atariOem[2] >= 0x20 && atariOem[3] >= 0x20 && atariOem[4] >= 0x20 && atariOem[5] >= 0x20; bool dosOemCorrect = dosOem[0] >= 0x20 && dosOem[1] >= 0x20 && dosOem[2] >= 0x20 && dosOem[3] >= 0x20 && dosOem[4] >= 0x20 && dosOem[5] >= 0x20 && dosOem[6] >= 0x20 && dosOem[7] >= 0x20; string oemString = Encoding.ASCII.GetString(dosOem); AaruLogging.Debug(MODULE_NAME, "atari_oem_correct = {0}", atariOemCorrect); AaruLogging.Debug(MODULE_NAME, "dos_oem_correct = {0}", dosOemCorrect); AaruLogging.Debug(MODULE_NAME, "bps = {0}", bps); AaruLogging.Debug(MODULE_NAME, "bits in bps = {0}", bitsInBps); AaruLogging.Debug(MODULE_NAME, "spc = {0}", spc); AaruLogging.Debug(MODULE_NAME, "correct_spc = {0}", correctSpc); AaruLogging.Debug(MODULE_NAME, "reserved_secs = {0}", reservedSecs); AaruLogging.Debug(MODULE_NAME, "fats_no = {0}", numberOfFats); AaruLogging.Debug(MODULE_NAME, "root_entries = {0}", rootEntries); AaruLogging.Debug(MODULE_NAME, "sectors = {0}", sectors); AaruLogging.Debug(MODULE_NAME, "media_descriptor = 0x{0:X2}", mediaDescriptor); AaruLogging.Debug(MODULE_NAME, "fat_sectors = {0}", fatSectors); AaruLogging.Debug(MODULE_NAME, "msx_id = \"{0}\"", msxString); AaruLogging.Debug(MODULE_NAME, "big_sectors = {0}", bigSectors); AaruLogging.Debug(MODULE_NAME, "bpb_signature = 0x{0:X2}", bpbSignature); AaruLogging.Debug(MODULE_NAME, "fat32_signature = 0x{0:X2}", fat32Signature); AaruLogging.Debug(MODULE_NAME, "fat32_id = \"{0}\"", fat32String); AaruLogging.Debug(MODULE_NAME, "huge_sectors = {0}", hugeSectors); AaruLogging.Debug(MODULE_NAME, "fat_id = 0x{0:X2}", fatId); ushort apricotBps = BitConverter.ToUInt16(bpbSector, 0x50); byte apricotSpc = bpbSector[0x52]; ushort apricotReservedSecs = BitConverter.ToUInt16(bpbSector, 0x53); byte apricotFatsNo = bpbSector[0x55]; ushort apricotRootEntries = BitConverter.ToUInt16(bpbSector, 0x56); ushort apricotSectors = BitConverter.ToUInt16(bpbSector, 0x58); byte apricotMediaDescriptor = bpbSector[0x5A]; ushort apricotFatSectors = BitConverter.ToUInt16(bpbSector, 0x5B); bool apricotCorrectSpc = apricotSpc is 1 or 2 or 4 or 8 or 16 or 32 or 64; int bitsInApricotBps = CountBits.Count(apricotBps); byte apricotPartitions = bpbSector[0x0C]; AaruLogging.Debug(MODULE_NAME, "apricot_bps = {0}", apricotBps); AaruLogging.Debug(MODULE_NAME, "apricot_spc = {0}", apricotSpc); AaruLogging.Debug(MODULE_NAME, "apricot_correct_spc = {0}", apricotCorrectSpc); AaruLogging.Debug(MODULE_NAME, "apricot_reserved_secs = {0}", apricotReservedSecs); AaruLogging.Debug(MODULE_NAME, "apricot_fats_no = {0}", apricotFatsNo); AaruLogging.Debug(MODULE_NAME, "apricot_root_entries = {0}", apricotRootEntries); AaruLogging.Debug(MODULE_NAME, "apricot_sectors = {0}", apricotSectors); AaruLogging.Debug(MODULE_NAME, "apricot_media_descriptor = 0x{0:X2}", apricotMediaDescriptor); AaruLogging.Debug(MODULE_NAME, "apricot_fat_sectors = {0}", apricotFatSectors); // This is to support FAT partitions on hybrid ISO/USB images if(imagePlugin.Info.MetadataMediaType == MetadataMediaType.OpticalDisc) { sectors /= 4; bigSectors /= 4; hugeSectors /= 4; } switch(oemString) { // exFAT case "EXFAT ": return false; // NTFS case "NTFS " when bootable == 0xAA55 && numberOfFats == 0 && fatSectors == 0: return false; // QNX4 case "FQNX4FS ": return false; } // HPFS if(16 + partition.Start <= partition.End) { errno = imagePlugin.ReadSector(16 + partition.Start, out byte[] hpfsSbSector); // Seek to superblock, on logical sector 16 if(errno != ErrorNumber.NoError) return false; uint hpfsMagic1 = BitConverter.ToUInt32(hpfsSbSector, 0x000); uint hpfsMagic2 = BitConverter.ToUInt32(hpfsSbSector, 0x004); if(hpfsMagic1 == 0xF995E849 && hpfsMagic2 == 0xFA53E9C5) return false; } switch(bitsInBps) { // FAT32 for sure case 1 when correctSpc && numberOfFats <= 2 && fatSectors == 0 && fat32Signature == 0x29 && fat32String == "FAT32 ": return true; // short FAT32 case 1 when correctSpc && numberOfFats <= 2 && fatSectors == 0 && fat32Signature == 0x28: return sectors == 0 ? bigSectors == 0 ? hugeSectors <= partition.End - partition.Start + 1 : bigSectors <= partition.End - partition.Start + 1 : sectors <= partition.End - partition.Start + 1; // MSX-DOS FAT12 case 1 when correctSpc && numberOfFats <= 2 && rootEntries > 0 && sectors <= partition.End - partition.Start + 1 && fatSectors > 0 && msxString == "VOL_ID": return true; // EBPB case 1 when correctSpc && numberOfFats <= 2 && rootEntries > 0 && fatSectors > 0 && bpbSignature is 0x28 or 0x29: return sectors == 0 ? bigSectors <= partition.End - partition.Start + 1 : sectors <= partition.End - partition.Start + 1; // BPB case 1 when correctSpc && reservedSecs < partition.End - partition.Start && numberOfFats <= 2 && rootEntries > 0 && fatSectors > 0: return sectors == 0 ? bigSectors <= partition.End - partition.Start + 1 : sectors <= partition.End - partition.Start + 1; } // Apricot BPB if(bitsInApricotBps == 1 && apricotCorrectSpc && apricotReservedSecs < partition.End - partition.Start && apricotFatsNo <= 2 && apricotRootEntries > 0 && apricotFatSectors > 0 && apricotSectors <= partition.End - partition.Start + 1 && apricotPartitions == 0) return true; // All FAT12 without BPB can only be used on floppies, without partitions. if(partition.Start != 0) return false; // DEC Rainbow, lacks a BPB but has a very concrete structure... if(imagePlugin.Info is { Sectors: 800, SectorSize: 512 }) { // 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 errno = imagePlugin.ReadSector(0x14, out byte[] fat1Sector0); if(errno != ErrorNumber.NoError) return false; // First FAT2 sector resides at LBA 0x1A errno = imagePlugin.ReadSector(0x1A, out byte[] fat2Sector0); if(errno != ErrorNumber.NoError) return false; bool equalFatIds = fat1Sector0[0] == fat2Sector0[0] && fat1Sector0[1] == fat2Sector0[1]; // Volume is software interleaved 2:1 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 false; 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) continue; validRootDir = false; break; } if(!validRootDir) break; } if(z80Di == 0xF3 && equalFatIds && (fat1Sector0[0] & 0xF0) == 0xF0 && fat1Sector0[1] == 0xFF && validRootDir) return true; } byte fat2 = fatSector[1]; byte fat3 = fatSector[2]; ushort fatCluster2 = (ushort)((fat2 << 8) + fat3 & 0xFFF); AaruLogging.Debug(MODULE_NAME, "1st fat cluster 1 = {0:X3}", fatCluster2); if(fatCluster2 < 0xFF0) return false; ulong fat2SectorNo = 0; switch(fatId) { case 0xE5: if(imagePlugin.Info is { Sectors: 2002, SectorSize: 128 }) fat2SectorNo = 2; break; case 0xFD: switch(imagePlugin.Info.Sectors) { case 4004 when imagePlugin.Info.SectorSize == 128: case 2002 when imagePlugin.Info.SectorSize == 128: fat2SectorNo = 7; break; } break; case 0xFE: fat2SectorNo = imagePlugin.Info.Sectors switch { 320 when imagePlugin.Info.SectorSize == 512 => 2, 2002 when imagePlugin.Info.SectorSize == 128 => 7, 1232 when imagePlugin.Info.SectorSize == 1024 => 3, 616 when imagePlugin.Info.SectorSize == 1024 => 2, 720 when imagePlugin.Info.SectorSize == 128 => 5, 640 when imagePlugin.Info.SectorSize == 512 => 2, _ => fat2SectorNo }; break; case 0xFF: if(imagePlugin.Info is { Sectors: 640, SectorSize: 512 }) fat2SectorNo = 2; break; default: if(fatId < 0xE8) return false; fat2SectorNo = 2; break; } if(fat2SectorNo > partition.End || fat2SectorNo == 0) return false; AaruLogging.Debug(MODULE_NAME, Localization.Second_fat_starts_at_0, fat2SectorNo); errno = imagePlugin.ReadSector(fat2SectorNo, out byte[] fat2Sector); if(errno != ErrorNumber.NoError) return false; fat2 = fat2Sector[1]; fat3 = fat2Sector[2]; fatCluster2 = (ushort)((fat2 << 8) + fat3 & 0xFFF); if(fatCluster2 < 0xFF0) return false; return fatId == fat2Sector[0]; } /// public void GetInformation(IMediaImage imagePlugin, Partition partition, Encoding encoding, out string information, out FileSystem metadata) { encoding ??= Encoding.GetEncoding("IBM437"); information = ""; var sb = new StringBuilder(); metadata = new FileSystem(); 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; 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; metadata.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(bpbSector); Fat32ParameterBlockShort shortFat32Bpb = Marshal.ByteArrayToStructureLittleEndian(bpbSector); // 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; } if(fat32Bpb.version != 0) { sb.AppendLine(Localization.FAT_Plus); metadata.Type = FS_TYPE_FAT_PLUS; } else { sb.AppendLine(Localization.Microsoft_FAT32); metadata.Type = FS_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(Localization.Volume_has_been_modified_by_Windows_9x_Me_Volume_Tracker); else metadata.SystemIdentifier = StringHandlers.CToString(fat32Bpb.oem_name); } if(!string.IsNullOrEmpty(metadata.SystemIdentifier)) sb.AppendFormat(Localization.OEM_name_0, metadata.SystemIdentifier.Trim()).AppendLine(); sb.AppendFormat(Localization._0_bytes_per_sector, fat32Bpb.bps).AppendLine(); sb.AppendFormat(Localization._0_sectors_per_cluster, fat32Bpb.spc).AppendLine(); metadata.ClusterSize = (uint)(fat32Bpb.bps * fat32Bpb.spc); sb.AppendFormat(Localization._0_sectors_reserved_between_BPB_and_FAT, fat32Bpb.rsectors).AppendLine(); if(fat32Bpb is { big_sectors: 0, signature: 0x28 }) { sb.AppendFormat(Localization._0_sectors_on_volume_1_bytes, shortFat32Bpb.huge_sectors, shortFat32Bpb.huge_sectors * shortFat32Bpb.bps) .AppendLine(); metadata.Clusters = shortFat32Bpb.huge_sectors / shortFat32Bpb.spc; } else if(fat32Bpb.sectors == 0) { sb.AppendFormat(Localization._0_sectors_on_volume_1_bytes, fat32Bpb.big_sectors, fat32Bpb.big_sectors * fat32Bpb.bps) .AppendLine(); metadata.Clusters = fat32Bpb.big_sectors / fat32Bpb.spc; } else { sb.AppendFormat(Localization._0_sectors_on_volume_1_bytes, fat32Bpb.sectors, fat32Bpb.sectors * fat32Bpb.bps) .AppendLine(); metadata.Clusters = (ulong)(fat32Bpb.sectors / fat32Bpb.spc); } sb.AppendFormat(Localization._0_clusters_on_volume, metadata.Clusters).AppendLine(); sb.AppendFormat(Localization.Media_descriptor_0, fat32Bpb.media).AppendLine(); sb.AppendFormat(Localization._0_sectors_per_FAT, fat32Bpb.big_spfat).AppendLine(); sb.AppendFormat(Localization._0_sectors_per_track, fat32Bpb.sptrk).AppendLine(); sb.AppendFormat(Localization._0_heads, fat32Bpb.heads).AppendLine(); sb.AppendFormat(Localization._0_hidden_sectors_before_BPB, fat32Bpb.hsectors).AppendLine(); sb.AppendFormat(Localization.Cluster_of_root_directory_0, fat32Bpb.root_cluster).AppendLine(); sb.AppendFormat(Localization.Sector_of_FSINFO_structure_0, fat32Bpb.fsinfo_sector).AppendLine(); sb.AppendFormat(Localization.Sector_of_backup_FAT32_parameter_block_0, fat32Bpb.backup_sector) .AppendLine(); sb.AppendFormat(Localization.Drive_number_0, fat32Bpb.drive_no).AppendLine(); sb.AppendFormat(Localization.Volume_Serial_Number_0, fat32Bpb.serial_no).AppendLine(); metadata.VolumeSerial = $"{fat32Bpb.serial_no:X8}"; if((fat32Bpb.flags & 0xF8) == 0x00) { if((fat32Bpb.flags & 0x01) == 0x01) { sb.AppendLine(Localization.Volume_should_be_checked_on_next_mount); metadata.Dirty = true; } if((fat32Bpb.flags & 0x02) == 0x02) sb.AppendLine(Localization.Disk_surface_should_be_checked_on_next_mount); } if((fat32Bpb.mirror_flags & 0x80) == 0x80) { sb.AppendFormat(Localization.FATs_are_out_of_sync_FAT_0_is_in_use, fat32Bpb.mirror_flags & 0xF) .AppendLine(); } else sb.AppendLine(Localization.All_copies_of_FAT_are_the_same); if((fat32Bpb.mirror_flags & 0x6F20) == 0x6F20) sb.AppendLine(Localization.DR_DOS_will_boot_this_FAT32_using_CHS); else if((fat32Bpb.mirror_flags & 0x4F20) == 0x4F20) sb.AppendLine(Localization.DR_DOS_will_boot_this_FAT32_using_LBA); if(fat32Bpb.signature == 0x29) { metadata.VolumeName = StringHandlers.SpacePaddedToString(fat32Bpb.volume_label, encoding); metadata.VolumeName = metadata.VolumeName?.Replace("\0", ""); sb.AppendFormat(Localization.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"...... 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; // 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) { errno = imagePlugin.ReadSector(fat32Bpb.fsinfo_sector + partition.Start, out byte[] fsinfoSector); if(errno != ErrorNumber.NoError) return; FsInfoSector fsInfo = Marshal.ByteArrayToStructureLittleEndian(fsinfoSector); if(fsInfo.signature1 == FSINFO_SIGNATURE1 && fsInfo is { signature2: FSINFO_SIGNATURE2, signature3: FSINFO_SIGNATURE3 }) { if(fsInfo.free_clusters < 0xFFFFFFFF) { sb.AppendFormat(Localization._0_free_clusters, fsInfo.free_clusters).AppendLine(); metadata.FreeClusters = fsInfo.free_clusters; } if(fsInfo.last_cluster is > 2 and < 0xFFFFFFFF) sb.AppendFormat(Localization.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) { metadata.Bootable = true; var atariSb = new StringBuilder(); atariSb.AppendFormat(Localization.cmdload_will_be_loaded_with_value_0, BigEndianBitConverter.ToUInt16(bpbSector, 0x01E)) .AppendLine(); atariSb.AppendFormat(Localization.Boot_program_will_be_loaded_at_address_0, atariBpb.ldaaddr) .AppendLine(); atariSb.AppendFormat(Localization.FAT_and_directory_will_be_cached_at_address_0, 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(Localization.Boot_program_resides_in_file_0, filename).AppendLine(); } else { atariSb.AppendFormat(Localization.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: metadata.Bootable = true; break; } if(!isFat32) { // 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(!isFat12 && !isFat16) { if(clusters < 4089) { // The first 2 FAT entries do not count as allocation clusters in FAT12 and FAT16 ushort[] fat12 = new ushort[clusters + 2]; _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; 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 + 2)) fat12Valid = false; ushort[] fat16 = MemoryMarshal.Cast(fatBytes).ToArray(); bool fat16Valid = fat16[0] >= FAT16_RESERVED && fat16[1] >= 0x3FF0; if(fat16.Any(entry => entry < FAT16_RESERVED && entry > clusters + 2)) fat16Valid = false; isFat12 = fat12Valid; isFat16 = fat16Valid; // Check BPB type if(isFat12 == isFat16) { isFat12 = fakeBpb.fs_type != null && Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT12 "; isFat16 = fakeBpb.fs_type != null && Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT16 "; } if(!isFat12 && !isFat16) isFat12 = true; } else isFat16 = true; } if(isFat12) { switch(bpbKind) { case BpbKind.Atari: sb.AppendLine(Localization.Atari_FAT12); break; case BpbKind.Apricot: sb.AppendLine(Localization.Apricot_FAT12); break; case BpbKind.Human: sb.AppendLine(Localization.Human68k_FAT12); break; default: sb.AppendLine(Localization.Microsoft_FAT12); break; } metadata.Type = FS_TYPE_FAT12; } else if(isFat16) { sb.AppendLine(bpbKind switch { BpbKind.Atari => Localization.Atari_FAT16, BpbKind.Human => Localization.Human68k_FAT16, _ => Localization.Microsoft_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) sb.AppendLine(Localization.Volume_has_been_modified_by_Windows_9x_Me_Volume_Tracker); else { metadata.VolumeSerial = $"{atariBpb.serial_no[0]:X2}{atariBpb.serial_no[1]:X2}{atariBpb.serial_no[2] :X2}"; } 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) sb.AppendLine(Localization.Volume_has_been_modified_by_Windows_9x_Me_Volume_Tracker); else { 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}"; } if(metadata.SystemIdentifier != null) sb.AppendFormat(Localization.OEM_name_0, metadata.SystemIdentifier.Trim()).AppendLine(); sb.AppendFormat(Localization._0_bytes_per_sector, fakeBpb.bps).AppendLine(); if(bpbKind != BpbKind.Human) { if(fakeBpb.sectors == 0) { sb.AppendFormat(Localization._0_sectors_on_volume_1_bytes, fakeBpb.big_sectors, fakeBpb.big_sectors * fakeBpb.bps) .AppendLine(); } else { sb.AppendFormat(Localization._0_sectors_on_volume_1_bytes, fakeBpb.sectors, fakeBpb.sectors * fakeBpb.bps) .AppendLine(); } } else { sb.AppendFormat(Localization._0_sectors_on_volume_1_bytes, clusters * humanBpb.bpc / imagePlugin.Info.SectorSize, clusters * humanBpb.bpc) .AppendLine(); } metadata.Clusters = clusters; sb.AppendFormat(Localization._0_sectors_per_cluster, fakeBpb.spc).AppendLine(); sb.AppendFormat(Localization._0_clusters_on_volume, metadata.Clusters).AppendLine(); metadata.ClusterSize = (uint)(fakeBpb.bps * fakeBpb.spc); sb.AppendFormat(Localization._0_sectors_reserved_between_BPB_and_FAT, fakeBpb.rsectors).AppendLine(); sb.AppendFormat(Localization._0_FATs, fakeBpb.fats_no).AppendLine(); sb.AppendFormat(Localization._0_entries_in_root_directory, fakeBpb.root_ent).AppendLine(); if(fakeBpb.media > 0) sb.AppendFormat(Localization.Media_descriptor_0, fakeBpb.media).AppendLine(); sb.AppendFormat(Localization._0_sectors_per_FAT, fakeBpb.spfat).AppendLine(); if(fakeBpb.sptrk is > 0 and < 64 && fakeBpb.heads is > 0 and < 256) { sb.AppendFormat(Localization._0_sectors_per_track, fakeBpb.sptrk).AppendLine(); sb.AppendFormat(Localization._0_heads, fakeBpb.heads).AppendLine(); } if(fakeBpb.hsectors <= partition.Start) sb.AppendFormat(Localization._0_hidden_sectors_before_BPB, fakeBpb.hsectors).AppendLine(); if(fakeBpb.signature is 0x28 or 0x29 || andosOemCorrect) { sb.AppendFormat(Localization.Drive_number_0, fakeBpb.drive_no).AppendLine(); if(metadata.VolumeSerial != null) sb.AppendFormat(Localization.Volume_Serial_Number_0, metadata.VolumeSerial).AppendLine(); if((fakeBpb.flags & 0xF8) == 0x00) { if((fakeBpb.flags & 0x01) == 0x01) { sb.AppendLine(Localization.Volume_should_be_checked_on_next_mount); metadata.Dirty = true; } if((fakeBpb.flags & 0x02) == 0x02) sb.AppendLine(Localization.Disk_surface_should_be_checked_on_next_mount); } if(fakeBpb.signature == 0x29 || andosOemCorrect) { metadata.VolumeName = StringHandlers.SpacePaddedToString(fakeBpb.volume_label, encoding); metadata.VolumeName = metadata.VolumeName?.Replace("\0", ""); sb.AppendFormat(Localization.Filesystem_type_0, Encoding.ASCII.GetString(fakeBpb.fs_type)) .AppendLine(); } } else if(bpbKind == BpbKind.Atari && metadata.VolumeSerial != null) sb.AppendFormat(Localization.Volume_Serial_Number_0, metadata.VolumeSerial).AppendLine(); bootChk = Sha1Context.Data(fakeBpb.boot_code, out _); // 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 && 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; } 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.MetadataMediaType != MetadataMediaType.OpticalDisc) { errno = imagePlugin.ReadSectors(rootDirectorySector + partition.Start, sectorsForRootDirectory, out byte[] rootDirectory); if(errno != ErrorNumber.NoError) return; 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; 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(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)) metadata.VolumeName = entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) ? volname.ToLower() : volname; 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); sb.AppendFormat(Localization.Volume_created_on_0, metadata.CreationDate).AppendLine(); } if(entry is { mtime: > 0, mdate: > 0 }) { metadata.ModificationDate = DateHandlers.DosToDateTime(entry.mdate, entry.mtime); sb.AppendFormat(Localization.Volume_last_modified_on_0, metadata.ModificationDate).AppendLine(); } if(entry.adate > 0) { sb.AppendFormat(Localization.Volume_last_accessed_on_0_d, DateHandlers.DosToDateTime(entry.adate, 0)) .AppendLine(); } break; } } if(!string.IsNullOrEmpty(metadata.VolumeName)) sb.AppendFormat(Localization.Volume_label_0, metadata.VolumeName).AppendLine(); if(metadata.Bootable) { switch(bpbSector[0]) { // Intel short jump case 0xEB when 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 _); break; } // Intel big jump case 0xE9 when 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 _); break; } } sb.AppendLine(Localization.Volume_is_bootable); sb.AppendFormat(Localization.Boot_code_SHA1_0, bootChk).AppendLine(); string bootName = _knownBootHashes.FirstOrDefault(t => t.hash == bootChk).name; if(string.IsNullOrWhiteSpace(bootName)) sb.AppendLine(Localization.Unknown_boot_code); else sb.AppendFormat(Localization.Boot_code_corresponds_to_0, bootName).AppendLine(); } information = sb.ToString(); } #endregion }