From bb7549f8b6b79ae315ca8b91e46e4c3504d65dee Mon Sep 17 00:00:00 2001
From: S3cur3Th1sSh1t <27858067+S3cur3Th1sSh1t@users.noreply.github.com>
Date: Wed, 3 Jun 2026 08:03:36 +0000
Subject: [PATCH] Add Linux/.NET 8 cross-platform support
Retarget net48 -> net8.0 so Volumiser builds and runs as a native Linux
(and macOS) binary as well as on Windows, and fix the cross-platform
issues that surfaced:
- AWS EBS: the SDK's synchronous ListSnapshotBlocks/GetSnapshotBlock
overloads only exist on .NET Framework. Use the *Async variants so the
EBS snapshot path builds and runs on .NET 8.
- Local disk images: VirtualDisk.OpenDisk(path) routes through a file
locator that hardcodes a '\' separator and fails on Linux/macOS
(e.g. "/tmp\/disk.img"). Dispatch by extension to the per-format path
constructors instead. This also fixes multi-extent VMDKs (a descriptor
pointing at -flat/-s001 extents), which the stream constructor rejected
with "Unknown type" / "Only Monolithic Sparse ... via a stream".
- Listings and downloads: replace Path.GetFileName with a
separator-agnostic LeafName so in-disk ('\') paths yield clean leaf
names off-Windows (ls output and download target filenames).
- Drop the Windows-only dnMerge IL-merge package; use the built-in
PublishSingleFile for single-binary output.
Build for Linux:
dotnet publish -c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true
---
DiscUtils.Ebs/DiskExtent.cs | 4 +--
DiscUtils.Ebs/DiskImageFile.cs | 4 +--
DiscUtils.Ebs/EbsMappedStream.cs | 8 ++---
Program.cs | 53 +++++++++++++++++++++++++++-----
Volumiser.csproj | 7 ++---
5 files changed, 56 insertions(+), 20 deletions(-)
diff --git a/DiscUtils.Ebs/DiskExtent.cs b/DiscUtils.Ebs/DiskExtent.cs
index 4b32be1..48ce08d 100644
--- a/DiscUtils.Ebs/DiskExtent.cs
+++ b/DiscUtils.Ebs/DiskExtent.cs
@@ -22,11 +22,11 @@ public DiskExtent(DiskImageFile ebsDisk) {
while (response == null || response.NextToken != null) {
- response = EbsDisk.EbsClient.ListSnapshotBlocks(new ListSnapshotBlocksRequest {
+ response = EbsDisk.EbsClient.ListSnapshotBlocksAsync(new ListSnapshotBlocksRequest {
MaxResults = 500,
SnapshotId = EbsDisk.SnapshotId,
NextToken = response?.NextToken
- });
+ }).GetAwaiter().GetResult();
foreach(var block in response.Blocks) {
SnapshotBlocks[block.BlockIndex] = new Block(block.BlockIndex, block.BlockToken);
diff --git a/DiscUtils.Ebs/DiskImageFile.cs b/DiscUtils.Ebs/DiskImageFile.cs
index 2721292..7784e37 100644
--- a/DiscUtils.Ebs/DiskImageFile.cs
+++ b/DiscUtils.Ebs/DiskImageFile.cs
@@ -30,10 +30,10 @@ public DiskImageFile(string snapshotId, AWSCredentials credentials, RegionEndpoi
SnapshotId = snapshotId;
EbsClient = new AmazonEBSClient(credentials.GetCredentials().AccessKey, credentials.GetCredentials().SecretKey, region);
- var result = EbsClient.ListSnapshotBlocks(new ListSnapshotBlocksRequest() {
+ var result = EbsClient.ListSnapshotBlocksAsync(new ListSnapshotBlocksRequest() {
MaxResults = 1,
SnapshotId = snapshotId
- });
+ }).GetAwaiter().GetResult();
if (result.HttpStatusCode != HttpStatusCode.OK)
throw new AmazonEBSException($"Failed to query EBS block with error {result.HttpStatusCode}");
diff --git a/DiscUtils.Ebs/EbsMappedStream.cs b/DiscUtils.Ebs/EbsMappedStream.cs
index 6303b37..89aa785 100644
--- a/DiscUtils.Ebs/EbsMappedStream.cs
+++ b/DiscUtils.Ebs/EbsMappedStream.cs
@@ -45,12 +45,12 @@ public EbsMappedStream(string snapshotId, AWSCredentials credentials, RegionEndp
while (response == null || response.NextToken != null) {
- response = EbsClient.ListSnapshotBlocks(new ListSnapshotBlocksRequest {
+ response = EbsClient.ListSnapshotBlocksAsync(new ListSnapshotBlocksRequest {
MaxResults = 500,
SnapshotId = SnapshotId,
StartingBlockIndex = 0,
NextToken = response?.NextToken
- });
+ }).GetAwaiter().GetResult();
if (response.HttpStatusCode != HttpStatusCode.OK)
throw new AmazonEBSException($"Failed to query EBS block with error {response.HttpStatusCode}");
@@ -94,11 +94,11 @@ public override int Read(byte[] buffer, int offset, int count) {
void GetBlockData(Block block, byte[] buffer, int offset) {
- var result = EbsClient.GetSnapshotBlock(new GetSnapshotBlockRequest() {
+ var result = EbsClient.GetSnapshotBlockAsync(new GetSnapshotBlockRequest() {
BlockIndex = block.Index,
BlockToken = block.Token,
SnapshotId = SnapshotId
- });
+ }).GetAwaiter().GetResult();
if(result.HttpStatusCode != System.Net.HttpStatusCode.OK) {
throw new IOException($"Failed to read EBS block {block.Index} with HTTP error {result.HttpStatusCode}");
diff --git a/Program.cs b/Program.cs
index 78b1328..f93f181 100644
--- a/Program.cs
+++ b/Program.cs
@@ -49,7 +49,34 @@ static VirtualDisk GetEBSDiskImage(string snapshotId, string profile, string aws
}
static VirtualDisk GetLocalDiskImage(string path) {
- return VirtualDisk.OpenDisk(path, FileAccess.Read);
+
+ // DiscUtils 0.16.13's generic VirtualDisk.OpenDisk(path) routes through a
+ // file locator that hardcodes a Windows '\' path separator, which breaks on
+ // Linux/macOS (e.g. "/tmp\/disk.img"). The per-format path constructors use
+ // a working locator, so dispatch by extension to those instead. Using the
+ // path (not stream) constructor is important: multi-extent disks (e.g. a
+ // VMDK descriptor pointing at -flat/-s001 extents) need the locator to
+ // resolve their sibling files. The stream constructor only handles
+ // self-contained images and throws "Unknown type" / "Only Monolithic
+ // Sparse ... can be accessed via a stream" for split/flat disks.
+ var ext = Path.GetExtension(path).ToLowerInvariant();
+
+ switch (ext) {
+ case ".vhd":
+ return new DiscUtils.Vhd.Disk(path, FileAccess.Read);
+ case ".vhdx":
+ return new DiscUtils.Vhdx.Disk(path, FileAccess.Read);
+ case ".vmdk":
+ return new DiscUtils.Vmdk.Disk(path, FileAccess.Read);
+ case ".dmg":
+ // Dmg has no path constructor; it is always a single self-contained file.
+ return new DiscUtils.Dmg.Disk(
+ new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read),
+ Ownership.Dispose);
+ default:
+ // .img/.raw/.dd/.bin and anything else: treat as a raw sector image.
+ return new DiscUtils.Raw.Disk(path, FileAccess.Read);
+ }
}
@@ -160,19 +187,19 @@ static void Main(string[] args) {
foreach (var file in fs.GetDirectories(filePath)) {
var dirInfo = fs.GetDirectoryInfo(file);
- Console.WriteLine($"{dirInfo.LastWriteTimeUtc} {"DIR",-15} {Path.GetFileName(file)}");
+ Console.WriteLine($"{dirInfo.LastWriteTimeUtc} {"DIR",-15} {LeafName(file)}");
}
foreach (var file in fs.GetFiles(filePath)) {
var fileInfo = fs.GetFileInfo(file);
- Console.WriteLine($"{fileInfo.LastWriteTimeUtc} { $"{fileInfo.Length / 1024.0 / 1024.0: 0.00}MB",-16} {Path.GetFileName(file)}");
+ Console.WriteLine($"{fileInfo.LastWriteTimeUtc} { $"{fileInfo.Length / 1024.0 / 1024.0: 0.00}MB",-16} {LeafName(file)}");
}
} else {
var fileStream = fs.OpenFile(filePath, FileMode.Open, FileAccess.Read);
Console.WriteLine($"[+] Opened file with path {filePath} for with size: {fileStream.Length}");
- File.WriteAllBytes(Path.GetFileName(filePath), StreamUtilities.ReadExact(fileStream, (int)fileStream.Length));
+ File.WriteAllBytes(LeafName(filePath), StreamUtilities.ReadExact(fileStream, (int)fileStream.Length));
}
}
} else {
@@ -255,7 +282,7 @@ private static void UI_ItemChanged(string itemValue) {
static private async Task DownloadFile(string volumePath) {
using (var fileStream = CurrentFileSystem.OpenFile(volumePath, FileMode.Open, FileAccess.Read)) {
- using (var outputStream = new FileStream(Path.GetFileName(volumePath), FileMode.OpenOrCreate, FileAccess.ReadWrite)) {
+ using (var outputStream = new FileStream(LeafName(volumePath), FileMode.OpenOrCreate, FileAccess.ReadWrite)) {
byte[] buffer = new byte[512 * 1024];
long totalRead = 0;
@@ -340,6 +367,18 @@ private static void Ui_ItemSelected(string itemValue) {
}
}
+ // DiscUtils returns in-disk paths using the volume's separator (e.g. '\' for
+ // NTFS/FAT). Path.GetFileName only strips the host OS separator, so on Linux it
+ // leaves the whole "\Windows\..." path intact in listings and download output
+ // filenames. Take the leaf after either separator so behaviour matches Windows.
+ static string LeafName(string path) {
+ if (string.IsNullOrEmpty(path)) {
+ return path;
+ }
+ int idx = path.LastIndexOfAny(new[] { '\\', '/' });
+ return idx < 0 ? path : path.Substring(idx + 1);
+ }
+
static string GetHumanSize(long size) {
if (size < 1024) {
return $"{size}";
@@ -356,8 +395,8 @@ static string GetHumanSize(long size) {
private static void UpdateView() {
UI.UpdateCurrentPathItems(new string[] { ".." }
- .Concat(CurrentFileSystem.GetDirectories(CurrentPath).Select(pi => Path.GetFileName(pi))
- .Concat(CurrentFileSystem.GetFiles(CurrentPath)).Select(pi => Path.GetFileName(pi)))
+ .Concat(CurrentFileSystem.GetDirectories(CurrentPath).Select(pi => LeafName(pi))
+ .Concat(CurrentFileSystem.GetFiles(CurrentPath)).Select(pi => LeafName(pi)))
.ToList());
}
}
diff --git a/Volumiser.csproj b/Volumiser.csproj
index fcd4fa6..6ac896b 100644
--- a/Volumiser.csproj
+++ b/Volumiser.csproj
@@ -2,7 +2,8 @@
Exe
- net48
+ net8.0
+ true
@@ -11,10 +12,6 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-