using System; using System.Buffers.Binary; using System.Linq; using System.Threading; using System.Threading.Tasks; using Duplicati.Proprietary.DiskImage.Disk; using Duplicati.Proprietary.DiskImage.General; namespace Duplicati.Proprietary.DiskImage.Partition; /// /// Factory for creating partition table instances with auto-detection. /// Parses the MBR first to determine if the disk uses GPT or MBR partition tables. /// internal static class PartitionTableFactory { // MBR constants (from PartitionConstants) private const int MbrSize = PartitionConstants.MbrSize; private const ushort MbrBootSignature = PartitionConstants.MbrBootSignature; private const byte ProtectiveMbrType = PartitionConstants.ProtectiveMbrType; // GPT constants (from PartitionConstants) private const int HeaderSize = PartitionConstants.GptHeaderSize; private const long GptSignature = PartitionConstants.GptSignature; /// /// Creates a partition table instance by auto-detecting the partition table type. /// Reads the MBR first, which determines if we have a GPT or MBR partition table. /// /// The raw disk to analyze. /// Cancellation token. /// The appropriate partition table instance (GPT or MBR), or null if detection fails. public static async Task CreateAsync(IRawDisk disk, CancellationToken cancellationToken) { if (disk == null) throw new ArgumentNullException(nameof(disk)); // Read the first sector (LBA 0 - MBR) var mbrBytes = new byte[MbrSize]; using var stream = await disk.ReadSectorsAsync(0, 1, cancellationToken).ConfigureAwait(false); await stream.ReadAtLeastAsync(mbrBytes, MbrSize, cancellationToken: cancellationToken).ConfigureAwait(false); // Detect partition table type from MBR var tableType = DetectPartitionTableType(mbrBytes); return tableType switch { PartitionTableType.GPT => await CreateGPTAsync(disk, mbrBytes, cancellationToken).ConfigureAwait(false), PartitionTableType.MBR => await CreateMBRAsync(disk, mbrBytes, cancellationToken).ConfigureAwait(false), _ => new UnknownPartitionTable(disk) }; } /// /// Creates a partition table instance from raw byte array with auto-detection. /// /// The raw disk bytes. /// The sector size (typically 512 or 4096). /// Cancellation token. /// The appropriate partition table instance (GPT or MBR), or null if detection fails. public static async Task CreateAsync(byte[] bytes, int sectorSize, CancellationToken cancellationToken) { if (bytes == null) throw new ArgumentNullException(nameof(bytes)); if (bytes.Length < MbrSize) throw new ArgumentException($"Byte array must be at least {MbrSize} bytes long.", nameof(bytes)); if (sectorSize <= 0) throw new ArgumentException("Sector size must be positive.", nameof(sectorSize)); // Read the first sector (LBA 0 - MBR) var mbrBytes = bytes[0..sectorSize]; // Detect partition table type from MBR var tableType = DetectPartitionTableType(mbrBytes); return tableType switch { PartitionTableType.GPT => await CreateGPTAsync(bytes, sectorSize, cancellationToken).ConfigureAwait(false), PartitionTableType.MBR => await CreateMBRAsync(mbrBytes, sectorSize, cancellationToken).ConfigureAwait(false), _ => new UnknownPartitionTable(null) }; } /// /// Detects the partition table type by examining the MBR. /// private static PartitionTableType DetectPartitionTableType(byte[] mbrBytes) { // Check for valid MBR boot signature ushort bootSignature = BinaryPrimitives.ReadUInt16LittleEndian(mbrBytes.AsSpan(510, 2)); if (bootSignature != MbrBootSignature) return PartitionTableType.Unknown; // Check for GPT protective MBR (type 0xEE in first partition entry) byte partitionType = mbrBytes[450]; // If first partition entry has type 0xEE, this is likely a GPT disk if (partitionType == ProtectiveMbrType) { return PartitionTableType.GPT; } // Otherwise, it's a traditional MBR disk return PartitionTableType.MBR; } /// /// Checks if the disk has a valid GPT header at LBA 1. /// This is used to confirm GPT detection when protective MBR is present. /// private static async Task HasValidGptHeaderAsync(IRawDisk disk, CancellationToken cancellationToken) { try { var sectorSize = disk.SectorSize; var headerBytes = new byte[HeaderSize]; // Read a sector at LBA 1 (GPT header) using var stream = await disk.ReadBytesAsync(sectorSize, sectorSize, cancellationToken).ConfigureAwait(false); await stream.ReadAtLeastAsync(headerBytes, HeaderSize, cancellationToken: cancellationToken).ConfigureAwait(false); var signature = BinaryPrimitives.ReadInt64LittleEndian(headerBytes.AsSpan(0, 8)); return signature == GptSignature; } catch { return false; } } /// /// Checks if the byte array has a valid GPT header at sector 1. /// private static bool HasValidGptHeader(byte[] bytes, int sectorSize) { try { if (bytes.Length < sectorSize + HeaderSize) return false; var headerBytes = bytes[sectorSize..(sectorSize + HeaderSize)]; var signature = BinaryPrimitives.ReadInt64LittleEndian(headerBytes.AsSpan(0, 8)); return signature == GptSignature; } catch { return false; } } private static async Task CreateGPTAsync(IRawDisk disk, byte[] mbrBytes, CancellationToken cancellationToken) { var gpt = new GPT(disk); // Verify GPT signature at LBA 1 if (!await HasValidGptHeaderAsync(disk, cancellationToken).ConfigureAwait(false)) { // Fall back to parsing as MBR if GPT header is invalid return await CreateMBRAsync(disk, mbrBytes, cancellationToken).ConfigureAwait(false); } await gpt.ParseAsync(cancellationToken).ConfigureAwait(false); return gpt; } private static async Task CreateGPTAsync(byte[] mbrBytes, int sectorSize, CancellationToken cancellationToken) { var gpt = new GPT(null); // Verify GPT signature at LBA 1 if (!HasValidGptHeader(mbrBytes, sectorSize)) { // Fall back to parsing as MBR if GPT header is invalid return await CreateMBRAsync(mbrBytes, sectorSize, cancellationToken).ConfigureAwait(false); } await gpt.ParseAsync(mbrBytes, sectorSize, cancellationToken).ConfigureAwait(false); return gpt; } private static async Task CreateMBRAsync(IRawDisk disk, byte[] mbrBytes, CancellationToken cancellationToken) { var mbr = new MBR(disk); // Use the already read bytes to avoid race conditions and redundant reads if (!await mbr.ParseAsync(mbrBytes, disk.SectorSize, cancellationToken).ConfigureAwait(false)) { throw new InvalidOperationException("Failed to parse MBR from validated bytes."); } return mbr; } private static async Task CreateMBRAsync(byte[] mbrBytes, int sectorSize, CancellationToken cancellationToken) { var mbr = new MBR(null); await mbr.ParseAsync(mbrBytes, sectorSize, cancellationToken).ConfigureAwait(false); return mbr; } }