// Copyright (c) 2026 Duplicati Inc. All rights reserved. using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Duplicati.Proprietary.DiskImage.Disk; using Duplicati.Proprietary.DiskImage.General; namespace Duplicati.Proprietary.DiskImage.Partition; /// /// Synthesizes MBR and GPT partition tables from geometry metadata. /// This class handles the creation of raw byte arrays representing partition tables /// for disk image restoration operations. /// internal static class PartitionTableSynthesizer { // Constants from PartitionConstants private const int MbrSize = PartitionConstants.MbrSize; private const int GptHeaderSize = PartitionConstants.GptHeaderSize; private const ushort MbrBootSignature = PartitionConstants.MbrBootSignature; private const byte ProtectiveMbrType = PartitionConstants.ProtectiveMbrType; private const long GptSignature = PartitionConstants.GptSignature; private const uint GptRevision = PartitionConstants.GptRevision; private const int PartitionEntrySize = PartitionConstants.GptPartitionEntrySize; /// /// Synthesizes a partition table (MBR or GPT) from geometry metadata into a byte array. /// Auto-detects whether to create MBR or GPT based on the metadata. /// /// The geometry metadata containing partition table information. /// A byte array containing the synthesized partition table data, or null if no partition table is specified. public static byte[]? SynthesizePartitionTable(GeometryMetadata metadata) { if (metadata.PartitionTable == null) return null; return metadata.PartitionTable.Type switch { PartitionTableType.MBR => SynthesizeMBR(metadata), PartitionTableType.GPT => SynthesizeGPT(metadata), _ => null }; } /// /// Synthesizes an MBR partition table from geometry metadata. /// /// The geometry metadata containing partition information. /// A byte array containing the synthesized MBR partition table. public static byte[] SynthesizeMBR(GeometryMetadata metadata) { var sectorSize = metadata.Disk?.SectorSize ?? MbrSize; var mbrData = new byte[sectorSize]; // Boot code area (first 446 bytes) - typically zeros for new MBR // Could copy from original if available, but zeros are fine for restore // Partition entries start at offset 446 int partitionEntryOffset = 446; int partitionEntrySize = 16; if (metadata.Partitions != null) { // MBR supports up to 4 primary partitions var mbrPartitions = metadata.Partitions .Where(p => p.TableType == PartitionTableType.MBR) .OrderBy(p => p.Number) .Take(4) .ToList(); for (int i = 0; i < mbrPartitions.Count && i < 4; i++) { var part = mbrPartitions[i]; int offset = partitionEntryOffset + (i * partitionEntrySize); WriteMBRPartitionEntry(mbrData, offset, part, sectorSize); } } // Boot signature at offset 510-511 (0xAA55) mbrData[510] = 0x55; mbrData[511] = 0xAA; return mbrData; } /// /// Writes a single MBR partition entry to the specified offset. /// /// The MBR data buffer. /// The offset in the buffer to write the entry. /// The partition geometry. /// The sector size in bytes. private static void WriteMBRPartitionEntry(byte[] mbrData, int offset, PartitionGeometry part, int sectorSize) { // Status byte (0x80 = bootable, 0x00 = not bootable) // Default to not bootable, could be enhanced to detect bootable partitions mbrData[offset] = 0x00; // CHS start (3 bytes) - use LBA translation or zeros // Modern systems use LBA, so we can set these to 0xFF for invalid CHS mbrData[offset + 1] = 0xFF; mbrData[offset + 2] = 0xFF; mbrData[offset + 3] = 0xFF; // Partition type byte mbrData[offset + 4] = MbrPartitionTypes.ToTypeByte(part.FilesystemType, part.Type); // CHS end (3 bytes) - use LBA translation or zeros mbrData[offset + 5] = 0xFF; mbrData[offset + 6] = 0xFF; mbrData[offset + 7] = 0xFF; // Start LBA (4 bytes, little-endian) uint startLba = (uint)(part.StartOffset / sectorSize); BinaryPrimitives.WriteUInt32LittleEndian(mbrData.AsSpan(offset + 8, 4), startLba); // Size in sectors (4 bytes, little-endian) uint sizeInSectors = (uint)(part.Size / sectorSize); BinaryPrimitives.WriteUInt32LittleEndian(mbrData.AsSpan(offset + 12, 4), sizeInSectors); } /// /// Synthesizes a GPT partition table from geometry metadata. /// /// The geometry metadata containing partition information. /// A byte array containing the synthesized GPT partition table. public static byte[] SynthesizeGPT(GeometryMetadata metadata) { var sectorSize = metadata.Disk?.SectorSize ?? MbrSize; var diskSize = metadata.Disk?.Size ?? 0; var diskSectors = diskSize / sectorSize; // Calculate sizes int numPartitionEntries = 128; // Standard GPT supports 128 entries int partitionEntriesSize = numPartitionEntries * PartitionEntrySize; int partitionEntriesSectors = (partitionEntriesSize + sectorSize - 1) / sectorSize; // Total GPT data: Protective MBR (1 sector) + GPT Header (1 sector) + Partition Entries int totalGptSectors = 2 + partitionEntriesSectors; long totalSize = totalGptSectors * sectorSize; var gptData = new byte[totalSize]; // Write protective MBR at LBA 0 WriteProtectiveMBR(gptData, metadata, sectorSize, diskSectors); // Write GPT header at LBA 1 (sectorSize offset) WriteGPTHeader(gptData, metadata, sectorSize, partitionEntriesSectors, numPartitionEntries, diskSectors); // Write partition entries starting at LBA 2 (2 * sectorSize offset) WriteGPTPartitionEntries(gptData, metadata, sectorSize, partitionEntriesSectors); return gptData; } /// /// Writes the protective MBR for GPT. /// /// The GPT data buffer. /// The geometry metadata. /// The sector size in bytes. /// The total number of sectors on the disk. private static void WriteProtectiveMBR(byte[] gptData, GeometryMetadata metadata, int sectorSize, long diskSectors) { // Boot code (first 446 bytes) - zeros gptData.AsSpan(0, 446).Clear(); // Partition entry 1 (at offset 446): Protective MBR entry // Status byte gptData[446] = 0x00; // CHS start gptData[447] = 0x00; gptData[448] = 0x02; gptData[449] = 0x00; // Partition type: 0xEE (GPT protective) gptData[450] = ProtectiveMbrType; // CHS end (max values for large disks) gptData[451] = 0xFF; gptData[452] = 0xFF; gptData[453] = 0xFF; // Start LBA = 1 (GPT header is at LBA 1) BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(454, 4), 1u); // Size in sectors (max 0xFFFFFFFF for protective MBR) uint sizeInSectors = diskSectors > uint.MaxValue ? uint.MaxValue : (uint)(diskSectors - 1); BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(458, 4), sizeInSectors); // Boot signature at offset 510-511 gptData[510] = 0x55; gptData[511] = 0xAA; } /// /// Writes the GPT header. /// /// The GPT data buffer. /// The geometry metadata. /// The sector size in bytes. /// The number of sectors for partition entries. /// The number of partition entries. /// The total number of sectors on the disk. private static void WriteGPTHeader(byte[] gptData, GeometryMetadata metadata, int sectorSize, int partitionEntriesSectors, int numPartitionEntries, long diskSectors) { int headerOffset = sectorSize; // GPT header is at LBA 1 // Signature: "EFI PART" in little-endian BinaryPrimitives.WriteInt64LittleEndian(gptData.AsSpan(headerOffset + 0, 8), GptSignature); // Revision: 1.0 (0x00010000) BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 8, 4), GptRevision); // Header size: 92 bytes BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 12, 4), (uint)GptHeaderSize); // CRC32 of header (calculated later) - set to 0 for now BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 16, 4), 0u); // Reserved: must be 0 BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 20, 4), 0u); // Current LBA: 1 (this header is at LBA 1) BinaryPrimitives.WriteInt64LittleEndian(gptData.AsSpan(headerOffset + 24, 8), (long)1); // Backup LBA: last sector of disk long backupLba = diskSectors - 1; BinaryPrimitives.WriteInt64LittleEndian(gptData.AsSpan(headerOffset + 32, 8), backupLba); // First usable LBA: after partition entries long firstUsableLba = 2 + partitionEntriesSectors; BinaryPrimitives.WriteInt64LittleEndian(gptData.AsSpan(headerOffset + 40, 8), firstUsableLba); // Last usable LBA: before backup header long lastUsableLba = diskSectors - partitionEntriesSectors - 2; BinaryPrimitives.WriteInt64LittleEndian(gptData.AsSpan(headerOffset + 48, 8), lastUsableLba); // Disk GUID - generate new or use from metadata if available var diskGuid = Guid.NewGuid(); diskGuid.ToByteArray().CopyTo(gptData, headerOffset + 56); // Partition entry LBA: 2 (entries start at LBA 2) BinaryPrimitives.WriteInt64LittleEndian(gptData.AsSpan(headerOffset + 72, 8), (long)2); // Number of partition entries BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 80, 4), (uint)numPartitionEntries); // Size of partition entry: 128 bytes BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 84, 4), (uint)PartitionEntrySize); // CRC32 of partition entries (calculated later) BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 88, 4), 0u); // Calculate and write CRC32 of header uint headerCrc = Crc32.Calculate(gptData, headerOffset, GptHeaderSize); BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 16, 4), headerCrc); } /// /// Writes GPT partition entries. /// /// The GPT data buffer. /// The geometry metadata. /// The sector size in bytes. /// The number of sectors for partition entries. private static void WriteGPTPartitionEntries(byte[] gptData, GeometryMetadata metadata, int sectorSize, int partitionEntriesSectors) { int entriesOffset = 2 * sectorSize; // Entries start at LBA 2 if (metadata.Partitions == null) return; var gptPartitions = metadata.Partitions .Where(p => p.TableType == PartitionTableType.GPT) .OrderBy(p => p.Number) .Take(128) // GPT standard supports 128 entries .ToList(); // Calculate partition entries CRC32 var entriesData = new byte[partitionEntriesSectors * sectorSize]; for (int i = 0; i < gptPartitions.Count; i++) { var part = gptPartitions[i]; int entryOffset = i * PartitionEntrySize; WriteGPTPartitionEntry(entriesData, entryOffset, part, sectorSize); } // Copy entries to main buffer entriesData.CopyTo(gptData, entriesOffset); // Calculate and write CRC32 of partition entries to header uint entriesCrc = Crc32.Calculate(entriesData, 0, entriesData.Length); int headerOffset = sectorSize; BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 88, 4), entriesCrc); // Recalculate header CRC with updated partition entries CRC BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 16, 4), 0u); // Clear header CRC before calculation uint headerCrc = Crc32.Calculate(gptData, headerOffset, GptHeaderSize); BinaryPrimitives.WriteUInt32LittleEndian(gptData.AsSpan(headerOffset + 16, 4), headerCrc); } /// /// Writes a single GPT partition entry. /// /// The partition entries buffer. /// The offset in the buffer to write the entry. /// The partition geometry. /// The sector size in bytes. private static void WriteGPTPartitionEntry(byte[] entriesData, int offset, PartitionGeometry part, int sectorSize) { // Partition type GUID (16 bytes) var typeGuid = GptPartitionTypeGuids.ToGuid(part.Type); typeGuid.ToByteArray().CopyTo(entriesData, offset + 0); // Unique partition GUID (16 bytes) - use VolumeGuid if available, otherwise generate var uniqueGuid = part.VolumeGuid ?? Guid.NewGuid(); uniqueGuid.ToByteArray().CopyTo(entriesData, offset + 16); // Starting LBA (8 bytes) long startLba = part.StartOffset / sectorSize; BinaryPrimitives.WriteInt64LittleEndian(entriesData.AsSpan(offset + 32, 8), startLba); // Ending LBA (8 bytes) long sizeInSectors = part.Size / sectorSize; long endLba = startLba + sizeInSectors - 1; BinaryPrimitives.WriteInt64LittleEndian(entriesData.AsSpan(offset + 40, 8), endLba); // Attributes (8 bytes) - default to 0 BinaryPrimitives.WriteInt64LittleEndian(entriesData.AsSpan(offset + 48, 8), (long)0); // Partition name (72 bytes, UTF-16LE) string name = part.Name ?? ""; var nameBytes = Encoding.Unicode.GetBytes(name); int nameLength = Math.Min(nameBytes.Length, 72); Array.Copy(nameBytes, 0, entriesData, offset + 56, nameLength); // Pad remainder with zeros (already zeroed) } /// /// Writes the secondary (backup) GPT header and partition entries to the end of the disk. /// /// The raw disk to write to. /// The geometry metadata. /// The primary GPT data. /// Cancellation token. /// A task representing the asynchronous operation. public static async Task WriteSecondaryGPTAsync(IRawDisk targetDisk, GeometryMetadata geometryMetadata, byte[] primaryGptData, CancellationToken cancel) { if (targetDisk == null || geometryMetadata?.Disk == null) return; var sectorSize = geometryMetadata.Disk.SectorSize; var diskSectors = geometryMetadata.Disk.Sectors; // Calculate sizes int numPartitionEntries = 128; int partitionEntriesSize = numPartitionEntries * PartitionEntrySize; int partitionEntriesSectors = (partitionEntriesSize + sectorSize - 1) / sectorSize; // Secondary GPT layout: // - Partition entries (before header) // - Secondary GPT header (last sector) // Read primary header to get disk GUID and other fields int primaryHeaderOffset = sectorSize; var diskGuid = new byte[16]; Array.Copy(primaryGptData, primaryHeaderOffset + 56, diskGuid, 0, 16); // Read partition entries CRC from primary byte[] partitionEntriesCrcBytes = new byte[4]; Array.Copy(primaryGptData, primaryHeaderOffset + 88, partitionEntriesCrcBytes, 0, 4); // Create secondary header var secondaryHeader = new byte[GptHeaderSize]; // Signature: "EFI PART" BinaryPrimitives.WriteInt64LittleEndian(secondaryHeader.AsSpan(0, 8), GptSignature); // Revision: 1.0 BinaryPrimitives.WriteUInt32LittleEndian(secondaryHeader.AsSpan(8, 4), GptRevision); // Header size: 92 bytes BinaryPrimitives.WriteUInt32LittleEndian(secondaryHeader.AsSpan(12, 4), (uint)GptHeaderSize); // CRC32 (calculated later) BinaryPrimitives.WriteUInt32LittleEndian(secondaryHeader.AsSpan(16, 4), 0u); // Reserved BinaryPrimitives.WriteUInt32LittleEndian(secondaryHeader.AsSpan(20, 4), 0u); // Current LBA: last sector (backup header location) long secondaryHeaderLba = diskSectors - 1; BinaryPrimitives.WriteInt64LittleEndian(secondaryHeader.AsSpan(24, 8), secondaryHeaderLba); // Backup LBA: 1 (primary header location) BinaryPrimitives.WriteInt64LittleEndian(secondaryHeader.AsSpan(32, 8), (long)1); // First usable LBA long firstUsableLba = 2 + partitionEntriesSectors; BinaryPrimitives.WriteInt64LittleEndian(secondaryHeader.AsSpan(40, 8), firstUsableLba); // Last usable LBA long lastUsableLba = diskSectors - partitionEntriesSectors - 2; BinaryPrimitives.WriteInt64LittleEndian(secondaryHeader.AsSpan(48, 8), lastUsableLba); // Disk GUID (same as primary) diskGuid.CopyTo(secondaryHeader, 56); // Partition entry LBA: right before the secondary header long secondaryEntriesLba = diskSectors - partitionEntriesSectors - 1; BinaryPrimitives.WriteInt64LittleEndian(secondaryHeader.AsSpan(72, 8), secondaryEntriesLba); // Number of partition entries BinaryPrimitives.WriteUInt32LittleEndian(secondaryHeader.AsSpan(80, 4), (uint)numPartitionEntries); // Size of partition entry BinaryPrimitives.WriteUInt32LittleEndian(secondaryHeader.AsSpan(84, 4), (uint)PartitionEntrySize); // Partition entries CRC32 (same as primary) partitionEntriesCrcBytes.CopyTo(secondaryHeader, 88); // Calculate and write CRC32 of secondary header uint headerCrc = Crc32.Calculate(secondaryHeader, 0, GptHeaderSize); BinaryPrimitives.WriteUInt32LittleEndian(secondaryHeader.AsSpan(16, 4), headerCrc); // Write secondary partition entries (same as primary) long entriesStartOffset = 2 * sectorSize; int entriesByteSize = partitionEntriesSectors * sectorSize; var partitionEntries = new byte[entriesByteSize]; Array.Copy(primaryGptData, entriesStartOffset, partitionEntries, 0, entriesByteSize); long secondaryEntriesOffset = secondaryEntriesLba * sectorSize; await targetDisk.WriteBytesAsync(secondaryEntriesOffset, partitionEntries, cancel).ConfigureAwait(false); // Write secondary header at the last sector long secondaryHeaderOffset = secondaryHeaderLba * sectorSize; await targetDisk.WriteBytesAsync(secondaryHeaderOffset, secondaryHeader, cancel).ConfigureAwait(false); } }