using System; using System.Buffers; 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 a GPT (GUID Partition Table) partition table. /// GPT is the modern partition table format used on UEFI-based systems. /// internal class GPT : IPartitionTable { // Constants for GPT parsing (from PartitionConstants) private const int HeaderSize = PartitionConstants.GptHeaderSize; private const long GptSignature = PartitionConstants.GptSignature; private const int MbrSize = PartitionConstants.MbrSize; private const ushort MbrBootSignature = PartitionConstants.MbrBootSignature; private const byte ProtectiveMbrType = PartitionConstants.ProtectiveMbrType; // Internal state private bool m_parsed = false; private bool m_disposed = false; // GPT Header fields private long m_signature; private uint m_revision; private uint m_headerSize; private uint m_headerCrc32; private long m_currentLba; private long m_backupLba; private long m_firstUsableLba; private long m_lastUsableLba; private Guid m_diskGuid; private long m_partitionEntryLba; private uint m_numPartitionEntries; private uint m_partitionEntrySize; private uint m_partitionEntryCrc32; // Additional tracking private IRawDisk? m_rawDisk; private byte[]? m_headerBytes; private byte[]? m_protectiveMbrBytes; private long m_bytesPerSector; // Partition storage private List? m_partitions; /// public IRawDisk? RawDisk { get => m_rawDisk; } /// public PartitionTableType TableType => PartitionTableType.GPT; /// /// Initializes a new instance of the class. /// /// The raw disk to parse, or null for byte array parsing. public GPT(IRawDisk? disk) { m_rawDisk = disk; } /// /// Parses the GPT partition table from the raw disk. /// /// Cancellation token. /// True if parsing was successful. public async Task ParseAsync(CancellationToken token) { var parsedHeader = await ParseHeaderAsync(token) .ConfigureAwait(false); if (!parsedHeader) return false; return await ParsePartitionEntriesAsync(token) .ConfigureAwait(false); } /// /// Parses the GPT 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; // Parse the protective MBR first (LBA 0; first sectorSize bytes) if (!await ParseProtectiveMbrAsync(bytes, sectorSize, token).ConfigureAwait(false)) return false; // Now parse the GPT header (LBA 1) - use span slicing to avoid allocation var parsedHeader = await ParseHeaderAsync(bytes.AsSpan(sectorSize, HeaderSize), token).ConfigureAwait(false); if (!parsedHeader) return false; // Calculate the byte offset for the partition entries int partitionEntriesOffset = (int)(m_partitionEntryLba * m_bytesPerSector); int sizeEntries = (int)(m_partitionEntrySize * m_numPartitionEntries); var partitionBytes = bytes[partitionEntriesOffset..(partitionEntriesOffset + sizeEntries)]; return await ParsePartitionEntriesAsync(partitionBytes, token).ConfigureAwait(false); } /// /// Parses the protective MBR to verify this is a GPT disk. /// private async Task ParseProtectiveMbrAsync(byte[] bytes, int sectorSize, CancellationToken token) { if (bytes.Length < sectorSize) throw new ArgumentException($"Byte array must be at least {sectorSize} bytes long.", nameof(bytes)); // Extract the MBR (first sector) m_protectiveMbrBytes = bytes[0..sectorSize]; // Verify MBR boot signature (offset 510) ushort bootSignature = BinaryPrimitives.ReadUInt16LittleEndian(m_protectiveMbrBytes.AsSpan(510)); if (bootSignature != MbrBootSignature) return false; // Check if first partition entry has protective MBR type (0xEE) byte partitionType = m_protectiveMbrBytes[450]; if (partitionType != ProtectiveMbrType) return false; return true; } /// /// Parses the GPT header from the raw disk. /// /// Cancellation token. /// True if parsing was successful. public async Task ParseHeaderAsync(CancellationToken token) { if (m_rawDisk == null) throw new InvalidOperationException("No raw disk available for reading GPT header."); m_bytesPerSector = m_rawDisk.SectorSize; // Read the GPT header (LBA 1) directly into a pooled buffer // Rent a sector-sized buffer, read into it, then copy just the header portion var sectorBuffer = ArrayPool.Shared.Rent((int)m_bytesPerSector); try { int bytesRead = await m_rawDisk.ReadBytesAsync(m_bytesPerSector, sectorBuffer.AsMemory(0, (int)m_bytesPerSector), token) .ConfigureAwait(false); if (bytesRead < HeaderSize) return false; // Copy header data to the long-lived header buffer m_headerBytes = new byte[HeaderSize]; sectorBuffer.AsSpan(0, HeaderSize).CopyTo(m_headerBytes); var result = await ParseHeaderAsync(m_headerBytes.AsSpan(), token) .ConfigureAwait(false); if (result) { // Verify backup header if (!await VerifyBackupHeaderAsync(token).ConfigureAwait(false)) return false; } return result; } finally { ArrayPool.Shared.Return(sectorBuffer); } } /// /// Parses the GPT header from a byte array. /// /// The header bytes. /// Cancellation token. /// True if parsing was successful. public async Task ParseHeaderAsync(byte[] bytes, CancellationToken token) => await ParseHeaderAsync(bytes.AsSpan(), token).ConfigureAwait(false); /// /// Parses the GPT header from a span of bytes. /// /// The header bytes. /// Cancellation token. /// True if parsing was successful. public Task ParseHeaderAsync(ReadOnlySpan bytes, CancellationToken token) { if (bytes.Length < HeaderSize) throw new ArgumentException($"Byte array must be at least {HeaderSize} bytes long.", nameof(bytes)); // Read signature (8 bytes, little-endian) m_signature = BinaryPrimitives.ReadInt64LittleEndian(bytes.Slice(0, 8)); // Verify signature if (m_signature != GptSignature) return Task.FromResult(false); // Read revision (4 bytes) m_revision = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(8, 4)); // Read header size (4 bytes) m_headerSize = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(12, 4)); // Read header CRC32 (4 bytes) m_headerCrc32 = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(16, 4)); // Reserved - must be zero (4 bytes at offset 20) var reserved = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(20, 4)); if (reserved != 0) return Task.FromResult(false); // Current LBA (8 bytes at offset 24) m_currentLba = BinaryPrimitives.ReadInt64LittleEndian(bytes.Slice(24, 8)); // Backup LBA (8 bytes at offset 32) m_backupLba = BinaryPrimitives.ReadInt64LittleEndian(bytes.Slice(32, 8)); // First usable LBA (8 bytes at offset 40) m_firstUsableLba = BinaryPrimitives.ReadInt64LittleEndian(bytes.Slice(40, 8)); // Last usable LBA (8 bytes at offset 48) m_lastUsableLba = BinaryPrimitives.ReadInt64LittleEndian(bytes.Slice(48, 8)); // Disk GUID (16 bytes at offset 56) m_diskGuid = new Guid(bytes.Slice(56, 16)); // Partition entry LBA (8 bytes at offset 72) m_partitionEntryLba = BinaryPrimitives.ReadInt64LittleEndian(bytes.Slice(72, 8)); // Number of partition entries (4 bytes at offset 80) m_numPartitionEntries = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(80, 4)); // Size of partition entry (4 bytes at offset 84) m_partitionEntrySize = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(84, 4)); // Partition entry CRC32 (4 bytes at offset 88) m_partitionEntryCrc32 = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(88, 4)); m_parsed = true; return Task.FromResult(true); } /// /// Parses the partition entries from the disk. /// /// Cancellation token. /// True if parsing was successful. /// Thrown if the raw disk is not available or if the header has not been parsed. private async Task ParsePartitionEntriesAsync(CancellationToken token) { if (m_rawDisk == null) throw new InvalidOperationException("No raw disk available for reading GPT partition entries."); if (m_partitions != null) return false; if (!m_parsed) return false; // Calculate the byte offset for the partition entries long partitionEntriesOffset = m_partitionEntryLba * m_bytesPerSector; // Read all partition entries in one go long totalSize = m_partitionEntrySize * m_numPartitionEntries; // Rent buffer from ArrayPool to avoid allocation for partition entries var buffer = ArrayPool.Shared.Rent((int)totalSize); try { using var stream = await m_rawDisk.ReadBytesAsync(partitionEntriesOffset, (int)totalSize, token) .ConfigureAwait(false); await stream.ReadAtLeastAsync(buffer, (int)totalSize, cancellationToken: token) .ConfigureAwait(false); return await ParsePartitionEntriesAsync(buffer.AsSpan(0, (int)totalSize), token) .ConfigureAwait(false); } finally { ArrayPool.Shared.Return(buffer); } } /// /// Parses the partition entries from a byte array. /// /// The byte array containing the partition entries. /// Cancellation token. /// True if parsing was successful. private Task ParsePartitionEntriesAsync(byte[] buffer, CancellationToken token) => ParsePartitionEntriesAsync(buffer.AsSpan(), token); /// /// Parses the partition entries from a span of bytes. /// /// The span containing the partition entries. /// Cancellation token. /// True if parsing was successful. private Task ParsePartitionEntriesAsync(ReadOnlySpan buffer, CancellationToken token) { if (m_partitions != null) return Task.FromResult(false); m_partitions = []; // Parse each partition entry for (int i = 0; i < m_numPartitionEntries; i++) { token.ThrowIfCancellationRequested(); int offset = (int)(i * m_partitionEntrySize); // Check if this entry is empty (all zeros in the first 16 bytes = partition type GUID) bool isEmpty = true; for (int j = 0; j < 16; j++) { if (buffer[offset + j] != 0) { isEmpty = false; break; } } if (isEmpty) continue; // Parse partition entry - use span slicing to avoid allocation var partition = ParsePartitionEntry(buffer.Slice(offset, (int)m_partitionEntrySize), i + 1); if (partition != null) m_partitions.Add(partition!); } return Task.FromResult(true); } /// /// Parses a single partition entry from a byte array. /// /// The byte array containing the partition entry. /// The offset in the byte array where the partition entry starts. /// The partition number. /// The parsed partition entry, or null if the entry is empty. private BasePartition? ParsePartitionEntry(byte[] buffer, int offset, int partitionNumber) => ParsePartitionEntry(buffer.AsSpan(offset), partitionNumber); /// /// Parses a single partition entry from a span of bytes. /// /// The span containing the partition entry. /// The partition number. /// The parsed partition entry, or null if the entry is empty. private BasePartition? ParsePartitionEntry(ReadOnlySpan entrySpan, int partitionNumber) { // Partition type GUID (16 bytes at offset 0) var typeGuid = new Guid(entrySpan.Slice(0, 16)); // Unique partition GUID (16 bytes at offset 16) var uniqueGuid = new Guid(entrySpan.Slice(16, 16)); // Starting LBA (8 bytes at offset 32) long startingLba = BinaryPrimitives.ReadInt64LittleEndian(entrySpan.Slice(32, 8)); // Ending LBA (8 bytes at offset 40) long endingLba = BinaryPrimitives.ReadInt64LittleEndian(entrySpan.Slice(40, 8)); // Attributes (8 bytes at offset 48) long attributes = BinaryPrimitives.ReadInt64LittleEndian(entrySpan.Slice(48, 8)); // Partition name (36 UTF-16LE characters = 72 bytes at offset 56) string name = System.Text.Encoding.Unicode.GetString(entrySpan.Slice(56, 72)).TrimEnd('\0'); // Calculate byte offsets and size long startOffset = startingLba * m_bytesPerSector; long size = (endingLba - startingLba + 1) * m_bytesPerSector; // Determine partition type based on type GUID PartitionType partitionType = DeterminePartitionType(typeGuid); // Determine filesystem type based on partition name and known patterns FileSystemType fsType = DetermineFilesystemType(name, typeGuid); return new BasePartition { PartitionNumber = partitionNumber, Type = partitionType, PartitionTable = this, StartOffset = startOffset, Size = size, Name = string.IsNullOrEmpty(name) ? null : name, FilesystemType = fsType, VolumeGuid = uniqueGuid, RawDisk = m_rawDisk, StartingLba = startingLba, EndingLba = endingLba, Attributes = attributes }; } /// /// Determines the partition type based on the partition type GUID. /// /// The partition type GUID. /// The corresponding . private static PartitionType DeterminePartitionType(Guid typeGuid) { return GptPartitionTypeGuids.ToPartitionType(typeGuid); } /// /// Determines the filesystem type based on the partition name and type GUID. /// Uses heuristics based on common naming patterns and known GUIDs. /// /// The partition name. /// The partition type GUID. /// The corresponding . private static FileSystemType DetermineFilesystemType(string name, Guid typeGuid) { if (!string.IsNullOrEmpty(name)) { var upperName = name.ToUpperInvariant(); if (upperName.Contains("NTFS")) return FileSystemType.NTFS; if (upperName.Contains("FAT32")) return FileSystemType.FAT32; if (upperName.Contains("FAT16")) return FileSystemType.FAT16; if (upperName.Contains("FAT12")) return FileSystemType.FAT12; if (upperName.Contains("EXFAT")) return FileSystemType.ExFAT; if (upperName.Contains("HFS")) return FileSystemType.HFSPlus; if (upperName.Contains("APFS")) return FileSystemType.APFS; if (upperName.Contains("EXT4")) return FileSystemType.Ext4; if (upperName.Contains("EXT3")) return FileSystemType.Ext3; if (upperName.Contains("EXT2")) return FileSystemType.Ext2; if (upperName.Contains("XFS")) return FileSystemType.XFS; if (upperName.Contains("BTRFS")) return FileSystemType.Btrfs; if (upperName.Contains("ZFS")) return FileSystemType.ZFS; if (upperName.Contains("REFS")) return FileSystemType.ReFS; } // Fallback based on GUID return typeGuid.ToString().ToUpper() switch { "48465300-0000-11AA-AA11-00306543ECAC" => FileSystemType.HFSPlus, "7C3457EF-0000-11AA-AA11-00306543ECAC" => FileSystemType.APFS, _ => FileSystemType.Unknown }; } // GPT-specific properties /// /// Gets the GPT signature ("EFI PART" in little-endian). /// /// Thrown if GPT header has not been parsed. public long Signature => m_parsed ? m_signature : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the GPT revision number. /// /// Thrown if GPT header has not been parsed. public uint Revision => m_parsed ? m_revision : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the GPT header size in bytes. /// /// Thrown if GPT header has not been parsed. public uint HeaderSizeField => m_parsed ? m_headerSize : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the CRC32 checksum of the GPT header. /// /// Thrown if GPT header has not been parsed. public uint HeaderCrc32 => m_parsed ? m_headerCrc32 : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the LBA (Logical Block Address) of the current GPT header. /// /// Thrown if GPT header has not been parsed. public long CurrentLba => m_parsed ? m_currentLba : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the LBA of the backup GPT header. /// /// Thrown if GPT header has not been parsed. public long BackupLba => m_parsed ? m_backupLba : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the first usable LBA for partitions. /// /// Thrown if GPT header has not been parsed. public long FirstUsableLba => m_parsed ? m_firstUsableLba : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the last usable LBA for partitions. /// /// Thrown if GPT header has not been parsed. public long LastUsableLba => m_parsed ? m_lastUsableLba : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the disk GUID. /// /// Thrown if GPT header has not been parsed. public Guid DiskGuid => m_parsed ? m_diskGuid : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the LBA where partition entries start. /// /// Thrown if GPT header has not been parsed. public long PartitionEntryLba => m_parsed ? m_partitionEntryLba : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the number of partition entries. /// /// Thrown if GPT header has not been parsed. public uint NumPartitionEntries => m_parsed ? m_numPartitionEntries : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the size of each partition entry in bytes. /// /// Thrown if GPT header has not been parsed. public uint PartitionEntrySizeField => m_parsed ? m_partitionEntrySize : throw new InvalidOperationException("GPT header not parsed."); /// /// Gets the CRC32 checksum of the partition entries. /// /// Thrown if GPT header has not been parsed. public uint PartitionEntryCrc32 => m_parsed ? m_partitionEntryCrc32 : throw new InvalidOperationException("GPT header not parsed."); /// public async IAsyncEnumerable EnumeratePartitions([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { if (!m_parsed) throw new InvalidOperationException("GPT header not parsed."); // Ensure partitions are parsed if (m_partitions == null && m_rawDisk != null) await ParsePartitionEntriesAsync(cancellationToken).ConfigureAwait(false); 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("GPT header not parsed."); // Ensure partitions are parsed if (m_partitions == null && m_rawDisk != null) await ParsePartitionEntriesAsync(cancellationToken).ConfigureAwait(false); if (m_partitions == null) return null; if (partitionNumber >= 1 && partitionNumber <= m_partitions.Count) return m_partitions[partitionNumber - 1]; return null; } /// public async Task GetProtectiveMbrAsync(CancellationToken cancellationToken) { if (!m_parsed) throw new InvalidOperationException("GPT header not parsed."); if (m_protectiveMbrBytes != null) { // Return a MemoryStream with the stored MBR bytes return new MemoryStream(m_protectiveMbrBytes, writable: false); } if (m_rawDisk != null) { // Read MBR from disk return await m_rawDisk.ReadBytesAsync(0, MbrSize, cancellationToken).ConfigureAwait(false); } throw new InvalidOperationException("No protective MBR available."); } /// public async Task GetPartitionTableDataAsync(CancellationToken cancellationToken) { if (!m_parsed) throw new InvalidOperationException("GPT header not parsed."); if (m_rawDisk == null) throw new InvalidOperationException("No raw disk available for reading GPT data."); // Calculate the total size needed: // - Protective MBR (512 bytes) // - GPT Header (1 sector) // - Partition Entries (m_partitionEntryLba sectors) long partitionEntriesEnd = m_partitionEntryLba * m_bytesPerSector + (m_numPartitionEntries * m_partitionEntrySize); long totalSize = partitionEntriesEnd; // Read all the data using var stream = await m_rawDisk.ReadBytesAsync(0, (int)totalSize, cancellationToken).ConfigureAwait(false); var buffer = new byte[totalSize]; await stream.ReadAtLeastAsync(buffer, (int)totalSize, cancellationToken: cancellationToken).ConfigureAwait(false); return new MemoryStream(buffer, writable: false); } /// /// Verifies the backup GPT header. /// /// Cancellation token. /// True if the backup header is valid, false otherwise. private async Task VerifyBackupHeaderAsync(CancellationToken token) { if (m_rawDisk == null) throw new InvalidOperationException("No raw disk available for reading GPT backup header."); if (m_backupLba == 0) return false; var backupHeaderBytes = new byte[HeaderSize]; long backupOffset = m_backupLba * m_bytesPerSector; try { using var stream = await m_rawDisk.ReadBytesAsync(backupOffset, (int)m_bytesPerSector, token).ConfigureAwait(false); await stream.ReadAtLeastAsync(backupHeaderBytes, HeaderSize, cancellationToken: token).ConfigureAwait(false); } catch { return false; } // Verify Signature long signature = BinaryPrimitives.ReadInt64LittleEndian(backupHeaderBytes.AsSpan(0, 8)); if (signature != GptSignature) return false; // Verify CRC32 uint storedCrc = BinaryPrimitives.ReadUInt32LittleEndian(backupHeaderBytes.AsSpan(16, 4)); // Zero out CRC field for calculation var copyForCrc = new byte[HeaderSize]; Array.Copy(backupHeaderBytes, copyForCrc, HeaderSize); BinaryPrimitives.WriteUInt32LittleEndian(copyForCrc.AsSpan(16), 0u); uint calculatedCrc = Crc32.Calculate(copyForCrc, 0, HeaderSize); if (storedCrc != calculatedCrc) return false; // Verify other fields // Revision should be same if (BinaryPrimitives.ReadUInt32LittleEndian(backupHeaderBytes.AsSpan(8, 4)) != m_revision) return false; // Header size should be same if (BinaryPrimitives.ReadUInt32LittleEndian(backupHeaderBytes.AsSpan(12, 4)) != m_headerSize) return false; // Reserved should be 0 if (BinaryPrimitives.ReadUInt32LittleEndian(backupHeaderBytes.AsSpan(20, 4)) != 0) return false; // Current LBA should be Backup LBA if (BinaryPrimitives.ReadInt64LittleEndian(backupHeaderBytes.AsSpan(24, 8)) != m_backupLba) return false; // Backup LBA should be Current LBA (Primary LBA) if (BinaryPrimitives.ReadInt64LittleEndian(backupHeaderBytes.AsSpan(32, 8)) != m_currentLba) return false; // Usable LBAs should be same if (BinaryPrimitives.ReadInt64LittleEndian(backupHeaderBytes.AsSpan(40, 8)) != m_firstUsableLba) return false; if (BinaryPrimitives.ReadInt64LittleEndian(backupHeaderBytes.AsSpan(48, 8)) != m_lastUsableLba) return false; // Disk GUID should be same if (new Guid(backupHeaderBytes.AsSpan(56, 16)) != m_diskGuid) return false; // Number of partition entries should be same if (BinaryPrimitives.ReadUInt32LittleEndian(backupHeaderBytes.AsSpan(80, 4)) != m_numPartitionEntries) return false; // Size of partition entry should be same if (BinaryPrimitives.ReadUInt32LittleEndian(backupHeaderBytes.AsSpan(84, 4)) != m_partitionEntrySize) return false; // Partition entry CRC32 should be same if (BinaryPrimitives.ReadUInt32LittleEndian(backupHeaderBytes.AsSpan(88, 4)) != m_partitionEntryCrc32) return false; return true; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes the GPT 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_headerBytes = null; m_protectiveMbrBytes = null; if (m_partitions != null) { foreach (var partition in m_partitions) partition.Dispose(); m_partitions = null; } } m_disposed = true; } } }