451 lines
20 KiB
C#
451 lines
20 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="metadata">The geometry metadata containing partition table information.</param>
|
|
/// <returns>A byte array containing the synthesized partition table data, or null if no partition table is specified.</returns>
|
|
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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Synthesizes an MBR partition table from geometry metadata.
|
|
/// </summary>
|
|
/// <param name="metadata">The geometry metadata containing partition information.</param>
|
|
/// <returns>A byte array containing the synthesized MBR partition table.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a single MBR partition entry to the specified offset.
|
|
/// </summary>
|
|
/// <param name="mbrData">The MBR data buffer.</param>
|
|
/// <param name="offset">The offset in the buffer to write the entry.</param>
|
|
/// <param name="part">The partition geometry.</param>
|
|
/// <param name="sectorSize">The sector size in bytes.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Synthesizes a GPT partition table from geometry metadata.
|
|
/// </summary>
|
|
/// <param name="metadata">The geometry metadata containing partition information.</param>
|
|
/// <returns>A byte array containing the synthesized GPT partition table.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the protective MBR for GPT.
|
|
/// </summary>
|
|
/// <param name="gptData">The GPT data buffer.</param>
|
|
/// <param name="metadata">The geometry metadata.</param>
|
|
/// <param name="sectorSize">The sector size in bytes.</param>
|
|
/// <param name="diskSectors">The total number of sectors on the disk.</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the GPT header.
|
|
/// </summary>
|
|
/// <param name="gptData">The GPT data buffer.</param>
|
|
/// <param name="metadata">The geometry metadata.</param>
|
|
/// <param name="sectorSize">The sector size in bytes.</param>
|
|
/// <param name="partitionEntriesSectors">The number of sectors for partition entries.</param>
|
|
/// <param name="numPartitionEntries">The number of partition entries.</param>
|
|
/// <param name="diskSectors">The total number of sectors on the disk.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes GPT partition entries.
|
|
/// </summary>
|
|
/// <param name="gptData">The GPT data buffer.</param>
|
|
/// <param name="metadata">The geometry metadata.</param>
|
|
/// <param name="sectorSize">The sector size in bytes.</param>
|
|
/// <param name="partitionEntriesSectors">The number of sectors for partition entries.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a single GPT partition entry.
|
|
/// </summary>
|
|
/// <param name="entriesData">The partition entries buffer.</param>
|
|
/// <param name="offset">The offset in the buffer to write the entry.</param>
|
|
/// <param name="part">The partition geometry.</param>
|
|
/// <param name="sectorSize">The sector size in bytes.</param>
|
|
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)
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the secondary (backup) GPT header and partition entries to the end of the disk.
|
|
/// </summary>
|
|
/// <param name="targetDisk">The raw disk to write to.</param>
|
|
/// <param name="geometryMetadata">The geometry metadata.</param>
|
|
/// <param name="primaryGptData">The primary GPT data.</param>
|
|
/// <param name="cancel">Cancellation token.</param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
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);
|
|
}
|
|
}
|