using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Duplicati.Proprietary.DiskImage.Disk; using Duplicati.Proprietary.DiskImage.General; namespace Duplicati.Proprietary.DiskImage.Partition; /// /// Represents an MBR (Master Boot Record) partition table. /// MBR is the traditional partition table format used on BIOS-based systems. /// internal class MBR : IPartitionTable { // Constants for MBR parsing (from PartitionConstants) private const int MbrSize = PartitionConstants.MbrSize; private const int PartitionEntrySize = PartitionConstants.MbrPartitionEntrySize; private const int MaxPartitionEntries = PartitionConstants.MaxMbrPartitionEntries; private const ushort MbrBootSignature = PartitionConstants.MbrBootSignature; // Internal state private bool m_parsed = false; private bool m_disposed = false; // MBR Header fields private ushort m_mbrBootSignature; private List? m_partitionEntries; // Additional tracking private IRawDisk? m_rawDisk; private byte[]? m_mbrBytes; private long m_bytesPerSector; // Partition storage private List? m_partitions; /// /// Initializes a new instance of the class. /// /// The raw disk to parse, or null for byte array parsing. public MBR(IRawDisk? disk) { m_rawDisk = disk; } // IPartitionTable implementation /// public IRawDisk? RawDisk { get => m_rawDisk; } /// public PartitionTableType TableType => PartitionTableType.MBR; /// /// Parses the MBR partition table from the raw disk. /// /// The raw disk to parse. /// Cancellation token. /// True if parsing was successful. public async Task ParseAsync(IRawDisk disk, CancellationToken token) { m_bytesPerSector = disk.SectorSize; // Read the MBR (LBA 0) m_mbrBytes = new byte[MbrSize]; using var bytestream = await disk.ReadBytesAsync(0, MbrSize, token).ConfigureAwait(false); await bytestream.ReadAtLeastAsync(m_mbrBytes, MbrSize, cancellationToken: token).ConfigureAwait(false); var result = await ParseMBRBytesAsync(m_mbrBytes, token).ConfigureAwait(false); return result; } /// /// Parses the MBR partition table from a byte array. /// /// The raw disk bytes. /// The sector size in bytes. /// Cancellation token. /// True if parsing was successful. public async Task ParseAsync(byte[] bytes, int sectorSize, CancellationToken token) { m_bytesPerSector = sectorSize; // Read MBR from the first sector m_mbrBytes = bytes[0..sectorSize]; return await ParseMBRBytesAsync(m_mbrBytes, token).ConfigureAwait(false); } /// /// Parses the MBR partition table from the given byte array containing the MBR sector. /// Assumes the byte array is at least 512 bytes and contains the MBR data. /// /// The byte array containing the MBR sector. /// Cancellation token. /// True if parsing was successful. /// Thrown if the byte array is smaller than the MBR size. private async Task ParseMBRBytesAsync(byte[] bytes, CancellationToken token) { if (bytes.Length < MbrSize) throw new ArgumentException($"Byte array must be at least {MbrSize} bytes long.", nameof(bytes)); // Read boot signature (2 bytes at offset 510) m_mbrBootSignature = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(510, 2)); // Verify boot signature if (m_mbrBootSignature != MbrBootSignature) return false; m_partitionEntries = []; // Parse partition entries (64 bytes starting at offset 446) for (int i = 0; i < MaxPartitionEntries; i++) { token.ThrowIfCancellationRequested(); int offset = 446 + (i * PartitionEntrySize); var entry = ParsePartitionEntry(bytes, offset, i + 1); if (entry != null) { m_partitionEntries.Add(entry); // Add to partitions list for IPartitionTable implementation if (m_partitions == null) m_partitions = []; m_partitions.Add(new BasePartition { PartitionNumber = i + 1, Type = entry.PartitionType, PartitionTable = this, StartOffset = entry.StartLBA * m_bytesPerSector, Size = entry.SizeInSectors * m_bytesPerSector, Name = $"Partition {i + 1}", FilesystemType = DetermineFilesystemType(entry), VolumeGuid = null, RawDisk = m_rawDisk, StartingLba = entry.StartLBA, EndingLba = entry.StartLBA + entry.SizeInSectors - 1 }); } } m_parsed = true; return true; } /// /// Parses a single partition entry from the MBR. /// /// The byte array containing the MBR sector. /// The offset within the byte array where the partition entry starts. /// The partition entry number (1-based). /// The parsed MBR partition entry, or null if the entry is empty or invalid. private MBRPartitionEntry? ParsePartitionEntry(byte[] bytes, int offset, int entryNumber) { // Read partition type byte byte partitionType = bytes[offset + 4]; // Skip empty entries if (partitionType == 0) return null; // Read CHS values (not commonly used, but we store them) byte startHead = bytes[offset + 1]; byte startSector = bytes[offset + 2]; ushort startCylinder = (ushort)(((bytes[offset + 3] & 0xC0) << 2) | bytes[offset + 2]); byte endHead = bytes[offset + 5]; byte endSector = bytes[offset + 6]; ushort endCylinder = (ushort)(((bytes[offset + 7] & 0xC0) << 2) | bytes[offset + 6]); // Read LBA values (more reliable than CHS) uint startLBA = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset + 8, 4)); uint sizeInSectors = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset + 12, 4)); // Skip entries with zero size if (sizeInSectors == 0) return null; return new MBRPartitionEntry { EntryNumber = entryNumber, PartitionType = DeterminePartitionType(partitionType), PartitionTypeByte = partitionType, StartLBA = startLBA, SizeInSectors = sizeInSectors, StartHead = startHead, StartSector = startSector, StartCylinder = startCylinder, EndHead = endHead, EndSector = endSector, EndCylinder = endCylinder }; } /// /// Determines the partition type based on the partition type byte. /// /// The partition type byte from the MBR. /// The determined partition type. private static PartitionType DeterminePartitionType(byte typeByte) { return MbrPartitionTypes.ToPartitionType(typeByte); } /// /// Determines the filesystem type based on the partition type byte. /// /// The MBR partition entry. /// The determined filesystem type. private static FileSystemType DetermineFilesystemType(MBRPartitionEntry entry) { return MbrPartitionTypes.ToFilesystemType(entry.PartitionTypeByte); } // MBR-specific properties /// /// Gets the MBR boot signature (0xAA55). /// /// Thrown if MBR has not been parsed. public ushort MbrBootSignatureValue => m_parsed ? m_mbrBootSignature : throw new InvalidOperationException("MBR not parsed."); /// /// Gets the number of partition entries. /// /// Thrown if MBR has not been parsed. public int NumPartitionEntries => m_parsed ? (m_partitionEntries?.Count ?? 0) : throw new InvalidOperationException("MBR not parsed."); /// public async IAsyncEnumerable EnumeratePartitions([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { if (!m_parsed) throw new InvalidOperationException("MBR not parsed."); if (m_partitions != null) { foreach (var partition in m_partitions) { cancellationToken.ThrowIfCancellationRequested(); yield return partition; } } } /// public async Task GetPartitionAsync(int partitionNumber, CancellationToken cancellationToken) { if (!m_parsed) throw new InvalidOperationException("MBR not parsed."); if (m_partitions == null) return null; if (partitionNumber >= 1 && partitionNumber <= m_partitions.Count) return m_partitions[partitionNumber - 1]; return null; } /// public Task GetProtectiveMbrAsync(CancellationToken cancellationToken) { // MBR doesn't have a protective MBR - it IS the MBR throw new NotSupportedException("MBR partition tables do not have a protective MBR. Use GetPartitionAsync instead."); } /// public Task GetPartitionTableDataAsync(CancellationToken cancellationToken) { if (!m_parsed || m_mbrBytes == null) throw new InvalidOperationException("MBR not parsed."); // Return the MBR sector (512 bytes) return Task.FromResult(new MemoryStream(m_mbrBytes, writable: false)); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes the MBR instance, releasing any resources. After disposal, the instance should not be used. /// /// Indicates whether the method is called from Dispose (true) or from a finalizer (false). protected virtual void Dispose(bool disposing) { if (!m_disposed) { if (disposing) { m_mbrBytes = null; if (m_partitions != null) { foreach (var partition in m_partitions) partition.Dispose(); m_partitions = null; } } m_disposed = true; } } /// /// Represents an MBR partition entry. /// private class MBRPartitionEntry { /// Gets the entry number (1-4). public int EntryNumber { get; init; } /// Gets the partition type. public PartitionType PartitionType { get; init; } /// Gets the raw partition type byte. public byte PartitionTypeByte { get; init; } /// Gets the starting LBA. public uint StartLBA { get; init; } /// Gets the size in sectors. public uint SizeInSectors { get; init; } /// Gets the starting head (CHS). public byte StartHead { get; init; } /// Gets the starting sector (CHS). public byte StartSector { get; init; } /// Gets the starting cylinder (CHS). public ushort StartCylinder { get; init; } /// Gets the ending head (CHS). public byte EndHead { get; init; } /// Gets the ending sector (CHS). public byte EndSector { get; init; } /// Gets the ending cylinder (CHS). public ushort EndCylinder { get; init; } } }