diff --git a/proprietary/DiskImage/Filesystem/Ntfs/NtfsBootSector.cs b/proprietary/DiskImage/Filesystem/Ntfs/NtfsBootSector.cs new file mode 100644 index 000000000..78bd655cc --- /dev/null +++ b/proprietary/DiskImage/Filesystem/Ntfs/NtfsBootSector.cs @@ -0,0 +1,283 @@ +// Copyright (c) 2026 Duplicati Inc. All rights reserved. + +using System; +using System.Buffers.Binary; +using System.Text; + +namespace Duplicati.Proprietary.DiskImage.Filesystem.Ntfs; + +/// +/// Represents a parsed NTFS Boot Sector (BIOS Parameter Block and extended NTFS fields). +/// This is a read-only record struct that extracts geometry information from the boot sector. +/// +public readonly record struct NtfsBootSector +{ + /// + /// The boot sector signature at offset 510. + /// On disk the bytes are 0x55, 0xAA; read as little-endian uint16 this is 0xAA55. + /// + private const ushort BootSectorSignature = 0xAA55; + + /// + /// Offset to the boot sector signature. + /// + private const int SignatureOffset = 510; + + /// + /// Offset to the OEM ID field. + /// + private const int OemIdOffset = 0x03; + + /// + /// Length of the OEM ID field (8 bytes). + /// + private const int OemIdLength = 8; + + /// + /// Expected OEM ID value for NTFS: "NTFS " + /// + private const string ExpectedOemId = "NTFS "; + + /// + /// Offset to BytesPerSector field. + /// + private const int BytesPerSectorOffset = 0x0B; + + /// + /// Offset to SectorsPerCluster field. + /// + private const int SectorsPerClusterOffset = 0x0D; + + /// + /// Offset to TotalSectors field (uint64). + /// + private const int TotalSectorsOffset = 0x28; + + /// + /// Offset to MftClusterNumber field (uint64). + /// + private const int MftClusterNumberOffset = 0x30; + + /// + /// Offset to MftMirrorClusterNumber field (uint64). + /// + private const int MftMirrorClusterNumberOffset = 0x38; + + /// + /// Offset to ClustersPerMftRecord field (int8). + /// + private const int ClustersPerMftRecordOffset = 0x40; + + /// + /// Offset to ClustersPerIndexBlock field (int8). + /// + private const int ClustersPerIndexBlockOffset = 0x44; + + /// + /// Offset to VolumeSerialNumber field (uint64). + /// + private const int VolumeSerialNumberOffset = 0x48; + + /// + /// Minimum valid bytes per sector (512). + /// + private const ushort MinBytesPerSector = 512; + + /// + /// Maximum valid bytes per sector (4096). + /// + private const ushort MaxBytesPerSector = 4096; + + /// + /// Minimum valid sectors per cluster (1). + /// + private const byte MinSectorsPerCluster = 1; + + /// + /// Maximum valid sectors per cluster (128). + /// + private const byte MaxSectorsPerCluster = 128; + + /// + /// Minimum required buffer size (512 bytes for boot sector). + /// + private const int MinimumBufferSize = 512; + + /// + /// Gets the number of bytes per sector (typically 512, 1024, 2048, or 4096). + /// + public ushort BytesPerSector { get; } + + /// + /// Gets the number of sectors per cluster (power of 2: 1, 2, 4, 8, 16, 32, 64, 128). + /// + public byte SectorsPerCluster { get; } + + /// + /// Gets the total number of sectors in the volume (uint64). + /// + public ulong TotalSectors { get; } + + /// + /// Gets the cluster number where the $MFT starts. + /// + public ulong MftClusterNumber { get; } + + /// + /// Gets the cluster number where the $MFTMirr (MFT mirror) starts. + /// + public ulong MftMirrorClusterNumber { get; } + + /// + /// Gets the clusters per MFT record as encoded in the boot sector. + /// If negative, record size is 2^abs(value) bytes. + /// If positive, record size is value * ClusterSize bytes. + /// + public sbyte ClustersPerMftRecord { get; } + + /// + /// Gets the clusters per index block as encoded in the boot sector. + /// Same encoding as ClustersPerMftRecord. + /// + public sbyte ClustersPerIndexBlock { get; } + + /// + /// Gets the volume serial number. + /// + public ulong VolumeSerialNumber { get; } + + /// + /// Gets the size of a cluster in bytes. + /// + public int ClusterSize => BytesPerSector * SectorsPerCluster; + + /// + /// Gets the size of an MFT record in bytes. + /// If ClustersPerMftRecord is negative: 2^abs(ClustersPerMftRecord) bytes. + /// If ClustersPerMftRecord is positive: ClustersPerMftRecord * ClusterSize bytes. + /// + public int MftRecordSize => ClustersPerMftRecord < 0 + ? 1 << -ClustersPerMftRecord // 2^abs(value) + : ClustersPerMftRecord * ClusterSize; + + /// + /// Gets the byte offset from the partition start to the MFT. + /// + public long MftByteOffset => (long)MftClusterNumber * ClusterSize; + + /// + /// Gets the total number of clusters in the volume. + /// + public long TotalClusters => (long)(TotalSectors / SectorsPerCluster); + + /// + /// Gets the total size of the volume in bytes. + /// + public long TotalSize => (long)TotalSectors * BytesPerSector; + + /// + /// Minimum valid total sectors for an NTFS volume. + /// + private const ulong MinTotalSectors = 16; + + /// + /// Initializes a new instance of the struct by parsing the provided boot sector data. + /// + /// The boot sector data (must be at least 512 bytes). + /// Thrown when the buffer is too small or validation fails. + public NtfsBootSector(ReadOnlySpan bootSectorData) + { + if (bootSectorData.Length < MinimumBufferSize) + throw new ArgumentException($"Boot sector data must be at least {MinimumBufferSize} bytes.", nameof(bootSectorData)); + + // Validate boot sector signature (bytes 0x55, 0xAA at offset 510; 0xAA55 as little-endian uint16) + var signature = BinaryPrimitives.ReadUInt16LittleEndian(bootSectorData.Slice(SignatureOffset, 2)); + if (signature != BootSectorSignature) + throw new ArgumentException($"Invalid boot sector signature: expected 0x{BootSectorSignature:X4}, got 0x{signature:X4}.", nameof(bootSectorData)); + + // Validate OEM ID at offset 0x03 (should be "NTFS ") + var oemId = Encoding.ASCII.GetString(bootSectorData.Slice(OemIdOffset, OemIdLength)); + if (oemId != ExpectedOemId) + throw new ArgumentException($"Invalid OEM ID: expected '{ExpectedOemId}', got '{oemId}'.", nameof(bootSectorData)); + + // Parse fields from the boot sector + BytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(bootSectorData.Slice(BytesPerSectorOffset, 2)); + SectorsPerCluster = bootSectorData[SectorsPerClusterOffset]; + TotalSectors = BinaryPrimitives.ReadUInt64LittleEndian(bootSectorData.Slice(TotalSectorsOffset, 8)); + MftClusterNumber = BinaryPrimitives.ReadUInt64LittleEndian(bootSectorData.Slice(MftClusterNumberOffset, 8)); + MftMirrorClusterNumber = BinaryPrimitives.ReadUInt64LittleEndian(bootSectorData.Slice(MftMirrorClusterNumberOffset, 8)); + ClustersPerMftRecord = (sbyte)bootSectorData[ClustersPerMftRecordOffset]; + ClustersPerIndexBlock = (sbyte)bootSectorData[ClustersPerIndexBlockOffset]; + VolumeSerialNumber = BinaryPrimitives.ReadUInt64LittleEndian(bootSectorData.Slice(VolumeSerialNumberOffset, 8)); + + // Validate BytesPerSector is a power of 2 (512, 1024, 2048, or 4096) + if (!IsPowerOfTwo(BytesPerSector) || BytesPerSector < MinBytesPerSector || BytesPerSector > MaxBytesPerSector) + throw new ArgumentException($"Invalid BytesPerSector: {BytesPerSector}. Must be a power of 2 between {MinBytesPerSector} and {MaxBytesPerSector}.", nameof(bootSectorData)); + + // Validate SectorsPerCluster is a power of 2 (1, 2, 4, 8, 16, 32, 64, or 128) + if (!IsPowerOfTwo(SectorsPerCluster) || SectorsPerCluster < MinSectorsPerCluster || SectorsPerCluster > MaxSectorsPerCluster) + throw new ArgumentException($"Invalid SectorsPerCluster: {SectorsPerCluster}. Must be a power of 2 between {MinSectorsPerCluster} and {MaxSectorsPerCluster}.", nameof(bootSectorData)); + + // Validate that BytesPerSector * SectorsPerCluster does not overflow + // This is effectively checking that ClusterSize is valid + try + { + _ = checked(BytesPerSector * SectorsPerCluster); + } + catch (OverflowException) + { + throw new ArgumentException($"Cluster size overflow: BytesPerSector ({BytesPerSector}) * SectorsPerCluster ({SectorsPerCluster}) exceeds maximum value.", nameof(bootSectorData)); + } + + // Validate TotalSectors > 0 + if (TotalSectors == 0) + throw new ArgumentException($"Invalid TotalSectors: {TotalSectors}. Must be greater than 0.", nameof(bootSectorData)); + + if (TotalSectors < MinTotalSectors) + throw new ArgumentException($"Invalid TotalSectors: {TotalSectors}. Must be at least {MinTotalSectors}.", nameof(bootSectorData)); + + // Validate MftClusterNumber is within volume bounds + if (MftClusterNumber >= (ulong)TotalClusters) + throw new ArgumentException($"Invalid MftClusterNumber: {MftClusterNumber}. Must be less than TotalClusters ({TotalClusters}).", nameof(bootSectorData)); + + // Validate MftRecordSize - if positive, ensure it doesn't overflow + if (ClustersPerMftRecord > 0) + { + try + { + _ = checked(ClustersPerMftRecord * ClusterSize); + } + catch (OverflowException) + { + throw new ArgumentException($"MFT record size overflow: ClustersPerMftRecord ({ClustersPerMftRecord}) * ClusterSize ({ClusterSize}) exceeds maximum value.", nameof(bootSectorData)); + } + } + + // Validate IndexBlockSize - if positive, ensure it doesn't overflow + if (ClustersPerIndexBlock > 0) + { + try + { + _ = checked(ClustersPerIndexBlock * ClusterSize); + } + catch (OverflowException) + { + throw new ArgumentException($"Index block size overflow: ClustersPerIndexBlock ({ClustersPerIndexBlock}) * ClusterSize ({ClusterSize}) exceeds maximum value.", nameof(bootSectorData)); + } + } + } + + /// + /// Checks if a value is a power of 2. + /// + /// The value to check. + /// True if the value is a power of 2; otherwise, false. + private static bool IsPowerOfTwo(ushort value) => value != 0 && (value & (value - 1)) == 0; + + /// + /// Checks if a value is a power of 2. + /// + /// The value to check. + /// True if the value is a power of 2; otherwise, false. + private static bool IsPowerOfTwo(byte value) => value != 0 && (value & (value - 1)) == 0; +}