// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Duplicati.Proprietary.DiskImage; using Duplicati.Proprietary.DiskImage.Disk; using Duplicati.Proprietary.DiskImage.General; #nullable enable namespace Duplicati.UnitTest.DiskImage { /// /// Helper methods for disk image unit tests. /// Provides common operations for creating, initializing, and managing disk images. /// internal static class DiskImageTestHelpers { private const long MiB = 1024 * 1024; /// /// Creates a raw disk interface for the specified disk identifier based on the current platform. /// /// The disk identifier (e.g., \\.\PhysicalDriveN on Windows, /dev/loopN on Linux). /// An initialized IRawDisk instance for the specified platform. /// Thrown when the current OS is not supported. internal static IRawDisk CreateRawDiskForIdentifier(string diskIdentifier) { if (OperatingSystem.IsWindows()) return new Duplicati.Proprietary.DiskImage.Disk.Windows(diskIdentifier); else if (OperatingSystem.IsLinux()) return new Duplicati.Proprietary.DiskImage.Disk.Linux(diskIdentifier); else if (OperatingSystem.IsMacOS()) return new Duplicati.Proprietary.DiskImage.Disk.Mac(diskIdentifier); else throw new PlatformNotSupportedException("Unsupported operating system."); } /// /// Creates and initializes a raw disk interface for the specified disk identifier. /// /// The disk identifier. /// Whether to open the disk in read-only mode. /// Cancellation token. /// An initialized IRawDisk instance. /// Thrown when the disk fails to initialize. internal static async Task CreateAndInitializeRawDiskAsync( IRawDisk rawDisk, bool readOnly = true, CancellationToken cancellationToken = default) { if (!await rawDisk.InitializeAsync(readOnly, cancellationToken)) throw new InvalidOperationException($"Failed to initialize raw disk"); } /// /// Creates a disk image file with the specified parameters. /// /// The disk image helper. /// The path where the disk image will be created. /// The size of the disk in bytes. /// The partition table type (GPT, MBR, etc.). /// Array of tuples specifying filesystem type and size for each partition. /// The disk identifier for the created disk. internal static string CreateDiskWithPartitions( IDiskImageHelper diskHelper, string imagePath, long size, PartitionTableType tableType, (FileSystemType, long)[] partitions) { var diskIdentifier = diskHelper.CreateDisk(imagePath, size); diskHelper.InitializeDisk(diskIdentifier, tableType, partitions); diskHelper.FlushDisk(diskIdentifier); diskHelper.Unmount(diskIdentifier); return diskIdentifier; } /// /// Creates a disk with two FAT32 partitions for testing. /// /// The disk image helper. /// The path where the disk image will be created. /// The partition table type (GPT or MBR). /// The size of each partition in bytes (0 for remaining space on second partition). /// The disk identifier for the created disk. internal static string CreateDiskWithTwoFat32Partitions( IDiskImageHelper diskHelper, string imagePath, PartitionTableType tableType, long partitionSize = 50 * MiB) { var totalSize = partitionSize > 0 ? partitionSize * 2 + (10 * MiB) : 110 * MiB; // Extra space for partition table return CreateDiskWithPartitions( diskHelper, imagePath, totalSize, tableType, [(FileSystemType.FAT32, partitionSize), (FileSystemType.FAT32, 0)]); } /// /// Fills a partition with well-known test data (repeating pattern of 0x00-0xFF). /// /// The raw disk interface. /// The starting offset of the partition in bytes. /// The size of the partition in bytes. /// Cancellation token. internal static async Task FillPartitionWithTestDataAsync( IRawDisk rawDisk, long partitionOffset, long partitionSize, CancellationToken cancellationToken = default) { const int bufferSize = 64 * 1024; // 64KB chunks var buffer = new byte[bufferSize]; long bytesWritten = 0; long currentOffset = partitionOffset; while (bytesWritten < partitionSize) { cancellationToken.ThrowIfCancellationRequested(); // Fill buffer with well-known pattern (repeating 0x00-0xFF) for (int i = 0; i < buffer.Length; i++) { buffer[i] = (byte)((bytesWritten + i) & 0xFF); } var remaining = partitionSize - bytesWritten; var toWrite = (int)Math.Min(bufferSize, remaining); await rawDisk.WriteBytesAsync(currentOffset, buffer.AsMemory(0, toWrite), cancellationToken); bytesWritten += toWrite; currentOffset += toWrite; } } /// /// Verifies that a partition contains the expected well-known test data pattern. /// /// The raw disk interface. /// The starting offset of the partition in bytes. /// The size of the partition in bytes. /// Cancellation token. /// True if the data matches the expected pattern; otherwise, false. internal static async Task VerifyPartitionTestDataAsync( IRawDisk rawDisk, long partitionOffset, long partitionSize, CancellationToken cancellationToken = default) { const int bufferSize = 64 * 1024; // 64KB chunks var buffer = new byte[bufferSize]; long bytesRead = 0; long currentOffset = partitionOffset; while (bytesRead < partitionSize) { cancellationToken.ThrowIfCancellationRequested(); var remaining = partitionSize - bytesRead; var toRead = (int)Math.Min(bufferSize, remaining); var actualRead = await rawDisk.ReadBytesAsync(currentOffset, buffer.AsMemory(0, toRead), cancellationToken); if (actualRead != toRead) return false; // Verify the pattern for (int i = 0; i < toRead; i++) { var expected = (byte)((bytesRead + i) & 0xFF); if (buffer[i] != expected) return false; } bytesRead += toRead; currentOffset += toRead; } return true; } /// /// Gets the file extension for disk images based on the current platform. /// /// The platform-specific disk image extension (vhdx, img, or dmg). internal static string GetPlatformDiskImageExtension() { if (OperatingSystem.IsWindows()) return "vhdx"; else if (OperatingSystem.IsLinux()) return "img"; else if (OperatingSystem.IsMacOS()) return "dmg"; else throw new PlatformNotSupportedException("Unsupported operating system."); } /// /// Safely unmounts a disk, catching any exceptions during cleanup. /// /// The disk image helper. /// The disk identifier to unmount. internal static void SafeUnmount(IDiskImageHelper diskHelper, string? diskIdentifier) { if (string.IsNullOrEmpty(diskIdentifier) || diskHelper == null) return; try { diskHelper.Unmount(diskIdentifier); } catch (Exception ex) { // Log but don't throw - this is cleanup code Console.WriteLine($"Warning: Failed to unmount disk {diskIdentifier}: {ex.Message}"); } } /// /// Safely disposes a raw disk and unmounts the underlying disk identifier. /// /// The raw disk to dispose (can be null). /// The disk image helper. /// The disk identifier to unmount. internal static void SafeDisposeRawDisk(IRawDisk? rawDisk, IDiskImageHelper diskHelper, string? diskIdentifier) { if (rawDisk != null) try { rawDisk.Dispose(); } catch (Exception ex) { Console.WriteLine($"Warning: Failed to dispose raw disk: {ex.Message}"); } SafeUnmount(diskHelper, diskIdentifier); } /// /// Deletes a file if it exists, catching any exceptions. /// /// The path to the file to delete. internal static void SafeDeleteFile(string? filePath) { if (string.IsNullOrEmpty(filePath)) return; try { if (File.Exists(filePath)) File.Delete(filePath); } catch (Exception ex) { Console.WriteLine($"Warning: Failed to delete file {filePath}: {ex.Message}"); } } } }