Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
42456f2
* screenshots refresh only on branches that change the UI
Hawkynt Oct 26, 2026
11bdfd2
+ ZIP method 95 (XZ): read and write
Hawkynt Oct 26, 2026
c01351b
* regenerate references
Hawkynt Oct 26, 2026
0edd488
# XAR: bzip2, lzma and xz members decoded and checked against the TOC
Hawkynt Oct 26, 2026
393d095
* regenerate references
Hawkynt Oct 26, 2026
3bcfeb8
# LHA: -lh2- and -lh3- decoded with their own decoders, writing them …
Hawkynt Oct 26, 2026
6493e5c
* regenerate references
Hawkynt Oct 26, 2026
e199a5f
# LHA: header levels 1/3, lh1, lzs, lz5, pm1, pm2 and lhx read as the…
Hawkynt Oct 26, 2026
fc95094
# PackIt and DiskDoubler: real on-disk formats, decode or refuse
Hawkynt Oct 26, 2026
2350d2a
* regenerate references
Hawkynt Oct 26, 2026
fd7e239
* FreeArc: real FreeArc 0.67 archives, read and write
Hawkynt Oct 26, 2026
dd3a154
* regenerate references
Hawkynt Oct 26, 2026
685d627
* NSIS: real installer format, solid and non-solid, read-only
Hawkynt Oct 26, 2026
7200477
* regenerate references
Hawkynt Oct 26, 2026
ffc73b3
+ PartitionedDisk write support, 4Kn GPT, BSD/Amiga/Sun/SGI labels, r…
Hawkynt Oct 26, 2026
2cb06be
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
ad124fa
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
fc8ce06
* regenerate references
Hawkynt Oct 26, 2026
1cfcb4f
# partition add counts in the disk's own sector size on 4Kn disks
Hawkynt Oct 26, 2026
dfd763c
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
c6d48b5
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
148a622
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
b171283
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
6afb631
* refresh the application screenshot
github-actions[bot] Oct 26, 2026
d69e707
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
6c6dd7b
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
f7ceb83
* refresh the archive browser screenshot
github-actions[bot] Oct 26, 2026
aa8748a
* record LHA decoder provenance and merge CI prerequisites
Hawkynt Oct 26, 2026
fd0f4d9
* record lhasa provenance and merge CI prerequisites
Hawkynt Oct 26, 2026
1c3d3f2
* record DiskDoubler provenance and merge CI prerequisites
Hawkynt Oct 26, 2026
05f41f3
* merge validated provenance prerequisites for FreeArc
Hawkynt Oct 26, 2026
32f1345
* merge validated provenance prerequisites for NSIS
Hawkynt Oct 26, 2026
6c11eee
* merge validated provenance prerequisites for partitioned disks
Hawkynt Oct 26, 2026
8f36f0b
* merge validated provenance prerequisites for 4Kn partition add
Hawkynt Oct 26, 2026
33bd2cc
* integrate logical-sector partition editing with current disk support
Hawkynt Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions Compression.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2592,15 +2592,16 @@ List all partitions in a disk image. Auto-detects the host container
});

var partitionAddImageArg = new Argument<FileInfo>("image") { Description = "Disk image to modify" };
var partitionAddStartOpt = new Option<long>("--start") { Description = "Start sector (LBA, multiplied by 512 internally)", Required = true };
var partitionAddLengthOpt = new Option<long>("--length") { Description = "Length in sectors", Required = true };
var partitionAddStartOpt = new Option<long>("--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<long>("--length") { Description = "Length in the disk's logical sectors", Required = true };
var partitionAddTypeOpt = new Option<string>("--type") { Description = "Partition type: Linux, Fat32Lba, NtfsExfat, EfiSystem, ExtendedLba, …", Required = true };
var partitionAddLabelOpt = new Option<string?>("--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
Expand All @@ -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}");
Expand Down
21 changes: 19 additions & 2 deletions Compression.Lib/PartitionOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ public sealed record PartitionInfo(
/// Snapshot returned by <see cref="List"/>: the detected scheme plus all
/// primary and logical entries currently on disk.
/// </summary>
/// <param name="SectorSize">Logical sector size the table counts in: 512, or 4096 on a 4Kn GPT disk.</param>
public sealed record PartitionListResult(
PartitionScheme Scheme,
IReadOnlyList<PartitionInfo> Partitions);
IReadOnlyList<PartitionInfo> Partitions,
int SectorSize = PartitionEditor.SectorSize);

/// <summary>
/// Lists all partitions in the given image.
Expand All @@ -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);
}

/// <summary>
/// <see cref="Add"/> with the range given in the disk's own logical sectors — 512 bytes, or
/// 4096 on a 4Kn GPT disk — as <c>fdisk</c>/<c>sgdisk</c> count them.
/// </summary>
/// <returns>The logical sector size the LBAs were counted in.</returns>
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;
}

/// <summary>
Expand Down
24 changes: 13 additions & 11 deletions Compression.NativeUI/Views/PartitionDialogs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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}).",
Expand Down
2 changes: 1 addition & 1 deletion Compression.NativeUI/Views/PartitionsWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
57 changes: 57 additions & 0 deletions Compression.Tests/Cli/PartitionCliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentOutOfRangeException>());
} finally {
if (File.Exists(path)) File.Delete(path);
}
}
}
Loading