From 71bc154303cac7925b77a2a288bb3d48dca43b94 Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Tue, 24 Feb 2026 11:37:03 +0100 Subject: [PATCH] Fixed partition parsing of APFS and HFSPlus systems --- .../DiskImage/MacOSDiskImageHelper.cs | 74 +++++++++++++++++-- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/Duplicati/UnitTest/DiskImage/MacOSDiskImageHelper.cs b/Duplicati/UnitTest/DiskImage/MacOSDiskImageHelper.cs index 7b3dcf418..4c0e80c08 100644 --- a/Duplicati/UnitTest/DiskImage/MacOSDiskImageHelper.cs +++ b/Duplicati/UnitTest/DiskImage/MacOSDiskImageHelper.cs @@ -101,19 +101,79 @@ namespace Duplicati.UnitTest.DiskImage { var output = RunProcess("diskutil", $"info /dev/{partition}"); - var mountLine = output - .Split('\n') - .FirstOrDefault(l => l.Trim().StartsWith("Mount Point:")) - ?? throw new InvalidOperationException($"Failed to get mount point for partition {partition} of disk {diskIdentifier}"); + // Check if this is an APFS Physical Store + var isApfsPhysicalStore = output.Contains("APFS Physical Store") || + output.Split('\n').Any(l => l.Trim().StartsWith("Partition Type:") && l.Contains("Apple_APFS")); - var mountPoint = mountLine.Split([':'], 2)[1].Trim(); - if (!string.IsNullOrEmpty(mountPoint) && mountPoint != "Not mounted") - mountPoints.Add(mountPoint); + if (isApfsPhysicalStore) + { + // For APFS, get the container and then get volumes from it + var containerLine = output + .Split('\n') + .FirstOrDefault(l => l.Trim().StartsWith("APFS Container:")); + + if (containerLine != null) + { + var containerId = containerLine.Split([':'], 2)[1].Trim(); + // Get volumes from the container (they are like disk5s1, disk5s2, etc.) + var containerVolumes = GetApfsContainerVolumes(containerId); + foreach (var volume in containerVolumes) + { + var volumeOutput = RunProcess("diskutil", $"info {volume}"); + var mountPoint = ExtractMountPoint(volumeOutput); + if (!string.IsNullOrEmpty(mountPoint)) + mountPoints.Add(mountPoint); + } + } + } + else + { + // Regular partition - extract mount point directly + var mountPoint = ExtractMountPoint(output); + if (!string.IsNullOrEmpty(mountPoint)) + mountPoints.Add(mountPoint); + } } return [.. mountPoints]; } + private string[] GetApfsContainerVolumes(string containerId) + { + var output = RunProcess("diskutil", $"list {containerId}"); + + // Parse volumes from container listing (format: disk5s1, disk5s2, etc.) + return output + .Split('\n') + .Select(x => x.Trim()) + .Where(x => x.StartsWith(containerId + "s", StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Split([' '], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()) + .Where(x => x is not null) + .Select(x => x!) + .ToArray(); + } + + private string? ExtractMountPoint(string diskutilInfoOutput) + { + // Look for "Mount Point:" line + var mountLine = diskutilInfoOutput + .Split('\n') + .FirstOrDefault(l => l.Trim().StartsWith("Mount Point:")); + + if (mountLine == null) + return null; + + var mountPoint = mountLine.Split([':'], 2)[1].Trim(); + + // Return null if not mounted or not applicable + if (string.IsNullOrEmpty(mountPoint) || + mountPoint == "Not mounted" || + mountPoint == "Not applicable (no file system)") + return null; + + return mountPoint; + } + /// public string[] InitializeDisk(string diskIdentifier, Proprietary.DiskImage.PartitionTableType tableType, (Proprietary.DiskImage.FileSystemType, long)[] partitions) {