diff --git a/Compression.CLI/Program.cs b/Compression.CLI/Program.cs index 950e91cc1..7d8090e98 100644 --- a/Compression.CLI/Program.cs +++ b/Compression.CLI/Program.cs @@ -2592,15 +2592,16 @@ List all partitions in a disk image. Auto-detects the host container }); var partitionAddImageArg = new Argument("image") { Description = "Disk image to modify" }; -var partitionAddStartOpt = new Option("--start") { Description = "Start sector (LBA, multiplied by 512 internally)", Required = true }; -var partitionAddLengthOpt = new Option("--length") { Description = "Length in sectors", Required = true }; +var partitionAddStartOpt = new Option("--start") { Description = "Start sector (LBA, in the disk's logical sectors: 512 B, or 4096 B on a 4Kn GPT disk)", Required = true }; +var partitionAddLengthOpt = new Option("--length") { Description = "Length in the disk's logical sectors", Required = true }; var partitionAddTypeOpt = new Option("--type") { Description = "Partition type: Linux, Fat32Lba, NtfsExfat, EfiSystem, ExtendedLba, …", Required = true }; var partitionAddLabelOpt = new Option("--label") { Description = "Partition label (GPT only)" }; var partitionAddCmd = new Command("add", """ Add a primary or logical partition. If an extended container exists and the range falls inside it, the new entry is added as a logical partition. - Sectors are 512 bytes each. + Sectors are the disk's logical sectors, as fdisk and sgdisk count them: + 512 bytes, or 4096 bytes on a 4Kn GPT disk. Example: cwb partition add disk.vhd --start 2048 --length 2048 --type Linux cwb partition add disk.img --start 4096 --length 1024 --type Fat32Lba --label boot @@ -2610,10 +2611,10 @@ cwb partition add disk.img --start 4096 --length 1024 --type Fat32Lba --label bo if (!image.Exists) { Console.Error.WriteLine($"File not found: {image.FullName}"); return 1; } try { var type = PartitionOperations.ParseType(ctx.GetValue(partitionAddTypeOpt)!); - var startBytes = ctx.GetValue(partitionAddStartOpt) * PartitionEditor.SectorSize; - var lengthBytes = ctx.GetValue(partitionAddLengthOpt) * PartitionEditor.SectorSize; - PartitionOperations.Add(image.FullName, startBytes, lengthBytes, type, ctx.GetValue(partitionAddLabelOpt)); - Console.WriteLine($"Added {type} partition: start={startBytes:N0} bytes, size={lengthBytes:N0} bytes."); + var startLba = ctx.GetValue(partitionAddStartOpt); + var lengthSectors = ctx.GetValue(partitionAddLengthOpt); + var sectorSize = PartitionOperations.AddBySectors(image.FullName, startLba, lengthSectors, type, ctx.GetValue(partitionAddLabelOpt)); + Console.WriteLine($"Added {type} partition: start={startLba * sectorSize:N0} bytes, size={lengthSectors * sectorSize:N0} bytes ({sectorSize}-byte sectors)."); return 0; } catch (Exception ex) { Console.Error.WriteLine($"Add failed: {ex.Message}"); diff --git a/Compression.Lib/PartitionOperations.cs b/Compression.Lib/PartitionOperations.cs index 141b36e37..de9fa2eee 100644 --- a/Compression.Lib/PartitionOperations.cs +++ b/Compression.Lib/PartitionOperations.cs @@ -43,9 +43,11 @@ public sealed record PartitionInfo( /// Snapshot returned by : the detected scheme plus all /// primary and logical entries currently on disk. /// + /// Logical sector size the table counts in: 512, or 4096 on a 4Kn GPT disk. public sealed record PartitionListResult( PartitionScheme Scheme, - IReadOnlyList Partitions); + IReadOnlyList Partitions, + int SectorSize = PartitionEditor.SectorSize); /// /// Lists all partitions in the given image. @@ -60,7 +62,22 @@ public static PartitionListResult List(string imagePath) { .Select(p => new PartitionInfo(p.Index, p.Source, p.StartOffset, p.Size, p.TypeCode, p.TypeName, p.Name, p.IsActive)) .ToList() .AsReadOnly(); - return new PartitionListResult(editor.Scheme, snapshot); + return new PartitionListResult(editor.Scheme, snapshot, editor.LogicalSectorSize); + } + + /// + /// with the range given in the disk's own logical sectors — 512 bytes, or + /// 4096 on a 4Kn GPT disk — as fdisk/sgdisk count them. + /// + /// The logical sector size the LBAs were counted in. + public static int AddBySectors( + string imagePath, long startLba, long lengthSectors, + PartitionType type, string? label = null) { + ArgumentOutOfRangeException.ThrowIfNegative(startLba); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(lengthSectors); + var sectorSize = List(imagePath).SectorSize; + Add(imagePath, checked(startLba * sectorSize), checked(lengthSectors * sectorSize), type, label); + return sectorSize; } /// diff --git a/Compression.NativeUI/Views/PartitionDialogs.cs b/Compression.NativeUI/Views/PartitionDialogs.cs index 5622e1bc0..254f9a8ba 100644 --- a/Compression.NativeUI/Views/PartitionDialogs.cs +++ b/Compression.NativeUI/Views/PartitionDialogs.cs @@ -14,6 +14,7 @@ internal sealed class AddPartitionDialog : Form { private const int LabelWidth = 120; private readonly long _diskBytes; + private readonly int _sectorSize; private readonly Label _heading = new(); private readonly TextBox _startLba = new(); private readonly TextBox _lengthSectors = new(); @@ -24,8 +25,9 @@ internal sealed class AddPartitionDialog : Form { private readonly Button _cancel = new() { Text = "Cancel", DialogResult = DialogResult.Cancel }; private readonly ToolTip _toolTips = new(); - public AddPartitionDialog(long diskBytes, bool isLogical) { + public AddPartitionDialog(long diskBytes, bool isLogical, int sectorSize = PartitionEditor.SectorSize) { this._diskBytes = diskBytes; + this._sectorSize = sectorSize; this.Text = "Add Partition"; this.ClientSize = new(460, 340); @@ -46,18 +48,18 @@ public AddPartitionDialog(long diskBytes, bool isLogical) { } this._type.SelectedItem = PartitionType.Linux; - // Start at sector 2048 (one mebibyte in), leaving room for the MBR, and suggest either the + // Start one mebibyte in (sector 2048, or 256 on a 4Kn disk), leaving room for the table, and suggest either the // first 64 MiB or whatever is left if the disk is smaller. - const int DefaultStartLba = 2048; - var totalLba = Math.Max(0, diskBytes / PartitionEditor.SectorSize); - var suggestedLength = Math.Max(1, Math.Min(64L * 1024 * 1024 / PartitionEditor.SectorSize, Math.Max(1, totalLba - DefaultStartLba))); + var defaultStartLba = 1024 * 1024 / sectorSize; + var totalLba = Math.Max(0, diskBytes / sectorSize); + var suggestedLength = Math.Max(1, Math.Min(64L * 1024 * 1024 / sectorSize, Math.Max(1, totalLba - defaultStartLba))); - this._startLba.Text = DefaultStartLba.ToString(); + this._startLba.Text = defaultStartLba.ToString(); this._lengthSectors.Text = suggestedLength.ToString(); - this._hint.Text = $"Disk size: {diskBytes:N0} bytes ({totalLba:N0} sectors of 512 B)."; + this._hint.Text = $"Disk size: {diskBytes:N0} bytes ({totalLba:N0} sectors of {sectorSize} B)."; - this._toolTips.SetToolTip(this._startLba, "Sector index from the start of the disk. Each sector is 512 bytes."); - this._toolTips.SetToolTip(this._lengthSectors, "Number of 512-byte sectors. Multiply by 512 to get bytes."); + this._toolTips.SetToolTip(this._startLba, $"Sector index from the start of the disk. Each sector is {sectorSize} bytes."); + this._toolTips.SetToolTip(this._lengthSectors, $"Number of {sectorSize}-byte sectors. Multiply by {sectorSize} to get bytes."); this._ok.Click += (_, _) => this.OnOk(); this._cancel.Click += (_, _) => this.Close(); @@ -110,8 +112,8 @@ private void OnOk() { return; } - var startBytes = startLba * PartitionEditor.SectorSize; - var lengthBytes = lengthLba * PartitionEditor.SectorSize; + var startBytes = startLba * this._sectorSize; + var lengthBytes = lengthLba * this._sectorSize; if (startBytes + lengthBytes > this._diskBytes) { MessageBox.Show(this, $"Partition end ({startBytes + lengthBytes:N0}) exceeds disk size ({this._diskBytes:N0}).", diff --git a/Compression.NativeUI/Views/PartitionsWindow.cs b/Compression.NativeUI/Views/PartitionsWindow.cs index 5005e789f..6f484e4c4 100644 --- a/Compression.NativeUI/Views/PartitionsWindow.cs +++ b/Compression.NativeUI/Views/PartitionsWindow.cs @@ -310,7 +310,7 @@ private void UpdateSelectionActions() { private void OnAdd(bool isLogical) { if (this._editor is not { } editor || this._guestStream is not { } guest) return; - var dialog = new AddPartitionDialog(guest.Length, isLogical); + var dialog = new AddPartitionDialog(guest.Length, isLogical, editor.LogicalSectorSize); if (dialog.ShowDialog(this) != DialogResult.OK) return; try { diff --git a/Compression.Tests/Cli/PartitionCliTests.cs b/Compression.Tests/Cli/PartitionCliTests.cs index d22ce552d..b06478637 100644 --- a/Compression.Tests/Cli/PartitionCliTests.cs +++ b/Compression.Tests/Cli/PartitionCliTests.cs @@ -267,4 +267,61 @@ public void Verify_OnBlankImage_ReportsNoScheme() { if (File.Exists(path)) File.Delete(path); } } + + // ── Logical sector size ───────────────────────────────────────────── + + private static string CreateEmptyGpt(int sectorSize, int sectors) { + var path = Path.Combine(Path.GetTempPath(), $"cwb-partcli-{Guid.NewGuid():N}.partcli-test"); + using (var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite)) { + fs.SetLength((long)sectorSize * sectors); + PartitionEditor.CreateEmpty(fs, PartitionScheme.Gpt, sectorSize); + } + return path; + } + + [TestCase(512, 2048L)] + [TestCase(4096, 256L)] + [Category("HappyPath")] + public void Given_GptDisk_When_AddedBySectors_Then_LbasCountInTheDisksOwnSectorSize(int sectorSize, long startLba) { + var path = CreateEmptyGpt(sectorSize, 4096); + try { + var used = PartitionOperations.AddBySectors(path, startLba, 1024, PartitionType.Linux, "data"); + + var result = PartitionOperations.List(path); + Assert.That(used, Is.EqualTo(sectorSize)); + Assert.That(result.SectorSize, Is.EqualTo(sectorSize)); + Assert.That(result.Partitions.Single().StartOffset, Is.EqualTo(startLba * sectorSize)); + Assert.That(result.Partitions.Single().Size, Is.EqualTo(1024L * sectorSize)); + } finally { + if (File.Exists(path)) File.Delete(path); + } + } + + [Test, Category("Boundary")] + public void Given_4KnGpt_When_AddedPastTheLastUsableSector_Then_RefusedAndTableUnchanged() { + var path = CreateEmptyGpt(4096, 1024); + try { + var before = File.ReadAllBytes(path); + // 1024 blocks: LastUsableLBA is 1024 - 4 entry blocks - 1 header - 1 = 1018. + Assert.That(() => PartitionOperations.AddBySectors(path, 256, 1018 - 256 + 2, PartitionType.Linux), + Throws.InvalidOperationException); + Assert.That(File.ReadAllBytes(path), Is.EqualTo(before)); + Assert.That(() => PartitionOperations.AddBySectors(path, 256, 1018 - 256 + 1, PartitionType.Linux), Throws.Nothing); + } finally { + if (File.Exists(path)) File.Delete(path); + } + } + + [TestCase(-1L, 8L)] + [TestCase(256L, 0L)] + [Category("ErrorPath")] + public void Given_NegativeStartOrEmptyLength_When_AddedBySectors_Then_Refused(long startLba, long length) { + var path = CreateEmptyGpt(4096, 1024); + try { + Assert.That(() => PartitionOperations.AddBySectors(path, startLba, length, PartitionType.Linux), + Throws.InstanceOf()); + } finally { + if (File.Exists(path)) File.Delete(path); + } + } }