5542 lines
245 KiB
C#
5542 lines
245 KiB
C#
// 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.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.UnitTest.DiskImage;
|
|
using Duplicati.Proprietary.DiskImage;
|
|
using Duplicati.Proprietary.DiskImage.Disk;
|
|
using Duplicati.Proprietary.DiskImage.Filesystem;
|
|
using Duplicati.Proprietary.DiskImage.General;
|
|
using Duplicati.Proprietary.DiskImage.Partition;
|
|
using NUnit.Framework;
|
|
using Assert = NUnit.Framework.Legacy.ClassicAssert;
|
|
using StringAssert = NUnit.Framework.Legacy.StringAssert;
|
|
|
|
#nullable enable
|
|
|
|
namespace Duplicati.UnitTest.DiskImage
|
|
{
|
|
/// <summary>
|
|
/// Unit tests for the DiskImage module's internal components.
|
|
/// These tests focus on individual component testing with a strong emphasis
|
|
/// on unaligned reads/writes and cross-platform compatibility.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("DiskImageUnit")]
|
|
[Platform("Win,MacOsX,Linux")]
|
|
public class DiskImageUnitTests : BasicSetupHelper
|
|
{
|
|
// Class-level read-only disks (set up once for entire test class)
|
|
private static IDiskImageHelper? s_diskHelper;
|
|
|
|
// GPT disk with 2 FAT32 partitions
|
|
private static string s_gptDiskPath = "";
|
|
private static string s_gptDiskIdentifier = "";
|
|
private static IRawDisk? s_gptRawDisk;
|
|
private static long s_gptPartition1Offset;
|
|
private static long s_gptPartition1Size;
|
|
private static long s_gptPartition2Offset;
|
|
private static long s_gptPartition2Size;
|
|
|
|
// MBR disk with 2 FAT32 partitions
|
|
private static string s_mbrDiskPath = "";
|
|
private static string s_mbrDiskIdentifier = "";
|
|
private static IRawDisk? s_mbrRawDisk;
|
|
private static long s_mbrPartition1Offset;
|
|
private static long s_mbrPartition1Size;
|
|
private static long s_mbrPartition2Offset;
|
|
private static long s_mbrPartition2Size;
|
|
|
|
// Writable disk for tests that need to write (re-initialized before each test)
|
|
private static string s_writableDiskPath = "";
|
|
private static string s_writableDiskIdentifier = "";
|
|
private static IRawDisk? s_writableRawDisk;
|
|
|
|
// Per-test instance members
|
|
private IRawDisk _writableRawDisk = null!;
|
|
|
|
private const long MiB = 1024 * 1024;
|
|
|
|
#region Class-level Setup and Teardown
|
|
|
|
/// <summary>
|
|
/// One-time setup for the entire test class.
|
|
/// Creates read-only GPT and MBR disks with 2 FAT32 partitions each.
|
|
/// </summary>
|
|
[OneTimeSetUp]
|
|
public void OneTimeSetUp()
|
|
{
|
|
base.BasicHelperSetUp();
|
|
|
|
// Create the appropriate disk image helper for the current platform
|
|
s_diskHelper = DiskImageHelperFactory.Create();
|
|
|
|
// Check for admin privileges
|
|
if (!s_diskHelper.HasRequiredPrivileges())
|
|
{
|
|
Assert.Ignore("DiskImage tests require administrator privileges");
|
|
}
|
|
|
|
var extension = DiskImageTestHelpers.GetPlatformDiskImageExtension();
|
|
|
|
// Create GPT disk with 2 FAT32 partitions
|
|
s_gptDiskPath = Path.Combine(BASEFOLDER, $"duplicati_gpt_class_test.{extension}");
|
|
s_gptDiskIdentifier = DiskImageTestHelpers.CreateDiskWithTwoFat32Partitions(
|
|
s_diskHelper, s_gptDiskPath, PartitionTableType.GPT, 50 * MiB);
|
|
|
|
// Get partition info from GPT disk
|
|
var gptPartitions = s_diskHelper.GetPartitions(s_gptDiskIdentifier);
|
|
if (gptPartitions.Length >= 2)
|
|
{
|
|
s_gptPartition1Offset = gptPartitions[0].StartOffset;
|
|
s_gptPartition1Size = gptPartitions[0].Size;
|
|
s_gptPartition2Offset = gptPartitions[1].StartOffset;
|
|
s_gptPartition2Size = gptPartitions[1].Size;
|
|
}
|
|
|
|
// Create and initialize raw disk for GPT (read-only)
|
|
s_gptRawDisk = DiskImageTestHelpers.CreateRawDiskForIdentifier(s_gptDiskIdentifier);
|
|
if (!s_gptRawDisk.InitializeAsync(true, CancellationToken.None).Result)
|
|
throw new InvalidOperationException("Failed to initialize GPT raw disk");
|
|
|
|
// Fill GPT partitions with well-known test data
|
|
DiskImageTestHelpers.FillPartitionWithTestDataAsync(s_gptRawDisk, s_gptPartition1Offset, s_gptPartition1Size, CancellationToken.None).Wait();
|
|
DiskImageTestHelpers.FillPartitionWithTestDataAsync(s_gptRawDisk, s_gptPartition2Offset, s_gptPartition2Size, CancellationToken.None).Wait();
|
|
s_diskHelper.FlushDisk(s_gptDiskIdentifier);
|
|
|
|
// Create MBR disk with 2 FAT32 partitions
|
|
s_mbrDiskPath = Path.Combine(BASEFOLDER, $"duplicati_mbr_class_test.{extension}");
|
|
s_mbrDiskIdentifier = DiskImageTestHelpers.CreateDiskWithTwoFat32Partitions(
|
|
s_diskHelper, s_mbrDiskPath, PartitionTableType.MBR, 50 * MiB);
|
|
|
|
// Get partition info from MBR disk
|
|
var mbrPartitions = s_diskHelper.GetPartitions(s_mbrDiskIdentifier);
|
|
if (mbrPartitions.Length >= 2)
|
|
{
|
|
s_mbrPartition1Offset = mbrPartitions[0].StartOffset;
|
|
s_mbrPartition1Size = mbrPartitions[0].Size;
|
|
s_mbrPartition2Offset = mbrPartitions[1].StartOffset;
|
|
s_mbrPartition2Size = mbrPartitions[1].Size;
|
|
}
|
|
|
|
// Create and initialize raw disk for MBR (read-only)
|
|
s_mbrRawDisk = DiskImageTestHelpers.CreateRawDiskForIdentifier(s_mbrDiskIdentifier);
|
|
if (!s_mbrRawDisk.InitializeAsync(true, CancellationToken.None).Result)
|
|
throw new InvalidOperationException("Failed to initialize MBR raw disk");
|
|
|
|
// Fill MBR partitions with well-known test data
|
|
DiskImageTestHelpers.FillPartitionWithTestDataAsync(s_mbrRawDisk, s_mbrPartition1Offset, s_mbrPartition1Size, CancellationToken.None).Wait();
|
|
DiskImageTestHelpers.FillPartitionWithTestDataAsync(s_mbrRawDisk, s_mbrPartition2Offset, s_mbrPartition2Size, CancellationToken.None).Wait();
|
|
s_diskHelper.FlushDisk(s_mbrDiskIdentifier);
|
|
|
|
// Create writable disk path (will be created per-test)
|
|
s_writableDiskPath = Path.Combine(BASEFOLDER, $"duplicati_writable_class_test.{extension}");
|
|
|
|
// Create new writable disk (100 MiB, uninitialized)
|
|
s_writableDiskIdentifier = s_diskHelper.CreateDisk(s_writableDiskPath, 100 * MiB);
|
|
s_diskHelper.Unmount(s_writableDiskIdentifier);
|
|
|
|
// Create raw disk interface (with write access)
|
|
s_writableRawDisk = DiskImageTestHelpers.CreateRawDiskForIdentifier(s_writableDiskIdentifier);
|
|
if (!s_writableRawDisk.InitializeAsync(true, CancellationToken.None).Result)
|
|
throw new InvalidOperationException("Failed to initialize writable raw disk");
|
|
}
|
|
|
|
/// <summary>
|
|
/// One-time teardown for the entire test class.
|
|
/// Cleans up all class-level disk resources.
|
|
/// </summary>
|
|
[OneTimeTearDown]
|
|
public void OneTimeTearDown()
|
|
{
|
|
// Dispose read-only raw disks
|
|
s_gptRawDisk?.Dispose();
|
|
s_mbrRawDisk?.Dispose();
|
|
s_writableRawDisk?.Dispose();
|
|
|
|
// Unmount and cleanup class-level disks
|
|
if (s_diskHelper != null)
|
|
{
|
|
DiskImageTestHelpers.SafeUnmount(s_diskHelper, s_gptDiskIdentifier);
|
|
DiskImageTestHelpers.SafeUnmount(s_diskHelper, s_mbrDiskIdentifier);
|
|
DiskImageTestHelpers.SafeUnmount(s_diskHelper, s_writableDiskIdentifier);
|
|
}
|
|
|
|
// Delete disk image files
|
|
DiskImageTestHelpers.SafeDeleteFile(s_gptDiskPath);
|
|
DiskImageTestHelpers.SafeDeleteFile(s_mbrDiskPath);
|
|
DiskImageTestHelpers.SafeDeleteFile(s_writableDiskPath);
|
|
|
|
s_diskHelper?.Dispose();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Per-test Setup and Teardown
|
|
|
|
/// <summary>
|
|
/// Sets up the test environment before each test that needs a writable disk.
|
|
/// </summary>
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
if (s_writableRawDisk != null)
|
|
{
|
|
_writableRawDisk = s_writableRawDisk;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans up after each test.
|
|
/// </summary>
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
// No longer unmounting per-test to improve performance
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IRawDisk Sector-Aligned Tests
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadSector_ReturnsNonEmptyData()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
|
|
var buffer = new byte[sectorSize];
|
|
var bytesRead = await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read 1 sector.");
|
|
bool hasData = buffer.Any(x => x != 0);
|
|
Assert.IsTrue(hasData, "Sector 0 should contain data.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadBytes_ReturnsData()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
using var stream = await s_gptRawDisk.ReadBytesAsync(sectorSize, sectorSize, CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, stream.Length, "Should have read the correct amount of bytes.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadBytesAsync_CallerProvidedBuffer()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
var buffer = new byte[sectorSize];
|
|
var bytesRead = await s_gptRawDisk.ReadBytesAsync(0, buffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read the correct amount of bytes.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteSectors_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var writeBuffer = new byte[sectorSize];
|
|
new Random().NextBytes(writeBuffer);
|
|
await _writableRawDisk.WriteSectorsAsync(1, writeBuffer, CancellationToken.None);
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(1, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read the correct amount of bytes.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteBytes_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var writeBuffer = new byte[sectorSize];
|
|
new Random().NextBytes(writeBuffer);
|
|
await _writableRawDisk.WriteBytesAsync(sectorSize, writeBuffer, CancellationToken.None);
|
|
using var readStream = await _writableRawDisk.ReadBytesAsync(sectorSize, sectorSize, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read the correct amount of bytes.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteBytes_Memory()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var writeBuffer = new byte[sectorSize];
|
|
new Random().NextBytes(writeBuffer);
|
|
await _writableRawDisk.WriteBytesAsync(sectorSize, writeBuffer.AsMemory(), CancellationToken.None);
|
|
using var readStream = await _writableRawDisk.ReadBytesAsync(sectorSize, sectorSize, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read the correct amount of bytes.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data should match.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IRawDisk Unaligned Read Tests
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadUnalignedOffset_ReturnsCorrectData()
|
|
{
|
|
// Use writable disk for this test
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern at sector 1
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)(i & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(1, writeBuffer, CancellationToken.None);
|
|
|
|
// Read at offset = sector_size + 1 (unaligned)
|
|
var offset = sectorSize + 1;
|
|
var length = sectorSize - 2;
|
|
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(offset, length, CancellationToken.None);
|
|
var readBuffer = new byte[length];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(length, bytesRead, "Should have read the requested length.");
|
|
for (int i = 0; i < length; i++)
|
|
Assert.AreEqual((byte)((i + 1) & 0xFF), readBuffer[i], $"Byte at position {i} should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadUnalignedLength_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern at sector 0
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)(i & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(0, writeBuffer, CancellationToken.None);
|
|
|
|
// Read with unaligned length (sectorSize - 1)
|
|
var length = sectorSize - 1;
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(0, length, CancellationToken.None);
|
|
var readBuffer = new byte[length];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(length, bytesRead, "Should have read the requested length.");
|
|
for (int i = 0; i < length; i++)
|
|
Assert.AreEqual((byte)(i & 0xFF), readBuffer[i], $"Byte at position {i} should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadShortLength_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern at sector 0
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)(i & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(0, writeBuffer, CancellationToken.None);
|
|
|
|
// Read short length
|
|
var length = sectorSize - 1;
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(0, length, CancellationToken.None);
|
|
var readBuffer = new byte[length];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(length, bytesRead, "Should have read the requested short length.");
|
|
for (int i = 0; i < length; i++)
|
|
Assert.AreEqual((byte)(i & 0xFF), readBuffer[i], $"Byte at position {i} should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadStraddlingSectors_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write different patterns at sectors 0 and 1
|
|
var sector0Data = new byte[sectorSize];
|
|
var sector1Data = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
{
|
|
sector0Data[i] = (byte)(i | 0xF0);
|
|
sector1Data[i] = (byte)(i | 0x0F);
|
|
}
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(0, sector0Data, CancellationToken.None);
|
|
await _writableRawDisk.WriteSectorsAsync(1, sector1Data, CancellationToken.None);
|
|
|
|
// Read at half-sector offset with full sector length
|
|
var offset = sectorSize / 2;
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(offset, sectorSize, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
|
|
// First half should be from end of sector 0
|
|
for (int i = 0; i < sectorSize / 2; i++)
|
|
{
|
|
var expected = (byte)((sectorSize / 2 + i) | 0xF0);
|
|
Assert.AreEqual(expected, readBuffer[i], $"Byte at position {i} should match sector 0 data.");
|
|
}
|
|
|
|
// Second half should be from start of sector 1
|
|
for (int i = sectorSize / 2; i < sectorSize; i++)
|
|
{
|
|
var expected = (byte)((i - sectorSize / 2) | 0x0F);
|
|
Assert.AreEqual(expected, readBuffer[i], $"Byte at position {i} should match sector 1 data.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadNearEndOfDisk_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
|
|
// Calculate the last sector and write data there
|
|
var lastSector = (diskSize / sectorSize) - 1;
|
|
var writeBuffer = new byte[sectorSize];
|
|
new Random().NextBytes(writeBuffer);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(lastSector, writeBuffer, CancellationToken.None);
|
|
|
|
// Read from near the end
|
|
var offset = diskSize - sectorSize;
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(offset, sectorSize, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read a full sector.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data should match what was written.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadUnalignedOffsetWithMemory_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern at sector 1
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)((i * 2) & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(1, writeBuffer, CancellationToken.None);
|
|
|
|
// Read at unaligned offset
|
|
var offset = sectorSize + 4;
|
|
var length = sectorSize - 8;
|
|
var readBuffer = new byte[length];
|
|
var bytesRead = await _writableRawDisk.ReadBytesAsync(offset, readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(length, bytesRead, "Should have read the requested length.");
|
|
for (int i = 0; i < length; i++)
|
|
Assert.AreEqual((byte)(((i + 4) * 2) & 0xFF), readBuffer[i], $"Byte at position {i} should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadUnalignedLengthWithMemory_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern at sector 0
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)((i + 100) & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(0, writeBuffer, CancellationToken.None);
|
|
|
|
// Read with unaligned length
|
|
var length = sectorSize - 5;
|
|
var readBuffer = new byte[length];
|
|
var bytesRead = await _writableRawDisk.ReadBytesAsync(0, readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(length, bytesRead, "Should have read the requested length.");
|
|
for (int i = 0; i < length; i++)
|
|
Assert.AreEqual((byte)((i + 100) & 0xFF), readBuffer[i], $"Byte at position {i} should match.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IRawDisk Unaligned Write Tests
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteUnalignedOffset_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// First, write known patterns to two consecutive sectors
|
|
var sector1Data = new byte[sectorSize];
|
|
var sector2Data = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
{
|
|
sector1Data[i] = (byte)(i | 0xA0);
|
|
sector2Data[i] = (byte)(i | 0xB0);
|
|
}
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(1, sector1Data, CancellationToken.None);
|
|
await _writableRawDisk.WriteSectorsAsync(2, sector2Data, CancellationToken.None);
|
|
|
|
// Now write at unaligned offset (sector_size + 5)
|
|
var unalignedOffset = sectorSize + 5;
|
|
var writeData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(unalignedOffset, writeData, CancellationToken.None);
|
|
|
|
Assert.AreEqual(writeData.Length, bytesWritten, "Should have written all bytes.");
|
|
|
|
// Read back both sectors and verify only the intended bytes were changed
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(1, 2, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize * 2];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize * 2, bytesRead, "Should have read two sectors.");
|
|
|
|
// Verify sector 1 data
|
|
for (int i = 0; i < 5; i++)
|
|
Assert.AreEqual((byte)(i | 0xA0), readBuffer[i], $"Byte at position {i} in sector 1 should be unchanged.");
|
|
for (int i = 0; i < writeData.Length; i++)
|
|
Assert.AreEqual(writeData[i], readBuffer[5 + i], $"Written byte at position {5 + i} should match.");
|
|
for (int i = 5 + writeData.Length; i < sectorSize; i++)
|
|
Assert.AreEqual((byte)(i | 0xA0), readBuffer[i], $"Byte at position {i} in sector 1 should be unchanged.");
|
|
|
|
// Verify sector 2 data (should be completely unchanged)
|
|
for (int i = 0; i < sectorSize; i++)
|
|
Assert.AreEqual((byte)(i | 0xB0), readBuffer[sectorSize + i], $"Byte at position {i} in sector 2 should be unchanged.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteUnalignedLength_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern to sector 3
|
|
var sectorData = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
sectorData[i] = (byte)(i | 0xC0);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(3, sectorData, CancellationToken.None);
|
|
|
|
// Write unaligned length (sectorSize - 10)
|
|
var writeLength = sectorSize - 10;
|
|
var writeData = new byte[writeLength];
|
|
for (int i = 0; i < writeLength; i++)
|
|
writeData[i] = (byte)((i + 50) & 0xFF);
|
|
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(3 * sectorSize, writeData, CancellationToken.None);
|
|
|
|
Assert.AreEqual(writeData.Length, bytesWritten, "Should have written all bytes.");
|
|
|
|
// Read back and verify
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(3, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
|
|
// First part should be our written data
|
|
for (int i = 0; i < writeLength; i++)
|
|
Assert.AreEqual((byte)((i + 50) & 0xFF), readBuffer[i], $"Byte at position {i} should match written data.");
|
|
|
|
// Remaining bytes should be unchanged
|
|
for (int i = writeLength; i < sectorSize; i++)
|
|
Assert.AreEqual((byte)(i | 0xC0), readBuffer[i], $"Byte at position {i} should be unchanged.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteSingleByte_VerifySingleByteChanged()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern to sector 5
|
|
var sectorData = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
sectorData[i] = (byte)(i & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(5, sectorData, CancellationToken.None);
|
|
|
|
// Write a single byte at offset 5 * sectorSize + 100
|
|
var byteOffset = 5 * sectorSize + 100;
|
|
var singleByte = new byte[] { 0xAB };
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(byteOffset, singleByte, CancellationToken.None);
|
|
|
|
Assert.AreEqual(1, bytesWritten, "Should have written 1 byte.");
|
|
|
|
// Read back the full sector
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(5, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
|
|
// Verify all bytes except position 100 are unchanged
|
|
for (int i = 0; i < sectorSize; i++)
|
|
if (i == 100)
|
|
Assert.AreEqual(0xAB, readBuffer[i], "Byte at position 100 should be the written value.");
|
|
else
|
|
Assert.AreEqual((byte)(i & 0xFF), readBuffer[i], $"Byte at position {i} should be unchanged.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteSpanningSectorBoundary_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known patterns to sectors 6 and 7
|
|
var sector6Data = new byte[sectorSize];
|
|
var sector7Data = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
{
|
|
sector6Data[i] = (byte)(i | 0xD0);
|
|
sector7Data[i] = (byte)(i | 0xE0);
|
|
}
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(6, sector6Data, CancellationToken.None);
|
|
await _writableRawDisk.WriteSectorsAsync(7, sector7Data, CancellationToken.None);
|
|
|
|
// Write spanning sector boundary: offset = sector_size - 4, length = 8
|
|
var spanOffset = 6 * sectorSize + sectorSize - 4;
|
|
var spanData = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 };
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(spanOffset, spanData, CancellationToken.None);
|
|
|
|
Assert.AreEqual(spanData.Length, bytesWritten, "Should have written all bytes.");
|
|
|
|
// Read back both sectors
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(6, 2, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize * 2];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize * 2, bytesRead, "Should have read two sectors.");
|
|
|
|
// Verify sector 6: last 4 bytes should be the first 4 of our written data
|
|
for (int i = 0; i < sectorSize - 4; i++)
|
|
Assert.AreEqual((byte)(i | 0xD0), readBuffer[i], $"Byte at position {i} in sector 6 should be unchanged.");
|
|
for (int i = 0; i < 4; i++)
|
|
Assert.AreEqual(spanData[i], readBuffer[sectorSize - 4 + i], $"Span byte at position {i} in sector 6 should match.");
|
|
|
|
// Verify sector 7: first 4 bytes should be the last 4 of our written data
|
|
for (int i = 0; i < 4; i++)
|
|
Assert.AreEqual(spanData[4 + i], readBuffer[sectorSize + i], $"Span byte at position {4 + i} in sector 7 should match.");
|
|
for (int i = 4; i < sectorSize; i++)
|
|
Assert.AreEqual((byte)(i | 0xE0), readBuffer[sectorSize + i], $"Byte at position {i} in sector 7 should be unchanged.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteAtLastSector_NoOverflow()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
|
|
// Calculate the last sector
|
|
var lastSector = (diskSize / sectorSize) - 1;
|
|
|
|
// Write a full sector at the last valid sector
|
|
var writeData = new byte[sectorSize];
|
|
new Random(42).NextBytes(writeData);
|
|
|
|
var bytesWritten = await _writableRawDisk.WriteSectorsAsync(lastSector, writeData, CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesWritten, "Should have written a full sector.");
|
|
|
|
// Read it back and verify
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(lastSector, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
Assert.AreEqual(writeData, readBuffer, "Data should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteUnalignedWithMemory_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern to sector 8
|
|
var sectorData = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
sectorData[i] = (byte)(i | 0xF0);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(8, sectorData, CancellationToken.None);
|
|
|
|
// Write at unaligned offset using Memory<byte>
|
|
var unalignedOffset = 8 * sectorSize + 10;
|
|
var writeData = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE };
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(unalignedOffset, writeData.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(writeData.Length, bytesWritten, "Should have written all bytes.");
|
|
|
|
// Read back and verify
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(8, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
|
|
// Verify data before written region
|
|
for (int i = 0; i < 10; i++)
|
|
Assert.AreEqual((byte)(i | 0xF0), readBuffer[i], $"Byte at position {i} should be unchanged.");
|
|
|
|
// Verify written data
|
|
for (int i = 0; i < writeData.Length; i++)
|
|
Assert.AreEqual(writeData[i], readBuffer[10 + i], $"Written byte at position {10 + i} should match.");
|
|
|
|
// Verify data after written region
|
|
for (int i = 10 + writeData.Length; i < sectorSize; i++)
|
|
Assert.AreEqual((byte)(i | 0xF0), readBuffer[i], $"Byte at position {i} should be unchanged.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PartitionTableFactory detection tests
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableFactory_GPTBytes_DetectsGPT()
|
|
{
|
|
var sectorSize = 512;
|
|
var diskBytes = new byte[sectorSize * 4];
|
|
|
|
// MBR (sector 0) - Protective MBR with GPT signature
|
|
diskBytes[510] = 0x55;
|
|
diskBytes[511] = 0xAA;
|
|
diskBytes[450] = 0xEE;
|
|
|
|
// GPT header (sector 1) - "EFI PART" signature
|
|
var gptSignature = "EFI PART"u8.ToArray();
|
|
Buffer.BlockCopy(gptSignature, 0, diskBytes, sectorSize, 8);
|
|
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be detected.");
|
|
Assert.AreEqual(PartitionTableType.GPT, partitionTable!.TableType, "Should detect GPT partition table.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableFactory_MBRBytes_DetectsMBR()
|
|
{
|
|
var sectorSize = 512;
|
|
var diskBytes = new byte[sectorSize];
|
|
|
|
diskBytes[510] = 0x55;
|
|
diskBytes[511] = 0xAA;
|
|
diskBytes[450] = 0x83;
|
|
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be detected.");
|
|
Assert.AreEqual(PartitionTableType.MBR, partitionTable!.TableType, "Should detect MBR partition table.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableFactory_InvalidBytes_ReturnsUnknown()
|
|
{
|
|
var sectorSize = 512;
|
|
var diskBytes = new byte[sectorSize];
|
|
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be created even for unknown type.");
|
|
Assert.AreEqual(PartitionTableType.Unknown, partitionTable!.TableType, "Should detect Unknown partition table for invalid bytes.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableFactory_ProtectiveMBRType_DetectsGPT()
|
|
{
|
|
var sectorSize = 512;
|
|
var diskBytes = new byte[sectorSize * 4];
|
|
|
|
diskBytes[510] = 0x55;
|
|
diskBytes[511] = 0xAA;
|
|
diskBytes[450] = 0xEE;
|
|
|
|
var gptSignature = "EFI PART"u8.ToArray();
|
|
Buffer.BlockCopy(gptSignature, 0, diskBytes, sectorSize, 8);
|
|
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be detected.");
|
|
Assert.AreEqual(PartitionTableType.GPT, partitionTable!.TableType, "Should detect GPT when protective MBR type (0xEE) is present.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableFactory_ProtectiveMBRWithoutGptHeader_FallsBackToMBR()
|
|
{
|
|
var sectorSize = 512;
|
|
var diskBytes = new byte[sectorSize * 2];
|
|
|
|
diskBytes[510] = 0x55;
|
|
diskBytes[511] = 0xAA;
|
|
diskBytes[450] = 0xEE;
|
|
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be detected.");
|
|
Assert.AreEqual(PartitionTableType.MBR, partitionTable!.TableType, "Should fall back to MBR when GPT header is invalid.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableFactory_GPTFromRealDisk_DetectsGPT()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
var sectorsToRead = 34;
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, sectorsToRead, CancellationToken.None);
|
|
var diskBytes = new byte[sectorSize * sectorsToRead];
|
|
await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be detected from real disk.");
|
|
Assert.AreEqual(PartitionTableType.GPT, partitionTable!.TableType, "Should detect GPT from real disk bytes.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GPT and MBR partition table parsing tests
|
|
|
|
[Test]
|
|
public async Task Test_GPT_ParseFromRealDisk_ReturnsCorrectPartitions()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
|
|
// Read enough sectors to include protective MBR, GPT header, and partition entries
|
|
var sectorsToRead = 34;
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, sectorsToRead, CancellationToken.None);
|
|
var diskBytes = new byte[sectorSize * sectorsToRead];
|
|
await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
// Parse the GPT from bytes
|
|
var gpt = new GPT(null);
|
|
var parsed = await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsTrue(parsed, "GPT should parse successfully from real disk bytes.");
|
|
|
|
// Verify partition count - we created 2 partitions
|
|
var partitions = new List<IPartition>();
|
|
await foreach (var partition in gpt.EnumeratePartitions(CancellationToken.None))
|
|
{
|
|
partitions.Add(partition);
|
|
}
|
|
|
|
Assert.AreEqual(2, partitions.Count, "Should have 2 partitions.");
|
|
|
|
// Verify partition properties
|
|
var firstPartition = partitions[0];
|
|
Assert.IsNotNull(firstPartition, "First partition should exist.");
|
|
Assert.AreEqual(1, firstPartition.PartitionNumber, "Partition number should be 1.");
|
|
Assert.Greater(firstPartition.StartOffset, 0, "StartOffset should be greater than 0.");
|
|
Assert.Greater(firstPartition.Size, 0, "Size should be greater than 0.");
|
|
|
|
// Verify sector alignment
|
|
Assert.AreEqual(0, firstPartition.StartOffset % sectorSize,
|
|
"StartOffset should be sector-aligned.");
|
|
Assert.AreEqual(0, firstPartition.Size % sectorSize,
|
|
"Size should be sector-aligned.");
|
|
|
|
gpt.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_MBR_ParseFromRealDisk_ReturnsCorrectPartitions()
|
|
{
|
|
var sectorSize = s_mbrRawDisk!.SectorSize;
|
|
|
|
// Read the MBR sector (first 512 bytes)
|
|
using var stream = await s_mbrRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
|
|
var mbrBytes = new byte[sectorSize];
|
|
await stream.ReadAtLeastAsync(mbrBytes, mbrBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
// Parse the MBR from bytes
|
|
var mbr = new MBR(null);
|
|
var parsed = await mbr.ParseAsync(mbrBytes, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsTrue(parsed, "MBR should parse successfully from real disk bytes.");
|
|
|
|
// Verify partition entries - we created 2 partitions
|
|
Assert.AreEqual(2, mbr.NumPartitionEntries, "Should have 2 partition entries.");
|
|
|
|
// Verify via EnumeratePartitions
|
|
var partitions = new List<IPartition>();
|
|
await foreach (var partition in mbr.EnumeratePartitions(CancellationToken.None))
|
|
{
|
|
partitions.Add(partition);
|
|
}
|
|
|
|
Assert.AreEqual(2, partitions.Count, "Should enumerate 2 partitions.");
|
|
|
|
// Verify partition properties
|
|
var firstPartition = partitions[0];
|
|
Assert.IsNotNull(firstPartition, "First partition should exist.");
|
|
Assert.AreEqual(1, firstPartition.PartitionNumber, "Partition number should be 1.");
|
|
Assert.Greater(firstPartition.StartOffset, 0, "StartOffset should be greater than 0.");
|
|
Assert.Greater(firstPartition.Size, 0, "Size should be greater than 0.");
|
|
|
|
// Verify sector alignment
|
|
Assert.AreEqual(0, firstPartition.StartOffset % sectorSize,
|
|
"StartOffset should be sector-aligned.");
|
|
Assert.AreEqual(0, firstPartition.Size % sectorSize,
|
|
"Size should be sector-aligned.");
|
|
|
|
mbr.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_GPT_EnumeratePartitions_ReturnsCorrectCount()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, 34, CancellationToken.None);
|
|
var diskBytes = new byte[sectorSize * 34];
|
|
await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
var gpt = new GPT(null);
|
|
await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
var count = 0;
|
|
await foreach (var partition in gpt.EnumeratePartitions(CancellationToken.None))
|
|
{
|
|
count++;
|
|
Assert.IsNotNull(partition, "Partition should not be null.");
|
|
Assert.Greater(partition.PartitionNumber, 0, "Partition number should be positive.");
|
|
}
|
|
|
|
Assert.AreEqual(2, count, "Should enumerate exactly 2 partitions.");
|
|
|
|
gpt.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_MBR_EnumeratePartitions_ReturnsCorrectCount()
|
|
{
|
|
var sectorSize = s_mbrRawDisk!.SectorSize;
|
|
|
|
using var stream = await s_mbrRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
|
|
var mbrBytes = new byte[sectorSize];
|
|
await stream.ReadAtLeastAsync(mbrBytes, mbrBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
var mbr = new MBR(null);
|
|
await mbr.ParseAsync(mbrBytes, sectorSize, CancellationToken.None);
|
|
|
|
var count = 0;
|
|
await foreach (var partition in mbr.EnumeratePartitions(CancellationToken.None))
|
|
{
|
|
count++;
|
|
Assert.IsNotNull(partition, "Partition should not be null.");
|
|
Assert.Greater(partition.PartitionNumber, 0, "Partition number should be positive.");
|
|
}
|
|
|
|
Assert.AreEqual(2, count, "Should enumerate exactly 2 partitions.");
|
|
|
|
mbr.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_GPT_GetPartitionAsync_ReturnsCorrectPartition()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, 34, CancellationToken.None);
|
|
var diskBytes = new byte[sectorSize * 34];
|
|
await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
var gpt = new GPT(null);
|
|
await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
// Test GetPartitionAsync for partition 1 (should exist)
|
|
var partition1 = await gpt.GetPartitionAsync(1, CancellationToken.None);
|
|
Assert.IsNotNull(partition1, "Partition 1 should exist.");
|
|
Assert.AreEqual(1, partition1!.PartitionNumber, "Partition number should be 1.");
|
|
|
|
// Test GetPartitionAsync for partition 2 (should exist)
|
|
var partition2 = await gpt.GetPartitionAsync(2, CancellationToken.None);
|
|
Assert.IsNotNull(partition2, "Partition 2 should exist.");
|
|
Assert.AreEqual(2, partition2!.PartitionNumber, "Partition number should be 2.");
|
|
|
|
// Test GetPartitionAsync for partition 3 (should not exist)
|
|
var partition3 = await gpt.GetPartitionAsync(3, CancellationToken.None);
|
|
Assert.IsNull(partition3, "Partition 3 should not exist.");
|
|
|
|
// Test GetPartitionAsync for invalid partition number 0
|
|
var partition0 = await gpt.GetPartitionAsync(0, CancellationToken.None);
|
|
Assert.IsNull(partition0, "Partition 0 should not exist (invalid number).");
|
|
|
|
gpt.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_MBR_GetPartitionAsync_ReturnsCorrectPartition()
|
|
{
|
|
var sectorSize = s_mbrRawDisk!.SectorSize;
|
|
|
|
using var stream = await s_mbrRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
|
|
var mbrBytes = new byte[sectorSize];
|
|
await stream.ReadAtLeastAsync(mbrBytes, mbrBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
var mbr = new MBR(null);
|
|
await mbr.ParseAsync(mbrBytes, sectorSize, CancellationToken.None);
|
|
|
|
// Test GetPartitionAsync for partition 1 (should exist)
|
|
var partition1 = await mbr.GetPartitionAsync(1, CancellationToken.None);
|
|
Assert.IsNotNull(partition1, "Partition 1 should exist.");
|
|
Assert.AreEqual(1, partition1!.PartitionNumber, "Partition number should be 1.");
|
|
|
|
// Test GetPartitionAsync for partition 2 (should exist)
|
|
var partition2 = await mbr.GetPartitionAsync(2, CancellationToken.None);
|
|
Assert.IsNotNull(partition2, "Partition 2 should exist.");
|
|
Assert.AreEqual(2, partition2!.PartitionNumber, "Partition number should be 2.");
|
|
|
|
// Test GetPartitionAsync for partition 3 (should not exist)
|
|
var partition3 = await mbr.GetPartitionAsync(3, CancellationToken.None);
|
|
Assert.IsNull(partition3, "Partition 3 should not exist.");
|
|
|
|
// Test GetPartitionAsync for invalid partition number 0
|
|
var partition0 = await mbr.GetPartitionAsync(0, CancellationToken.None);
|
|
Assert.IsNull(partition0, "Partition 0 should not exist (invalid number).");
|
|
|
|
mbr.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_GPT_PartitionAlignment_IsSectorAligned()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, 34, CancellationToken.None);
|
|
var diskBytes = new byte[sectorSize * 34];
|
|
await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
var gpt = new GPT(null);
|
|
await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
|
|
// Verify all partitions are sector-aligned
|
|
await foreach (var partition in gpt.EnumeratePartitions(CancellationToken.None))
|
|
{
|
|
Assert.AreEqual(0, partition.StartOffset % sectorSize,
|
|
$"Partition {partition.PartitionNumber} StartOffset ({partition.StartOffset}) should be sector-aligned (sector size: {sectorSize}).");
|
|
Assert.AreEqual(0, partition.Size % sectorSize,
|
|
$"Partition {partition.PartitionNumber} Size ({partition.Size}) should be sector-aligned (sector size: {sectorSize}).");
|
|
}
|
|
|
|
gpt.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_MBR_PartitionAlignment_IsSectorAligned()
|
|
{
|
|
var sectorSize = s_mbrRawDisk!.SectorSize;
|
|
|
|
using var stream = await s_mbrRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
|
|
var mbrBytes = new byte[sectorSize];
|
|
await stream.ReadAtLeastAsync(mbrBytes, mbrBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
var mbr = new MBR(null);
|
|
await mbr.ParseAsync(mbrBytes, sectorSize, CancellationToken.None);
|
|
|
|
// Verify all partitions are sector-aligned
|
|
await foreach (var partition in mbr.EnumeratePartitions(CancellationToken.None))
|
|
{
|
|
Assert.AreEqual(0, partition.StartOffset % sectorSize,
|
|
$"Partition {partition.PartitionNumber} StartOffset ({partition.StartOffset}) should be sector-aligned (sector size: {sectorSize}).");
|
|
Assert.AreEqual(0, partition.Size % sectorSize,
|
|
$"Partition {partition.PartitionNumber} Size ({partition.Size}) should be sector-aligned (sector size: {sectorSize}).");
|
|
}
|
|
|
|
mbr.Dispose();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PartitionWriteStream tests
|
|
|
|
[Test]
|
|
public async Task Test_PartitionWriteStream_WriteAndDispose_DataFlushedToDisk()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 10 * sectorSize;
|
|
var partitionSize = 4 * sectorSize;
|
|
|
|
// Write data using PartitionWriteStream
|
|
var writeData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
|
|
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
|
|
await stream.WriteAsync(writeData, CancellationToken.None);
|
|
|
|
// Read back and verify data was flushed to disk
|
|
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, writeData.Length, CancellationToken.None);
|
|
var readBuffer = new byte[writeData.Length];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(writeData.Length, bytesRead, "Should have read the written data.");
|
|
Assert.AreEqual(writeData, readBuffer, "Data should match what was written.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_SeekBegin_ValidPosition()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 4 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Test Seek with SeekOrigin.Begin
|
|
var newPosition = stream.Seek(100, SeekOrigin.Begin);
|
|
Assert.AreEqual(100, newPosition, "Position should be 100 after seeking from Begin.");
|
|
Assert.AreEqual(100, stream.Position, "Stream.Position should match returned position.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_SeekCurrent_ValidPosition()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 4 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Write some data first to move position
|
|
stream.Write([0x01, 0x02, 0x03], 0, 3);
|
|
Assert.AreEqual(3, stream.Position, "Position should be 3 after writing 3 bytes.");
|
|
|
|
// Test Seek with SeekOrigin.Current
|
|
var newPosition = stream.Seek(50, SeekOrigin.Current);
|
|
Assert.AreEqual(53, newPosition, "Position should be 53 after seeking 50 from current position 3.");
|
|
Assert.AreEqual(53, stream.Position, "Stream.Position should match returned position.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_SeekEnd_ValidPosition()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 4 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Write some data first to establish length
|
|
stream.Write([0x01, 0x02, 0x03, 0x04, 0x05], 0, 5);
|
|
Assert.AreEqual(5, stream.Length, "Length should be 5 after writing 5 bytes.");
|
|
|
|
// Test Seek with SeekOrigin.End
|
|
var newPosition = stream.Seek(-2, SeekOrigin.End);
|
|
Assert.AreEqual(3, newPosition, "Position should be 3 after seeking -2 from end of length 5.");
|
|
Assert.AreEqual(3, stream.Position, "Stream.Position should match returned position.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_WriteBeyondSize_ThrowsIOException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 10 * sectorSize;
|
|
var partitionSize = 2 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize);
|
|
|
|
// Try to write more than partition size
|
|
var largeBuffer = new byte[partitionSize + 100];
|
|
new Random().NextBytes(largeBuffer);
|
|
|
|
var ex = Assert.Throws<IOException>(() =>
|
|
{
|
|
stream.Write(largeBuffer, 0, largeBuffer.Length);
|
|
}, "Writing beyond partition size should throw IOException.");
|
|
|
|
StringAssert.Contains("partition size", ex!.Message, "Exception message should mention partition size.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_SeekBeyondSize_ThrowsIOException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 2 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Try to seek beyond partition size
|
|
var ex = Assert.Throws<IOException>(() =>
|
|
{
|
|
stream.Seek(partitionSize + 100, SeekOrigin.Begin);
|
|
}, "Seeking beyond partition size should throw IOException.");
|
|
|
|
StringAssert.Contains("partition size", ex!.Message, "Exception message should mention partition size.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_SeekNegativePosition_ThrowsIOException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 2 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Try to seek to negative position
|
|
Assert.Throws<IOException>(() =>
|
|
{
|
|
stream.Seek(-100, SeekOrigin.Begin);
|
|
}, "Seeking to negative position should throw IOException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionWriteStream_WriteAtUnalignedPosition_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 15 * sectorSize;
|
|
var partitionSize = 4 * sectorSize;
|
|
|
|
// Write data at various unaligned positions
|
|
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
|
|
{
|
|
// Write at position 0
|
|
stream.Write([0xAA, 0xBB], 0, 2);
|
|
|
|
// Seek to unaligned position
|
|
stream.Seek(7, SeekOrigin.Begin);
|
|
stream.Write([0xCC, 0xDD, 0xEE], 0, 3);
|
|
|
|
// Seek to another unaligned position
|
|
stream.Seek(20, SeekOrigin.Begin);
|
|
stream.Write([0x11, 0x22, 0x33, 0x44], 0, 4);
|
|
}
|
|
|
|
// Read back and verify
|
|
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 25, CancellationToken.None);
|
|
var readBuffer = new byte[25];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(25, bytesRead, "Should read exactly 25 bytes.");
|
|
|
|
// Verify data at position 0
|
|
Assert.AreEqual(0xAA, readBuffer[0], "Byte at position 0 should be 0xAA.");
|
|
Assert.AreEqual(0xBB, readBuffer[1], "Byte at position 1 should be 0xBB.");
|
|
|
|
// Verify data at position 7
|
|
Assert.AreEqual(0xCC, readBuffer[7], "Byte at position 7 should be 0xCC.");
|
|
Assert.AreEqual(0xDD, readBuffer[8], "Byte at position 8 should be 0xDD.");
|
|
Assert.AreEqual(0xEE, readBuffer[9], "Byte at position 9 should be 0xEE.");
|
|
|
|
// Verify data at position 20
|
|
Assert.AreEqual(0x11, readBuffer[20], "Byte at position 20 should be 0x11.");
|
|
Assert.AreEqual(0x22, readBuffer[21], "Byte at position 21 should be 0x22.");
|
|
Assert.AreEqual(0x33, readBuffer[22], "Byte at position 22 should be 0x33.");
|
|
Assert.AreEqual(0x44, readBuffer[23], "Byte at position 23 should be 0x44.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionWriteStream_WriteMultipleTimes_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 20 * sectorSize;
|
|
var partitionSize = 3 * sectorSize;
|
|
|
|
// Write data in multiple operations
|
|
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
|
|
{
|
|
// First write
|
|
stream.Write([0x01, 0x02, 0x03], 0, 3);
|
|
|
|
// Second write (should append)
|
|
stream.Write([0x04, 0x05], 0, 2);
|
|
|
|
// Third write
|
|
stream.Write([0x06, 0x07, 0x08, 0x09], 0, 4);
|
|
}
|
|
|
|
// Read back and verify all data was written
|
|
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 9, CancellationToken.None);
|
|
var readBuffer = new byte[9];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(9, bytesRead, "Should read exactly 9 bytes.");
|
|
|
|
var expected = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 };
|
|
Assert.AreEqual(expected, readBuffer, "All written data should match.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_SetLengthBeyondMaxSize_ThrowsIOException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 2 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Try to set length beyond partition size
|
|
var ex = Assert.Throws<IOException>(() =>
|
|
{
|
|
stream.SetLength(partitionSize + 100);
|
|
}, "Setting length beyond partition size should throw IOException.");
|
|
|
|
StringAssert.Contains("partition size", ex!.Message, "Exception message should mention partition size.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionWriteStream_SetLengthWithinMaxSize_SetsLength()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 10 * sectorSize;
|
|
var partitionSize = 2 * sectorSize;
|
|
|
|
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
|
|
{
|
|
// Set length to a specific value within bounds
|
|
stream.SetLength(100);
|
|
Assert.AreEqual(100, stream.Length, "Length should be set to 100.");
|
|
|
|
// Write some data at position 0
|
|
stream.Write([0xAA, 0xBB, 0xCC], 0, 3);
|
|
// Length should be max(SetLength(100), written position) = 100
|
|
Assert.AreEqual(100, stream.Length, "Length should remain 100 (SetLength value).");
|
|
}
|
|
|
|
// Verify the 3 bytes were written at position 0 and length 100 bytes are flushed
|
|
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 100, CancellationToken.None);
|
|
var readBuffer = new byte[100];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(100, bytesRead, "Should read exactly 100 bytes.");
|
|
|
|
Assert.AreEqual(0xAA, readBuffer[0], "Byte at position 0 should be 0xAA.");
|
|
Assert.AreEqual(0xBB, readBuffer[1], "Byte at position 1 should be 0xBB.");
|
|
Assert.AreEqual(0xCC, readBuffer[2], "Byte at position 2 should be 0xCC.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_CanRead_ReturnsFalse()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
|
|
|
|
Assert.IsFalse(stream.CanRead, "CanRead should be false.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_CanWrite_ReturnsTrue()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
|
|
|
|
Assert.IsTrue(stream.CanWrite, "CanWrite should be true.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_CanSeek_ReturnsTrue()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
|
|
|
|
Assert.IsTrue(stream.CanSeek, "CanSeek should be true.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_Read_ThrowsNotSupported()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
|
|
|
|
Assert.Throws<NotSupportedException>(() =>
|
|
{
|
|
var _ = stream.Read(new byte[10], 0, 10);
|
|
}, "Read should throw NotSupportedException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionWriteStream_WriteEmptyData_NoFlush()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 10 * sectorSize;
|
|
var partitionSize = sectorSize;
|
|
|
|
// Create stream but write nothing
|
|
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
|
|
// Don't write anything
|
|
Assert.AreEqual(0, stream.Length, "Length should be 0 when nothing is written.");
|
|
|
|
// Read back and verify no data was flushed (should be zeros)
|
|
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 10, CancellationToken.None);
|
|
var readBuffer = new byte[10];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(10, bytesRead, "Should read 10 bytes.");
|
|
|
|
// All bytes should be 0 (uninitialized disk)
|
|
for (int i = 0; i < readBuffer.Length; i++)
|
|
{
|
|
Assert.AreEqual(0, readBuffer[i], $"Byte at position {i} should be 0 (no data written).");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PartitionTableSynthesizer tests
|
|
|
|
[Test]
|
|
public void Test_PartitionTableSynthesizer_SynthesizeMBR_ContainsValidBootSignature()
|
|
{
|
|
// Create a GeometryMetadata with known partition info
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry
|
|
{
|
|
SectorSize = 512,
|
|
Size = 100 * MiB,
|
|
Sectors = (int)(100 * MiB / 512)
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.MBR,
|
|
SectorSize = 512
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = 512 * 2048, // Start at sector 2048
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.MBR
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 2,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = 512 * 2048 + 20 * MiB,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.MBR
|
|
}
|
|
]
|
|
};
|
|
|
|
// Synthesize MBR
|
|
var mbrData = PartitionTableSynthesizer.SynthesizeMBR(metadata);
|
|
|
|
Assert.IsNotNull(mbrData, "MBR data should not be null.");
|
|
Assert.GreaterOrEqual(mbrData.Length, 512, "MBR should be at least one sector.");
|
|
|
|
// Verify boot signature at offset 510-511 (0x55 0xAA)
|
|
Assert.AreEqual(0x55, mbrData[510], "Boot signature first byte should be 0x55.");
|
|
Assert.AreEqual(0xAA, mbrData[511], "Boot signature second byte should be 0xAA.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionTableSynthesizer_SynthesizeGPT_ContainsValidSignature()
|
|
{
|
|
// Create a GeometryMetadata with known partition info
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry
|
|
{
|
|
SectorSize = 512,
|
|
Size = 100 * MiB,
|
|
Sectors = (int)(100 * MiB / 512)
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.GPT,
|
|
SectorSize = 512,
|
|
HasProtectiveMbr = true
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = 512 * 2048, // Start at sector 2048
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.GPT,
|
|
Name = "Partition 1"
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 2,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = 512 * 2048 + 20 * MiB,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.GPT,
|
|
Name = "Partition 2"
|
|
}
|
|
]
|
|
};
|
|
|
|
// Synthesize GPT
|
|
var gptData = PartitionTableSynthesizer.SynthesizeGPT(metadata);
|
|
|
|
Assert.IsNotNull(gptData, "GPT data should not be null.");
|
|
Assert.Greater(gptData.Length, 512 * 2, "GPT data should include protective MBR and GPT header.");
|
|
|
|
// Verify protective MBR boot signature at offset 510-511
|
|
Assert.AreEqual(0x55, gptData[510], "Protective MBR boot signature first byte should be 0x55.");
|
|
Assert.AreEqual(0xAA, gptData[511], "Protective MBR boot signature second byte should be 0xAA.");
|
|
|
|
// Verify GPT signature "EFI PART" at sector 1 (offset 512)
|
|
var expectedSignature = "EFI PART"u8.ToArray();
|
|
for (int i = 0; i < 8; i++)
|
|
Assert.AreEqual(expectedSignature[i], gptData[512 + i], $"GPT signature byte {i} should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableSynthesizer_MBRRoundTrip_PreservesPartitionData()
|
|
{
|
|
// Create a GeometryMetadata with known partition info
|
|
var sectorSize = 512;
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry
|
|
{
|
|
SectorSize = sectorSize,
|
|
Size = 100 * MiB,
|
|
Sectors = (int)(100 * MiB / sectorSize)
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.MBR,
|
|
SectorSize = sectorSize
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = sectorSize * 2048,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.MBR
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 2,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = sectorSize * 2048 + 20 * MiB,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.MBR
|
|
}
|
|
]
|
|
};
|
|
|
|
// Synthesize MBR
|
|
var mbrData = PartitionTableSynthesizer.SynthesizeMBR(metadata);
|
|
|
|
// Parse with PartitionTableFactory
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(mbrData, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be detected.");
|
|
Assert.AreEqual(PartitionTableType.MBR, partitionTable!.TableType, "Should detect MBR partition table.");
|
|
|
|
// Verify partition count
|
|
var partitions = new List<IPartition>();
|
|
await foreach (var partition in partitionTable.EnumeratePartitions(CancellationToken.None))
|
|
partitions.Add(partition);
|
|
|
|
Assert.AreEqual(2, partitions.Count, "Should have 2 partitions.");
|
|
|
|
// Verify partition properties match original metadata
|
|
for (int i = 0; i < partitions.Count; i++)
|
|
{
|
|
var originalPart = metadata.Partitions![i];
|
|
var parsedPart = partitions[i];
|
|
|
|
Assert.AreEqual(originalPart.Number, parsedPart.PartitionNumber, $"Partition {i + 1} number should match.");
|
|
Assert.AreEqual(originalPart.StartOffset, parsedPart.StartOffset, $"Partition {i + 1} start offset should match.");
|
|
Assert.AreEqual(originalPart.Size, parsedPart.Size, $"Partition {i + 1} size should match.");
|
|
}
|
|
|
|
partitionTable.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_PartitionTableSynthesizer_GPTRoundTrip_PreservesPartitionData()
|
|
{
|
|
// Create a GeometryMetadata with known partition info
|
|
var sectorSize = 512;
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry
|
|
{
|
|
SectorSize = sectorSize,
|
|
Size = 100 * MiB,
|
|
Sectors = (int)(100 * MiB / sectorSize)
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.GPT,
|
|
SectorSize = sectorSize,
|
|
HasProtectiveMbr = true
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = sectorSize * 2048,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.GPT,
|
|
Name = "Test Partition 1"
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 2,
|
|
Type = PartitionType.Primary,
|
|
FilesystemType = FileSystemType.FAT32,
|
|
StartOffset = sectorSize * 2048 + 20 * MiB,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.GPT,
|
|
Name = "Test Partition 2"
|
|
}
|
|
]
|
|
};
|
|
|
|
// Synthesize GPT
|
|
var gptData = PartitionTableSynthesizer.SynthesizeGPT(metadata);
|
|
|
|
// Parse with PartitionTableFactory
|
|
var partitionTable = await PartitionTableFactory.CreateAsync(gptData, sectorSize, CancellationToken.None);
|
|
|
|
Assert.IsNotNull(partitionTable, "Partition table should be detected.");
|
|
Assert.AreEqual(PartitionTableType.GPT, partitionTable!.TableType, "Should detect GPT partition table.");
|
|
|
|
// Verify partition count
|
|
var partitions = new List<IPartition>();
|
|
await foreach (var partition in partitionTable.EnumeratePartitions(CancellationToken.None))
|
|
partitions.Add(partition);
|
|
|
|
Assert.AreEqual(2, partitions.Count, "Should have 2 partitions.");
|
|
|
|
// Verify partition properties match original metadata
|
|
for (int i = 0; i < partitions.Count; i++)
|
|
{
|
|
var originalPart = metadata.Partitions![i];
|
|
var parsedPart = partitions[i];
|
|
|
|
Assert.AreEqual(originalPart.Number, parsedPart.PartitionNumber, $"Partition {i + 1} number should match.");
|
|
Assert.AreEqual(originalPart.StartOffset, parsedPart.StartOffset, $"Partition {i + 1} start offset should match.");
|
|
Assert.AreEqual(originalPart.Size, parsedPart.Size, $"Partition {i + 1} size should match.");
|
|
}
|
|
|
|
partitionTable.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionTableSynthesizer_SynthesizePartitionTable_MBR_ReturnsMBRData()
|
|
{
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry { SectorSize = 512, Size = 50 * MiB },
|
|
PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.MBR },
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 512 * 2048,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.MBR
|
|
}
|
|
]
|
|
};
|
|
|
|
var result = PartitionTableSynthesizer.SynthesizePartitionTable(metadata);
|
|
|
|
Assert.IsNotNull(result, "Should return MBR data.");
|
|
Assert.AreEqual(0x55, result![510], "Should have valid boot signature.");
|
|
Assert.AreEqual(0xAA, result[511], "Should have valid boot signature.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionTableSynthesizer_SynthesizePartitionTable_GPT_ReturnsGPTData()
|
|
{
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry { SectorSize = 512, Size = 50 * MiB, Sectors = (int)(50 * MiB / 512) },
|
|
PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.GPT },
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 512 * 2048,
|
|
Size = 20 * MiB,
|
|
TableType = PartitionTableType.GPT
|
|
}
|
|
]
|
|
};
|
|
|
|
var result = PartitionTableSynthesizer.SynthesizePartitionTable(metadata);
|
|
|
|
Assert.IsNotNull(result, "Should return GPT data.");
|
|
// Check for "EFI PART" signature at sector 1
|
|
var expectedSignature = "EFI PART"u8.ToArray();
|
|
for (int i = 0; i < 8; i++)
|
|
Assert.AreEqual(expectedSignature[i], result![512 + i], $"GPT signature byte {i} should match.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionTableSynthesizer_SynthesizePartitionTable_NullPartitionTable_ReturnsNull()
|
|
{
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry { SectorSize = 512 },
|
|
PartitionTable = null
|
|
};
|
|
|
|
var result = PartitionTableSynthesizer.SynthesizePartitionTable(metadata);
|
|
|
|
Assert.IsNull(result, "Should return null when PartitionTable is null.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionTableSynthesizer_SynthesizePartitionTable_UnknownType_ReturnsNull()
|
|
{
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry { SectorSize = 512 },
|
|
PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.Unknown }
|
|
};
|
|
|
|
var result = PartitionTableSynthesizer.SynthesizePartitionTable(metadata);
|
|
|
|
Assert.IsNull(result, "Should return null for Unknown partition table type.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GeometryMetadata Serialization Tests
|
|
|
|
/// <summary>
|
|
/// Tests that a fully populated GeometryMetadata can be serialized to JSON and
|
|
/// deserialized back, preserving all field values.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_GeometryMetadata_RoundTrip_PreservesAllFields()
|
|
{
|
|
// Create a GeometryMetadata with all fields populated
|
|
var original = new GeometryMetadata
|
|
{
|
|
Version = 1,
|
|
Disk = new DiskGeometry
|
|
{
|
|
DevicePath = "/dev/sda",
|
|
Size = 100 * MiB,
|
|
SectorSize = 512,
|
|
Sectors = (int)(100 * MiB / 512),
|
|
TableType = PartitionTableType.GPT
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.GPT,
|
|
Size = 17408, // Typical GPT size
|
|
SectorSize = 512,
|
|
HasProtectiveMbr = true,
|
|
HeaderSize = 92
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 512 * 2048,
|
|
Size = 40 * MiB,
|
|
Name = "Test Partition 1",
|
|
FilesystemType = FileSystemType.FAT32,
|
|
VolumeGuid = Guid.Parse("12345678-1234-1234-1234-123456789abc"),
|
|
TableType = PartitionTableType.GPT
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 2,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 512 * 2048 + 40 * MiB,
|
|
Size = 40 * MiB,
|
|
Name = "Test Partition 2",
|
|
FilesystemType = FileSystemType.NTFS,
|
|
VolumeGuid = Guid.Parse("abcdef12-3456-7890-abcd-ef1234567890"),
|
|
TableType = PartitionTableType.GPT
|
|
}
|
|
],
|
|
Filesystems =
|
|
[
|
|
new FilesystemGeometry
|
|
{
|
|
PartitionNumber = 1,
|
|
Type = FileSystemType.FAT32,
|
|
PartitionStartOffset = 512 * 2048,
|
|
BlockSize = 4096,
|
|
Metadata = "{\"label\":\"FAT32_VOL\",\"serial\":\"1234-5678\"}"
|
|
},
|
|
new FilesystemGeometry
|
|
{
|
|
PartitionNumber = 2,
|
|
Type = FileSystemType.NTFS,
|
|
PartitionStartOffset = 512 * 2048 + 40 * MiB,
|
|
BlockSize = 4096,
|
|
Metadata = "{\"label\":\"NTFS_VOL\",\"guid\":\"{12345678-1234-1234-1234-123456789ABC}\"}"
|
|
}
|
|
]
|
|
};
|
|
|
|
// Serialize to JSON
|
|
var json = original.ToJson();
|
|
|
|
// Verify JSON is not null or empty
|
|
Assert.IsNotNull(json, "JSON output should not be null.");
|
|
Assert.IsNotEmpty(json, "JSON output should not be empty.");
|
|
|
|
// Deserialize from JSON
|
|
var deserialized = GeometryMetadata.FromJson(json);
|
|
|
|
// Verify deserialized object is not null
|
|
Assert.IsNotNull(deserialized, "Deserialized object should not be null.");
|
|
|
|
// Verify Version
|
|
Assert.AreEqual(original.Version, deserialized!.Version, "Version should match.");
|
|
|
|
// Verify Disk properties
|
|
Assert.IsNotNull(deserialized.Disk, "Disk should not be null after deserialization.");
|
|
Assert.AreEqual(original.Disk!.DevicePath, deserialized.Disk!.DevicePath, "Disk DevicePath should match.");
|
|
Assert.AreEqual(original.Disk.Size, deserialized.Disk.Size, "Disk Size should match.");
|
|
Assert.AreEqual(original.Disk.SectorSize, deserialized.Disk.SectorSize, "Disk SectorSize should match.");
|
|
Assert.AreEqual(original.Disk.Sectors, deserialized.Disk.Sectors, "Disk Sectors should match.");
|
|
Assert.AreEqual(original.Disk.TableType, deserialized.Disk.TableType, "Disk TableType should match.");
|
|
|
|
// Verify PartitionTable properties
|
|
Assert.IsNotNull(deserialized.PartitionTable, "PartitionTable should not be null after deserialization.");
|
|
Assert.AreEqual(original.PartitionTable!.Type, deserialized.PartitionTable!.Type, "PartitionTable Type should match.");
|
|
Assert.AreEqual(original.PartitionTable.Size, deserialized.PartitionTable.Size, "PartitionTable Size should match.");
|
|
Assert.AreEqual(original.PartitionTable.SectorSize, deserialized.PartitionTable.SectorSize, "PartitionTable SectorSize should match.");
|
|
Assert.AreEqual(original.PartitionTable.HasProtectiveMbr, deserialized.PartitionTable.HasProtectiveMbr, "PartitionTable HasProtectiveMbr should match.");
|
|
Assert.AreEqual(original.PartitionTable.HeaderSize, deserialized.PartitionTable.HeaderSize, "PartitionTable HeaderSize should match.");
|
|
|
|
// Verify Partitions
|
|
Assert.IsNotNull(deserialized.Partitions, "Partitions should not be null after deserialization.");
|
|
Assert.AreEqual(original.Partitions!.Count, deserialized.Partitions!.Count, "Partition count should match.");
|
|
|
|
for (int i = 0; i < original.Partitions.Count; i++)
|
|
{
|
|
var originalPart = original.Partitions[i];
|
|
var deserializedPart = deserialized.Partitions[i];
|
|
|
|
Assert.AreEqual(originalPart.Number, deserializedPart.Number, $"Partition {i + 1} Number should match.");
|
|
Assert.AreEqual(originalPart.Type, deserializedPart.Type, $"Partition {i + 1} Type should match.");
|
|
Assert.AreEqual(originalPart.StartOffset, deserializedPart.StartOffset, $"Partition {i + 1} StartOffset should match.");
|
|
Assert.AreEqual(originalPart.Size, deserializedPart.Size, $"Partition {i + 1} Size should match.");
|
|
Assert.AreEqual(originalPart.Name, deserializedPart.Name, $"Partition {i + 1} Name should match.");
|
|
Assert.AreEqual(originalPart.FilesystemType, deserializedPart.FilesystemType, $"Partition {i + 1} FilesystemType should match.");
|
|
Assert.AreEqual(originalPart.VolumeGuid, deserializedPart.VolumeGuid, $"Partition {i + 1} VolumeGuid should match.");
|
|
Assert.AreEqual(originalPart.TableType, deserializedPart.TableType, $"Partition {i + 1} TableType should match.");
|
|
}
|
|
|
|
// Verify Filesystems
|
|
Assert.IsNotNull(deserialized.Filesystems, "Filesystems should not be null after deserialization.");
|
|
Assert.AreEqual(original.Filesystems!.Count, deserialized.Filesystems!.Count, "Filesystem count should match.");
|
|
|
|
for (int i = 0; i < original.Filesystems.Count; i++)
|
|
{
|
|
var originalFs = original.Filesystems[i];
|
|
var deserializedFs = deserialized.Filesystems[i];
|
|
|
|
Assert.AreEqual(originalFs.PartitionNumber, deserializedFs.PartitionNumber, $"Filesystem {i + 1} PartitionNumber should match.");
|
|
Assert.AreEqual(originalFs.Type, deserializedFs.Type, $"Filesystem {i + 1} Type should match.");
|
|
Assert.AreEqual(originalFs.PartitionStartOffset, deserializedFs.PartitionStartOffset, $"Filesystem {i + 1} PartitionStartOffset should match.");
|
|
Assert.AreEqual(originalFs.BlockSize, deserializedFs.BlockSize, $"Filesystem {i + 1} BlockSize should match.");
|
|
Assert.AreEqual(originalFs.Metadata, deserializedFs.Metadata, $"Filesystem {i + 1} Metadata should match.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that a GeometryMetadata with null optional fields can be serialized
|
|
/// and deserialized correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_GeometryMetadata_RoundTrip_NullOptionalFields_PreservesStructure()
|
|
{
|
|
// Create a GeometryMetadata with minimal/null optional fields
|
|
var original = new GeometryMetadata
|
|
{
|
|
Version = 1,
|
|
Disk = new DiskGeometry
|
|
{
|
|
DevicePath = null, // Null optional field
|
|
Size = 50 * MiB,
|
|
SectorSize = 512,
|
|
Sectors = (int)(50 * MiB / 512),
|
|
TableType = PartitionTableType.MBR
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.MBR,
|
|
Size = 512,
|
|
SectorSize = 512,
|
|
HasProtectiveMbr = false,
|
|
HeaderSize = 0,
|
|
MbrSize = 512
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 512 * 2048,
|
|
Size = 40 * MiB,
|
|
Name = null, // Null optional field
|
|
FilesystemType = FileSystemType.Unknown,
|
|
VolumeGuid = null, // Null optional field
|
|
TableType = PartitionTableType.MBR
|
|
}
|
|
],
|
|
Filesystems = null // Null optional collection
|
|
};
|
|
|
|
// Serialize to JSON
|
|
var json = original.ToJson();
|
|
|
|
// Verify JSON is not null or empty
|
|
Assert.IsNotNull(json, "JSON output should not be null.");
|
|
Assert.IsNotEmpty(json, "JSON output should not be empty.");
|
|
|
|
// Deserialize from JSON
|
|
var deserialized = GeometryMetadata.FromJson(json);
|
|
|
|
// Verify deserialized object is not null
|
|
Assert.IsNotNull(deserialized, "Deserialized object should not be null.");
|
|
|
|
// Verify null fields remain null
|
|
Assert.IsNull(deserialized!.Disk!.DevicePath, "DevicePath should be null after deserialization.");
|
|
Assert.IsNull(deserialized.Filesystems, "Filesystems should be null after deserialization.");
|
|
|
|
// Verify partition with null fields
|
|
Assert.IsNotNull(deserialized.Partitions, "Partitions should not be null.");
|
|
Assert.AreEqual(1, deserialized.Partitions!.Count, "Should have 1 partition.");
|
|
Assert.IsNull(deserialized.Partitions[0].Name, "Partition Name should be null.");
|
|
Assert.IsNull(deserialized.Partitions[0].VolumeGuid, "Partition VolumeGuid should be null.");
|
|
|
|
// Verify other fields are preserved
|
|
Assert.AreEqual(original.Disk.Size, deserialized.Disk.Size, "Disk Size should match.");
|
|
Assert.AreEqual(original.Disk.SectorSize, deserialized.Disk.SectorSize, "Disk SectorSize should match.");
|
|
Assert.AreEqual(original.PartitionTable!.Type, deserialized.PartitionTable!.Type, "PartitionTable Type should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that an empty GeometryMetadata (with all null/empty fields) can be
|
|
/// serialized and deserialized correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_GeometryMetadata_RoundTrip_EmptyMetadata_HandlesGracefully()
|
|
{
|
|
// Create a GeometryMetadata with mostly empty/null fields
|
|
var original = new GeometryMetadata
|
|
{
|
|
Version = 1,
|
|
Disk = null,
|
|
PartitionTable = null,
|
|
Partitions = null,
|
|
Filesystems = null
|
|
};
|
|
|
|
// Serialize to JSON
|
|
var json = original.ToJson();
|
|
|
|
// Verify JSON is not null or empty
|
|
Assert.IsNotNull(json, "JSON output should not be null.");
|
|
Assert.IsNotEmpty(json, "JSON output should not be empty.");
|
|
|
|
// Deserialize from JSON
|
|
var deserialized = GeometryMetadata.FromJson(json);
|
|
|
|
// Verify deserialized object is not null
|
|
Assert.IsNotNull(deserialized, "Deserialized object should not be null.");
|
|
|
|
// Verify fields are null
|
|
Assert.IsNull(deserialized!.Disk, "Disk should be null.");
|
|
Assert.IsNull(deserialized.PartitionTable, "PartitionTable should be null.");
|
|
Assert.IsNull(deserialized.Partitions, "Partitions should be null.");
|
|
Assert.IsNull(deserialized.Filesystems, "Filesystems should be null.");
|
|
Assert.AreEqual(1, deserialized.Version, "Version should be preserved.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that a GeometryMetadata with multiple partitions and filesystems of
|
|
/// different types can be serialized and deserialized correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_GeometryMetadata_RoundTrip_MultiplePartitionsAndFilesystems_PreservesAllData()
|
|
{
|
|
// Create a GeometryMetadata with multiple partitions and filesystems
|
|
var original = new GeometryMetadata
|
|
{
|
|
Version = 1,
|
|
Disk = new DiskGeometry
|
|
{
|
|
DevicePath = "/dev/nvme0n1",
|
|
Size = 500 * MiB,
|
|
SectorSize = 4096, // 4K sectors
|
|
Sectors = (int)(500 * MiB / 4096),
|
|
TableType = PartitionTableType.GPT
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.GPT,
|
|
Size = 32768,
|
|
SectorSize = 4096,
|
|
HasProtectiveMbr = true,
|
|
HeaderSize = 92
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 4096 * 6, // Start at sector 6 (after GPT headers)
|
|
Size = 100 * MiB,
|
|
Name = "EFI System Partition",
|
|
FilesystemType = FileSystemType.FAT32,
|
|
VolumeGuid = Guid.NewGuid(),
|
|
TableType = PartitionTableType.GPT
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 2,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 4096 * 6 + 100 * MiB,
|
|
Size = 150 * MiB,
|
|
Name = "Windows OS",
|
|
FilesystemType = FileSystemType.NTFS,
|
|
VolumeGuid = Guid.NewGuid(),
|
|
TableType = PartitionTableType.GPT
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 3,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 4096 * 6 + 250 * MiB,
|
|
Size = 200 * MiB,
|
|
Name = "Linux Root",
|
|
FilesystemType = FileSystemType.Ext4,
|
|
VolumeGuid = Guid.NewGuid(),
|
|
TableType = PartitionTableType.GPT
|
|
}
|
|
],
|
|
Filesystems =
|
|
[
|
|
new FilesystemGeometry
|
|
{
|
|
PartitionNumber = 1,
|
|
Type = FileSystemType.FAT32,
|
|
PartitionStartOffset = 4096 * 6,
|
|
BlockSize = 4096,
|
|
Metadata = "{\"label\":\"EFI\"}"
|
|
},
|
|
new FilesystemGeometry
|
|
{
|
|
PartitionNumber = 2,
|
|
Type = FileSystemType.NTFS,
|
|
PartitionStartOffset = 4096 * 6 + 100 * MiB,
|
|
BlockSize = 4096,
|
|
Metadata = "{\"label\":\"Windows\"}"
|
|
},
|
|
new FilesystemGeometry
|
|
{
|
|
PartitionNumber = 3,
|
|
Type = FileSystemType.Ext4,
|
|
PartitionStartOffset = 4096 * 6 + 250 * MiB,
|
|
BlockSize = 4096,
|
|
Metadata = "{\"label\":\"/\"}"
|
|
}
|
|
]
|
|
};
|
|
|
|
// Serialize to JSON
|
|
var json = original.ToJson();
|
|
|
|
// Verify JSON is not null or empty
|
|
Assert.IsNotNull(json, "JSON output should not be null.");
|
|
Assert.IsNotEmpty(json, "JSON output should not be empty.");
|
|
|
|
// Deserialize from JSON
|
|
var deserialized = GeometryMetadata.FromJson(json);
|
|
|
|
// Verify deserialized object is not null
|
|
Assert.IsNotNull(deserialized, "Deserialized object should not be null.");
|
|
|
|
// Verify counts
|
|
Assert.AreEqual(3, deserialized!.Partitions!.Count, "Should have 3 partitions.");
|
|
Assert.AreEqual(3, deserialized.Filesystems!.Count, "Should have 3 filesystems.");
|
|
|
|
// Verify each partition's filesystem type
|
|
Assert.AreEqual(FileSystemType.FAT32, deserialized.Partitions[0].FilesystemType, "Partition 1 should be FAT32.");
|
|
Assert.AreEqual(FileSystemType.NTFS, deserialized.Partitions[1].FilesystemType, "Partition 2 should be NTFS.");
|
|
Assert.AreEqual(FileSystemType.Ext4, deserialized.Partitions[2].FilesystemType, "Partition 3 should be Ext4.");
|
|
|
|
// Verify each filesystem's type
|
|
Assert.AreEqual(FileSystemType.FAT32, deserialized.Filesystems[0].Type, "Filesystem 1 should be FAT32.");
|
|
Assert.AreEqual(FileSystemType.NTFS, deserialized.Filesystems[1].Type, "Filesystem 2 should be NTFS.");
|
|
Assert.AreEqual(FileSystemType.Ext4, deserialized.Filesystems[2].Type, "Filesystem 3 should be Ext4.");
|
|
|
|
// Verify 4K sector size is preserved
|
|
Assert.AreEqual(4096, deserialized.Disk!.SectorSize, "Disk SectorSize should be 4096.");
|
|
Assert.AreEqual(4096, deserialized.PartitionTable!.SectorSize, "PartitionTable SectorSize should be 4096.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that serializing an MBR-based GeometryMetadata produces valid JSON
|
|
/// that can be correctly deserialized.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_GeometryMetadata_RoundTrip_MBRType_PreservesMBRData()
|
|
{
|
|
var original = new GeometryMetadata
|
|
{
|
|
Version = 1,
|
|
Disk = new DiskGeometry
|
|
{
|
|
DevicePath = "/dev/sdb",
|
|
Size = 200 * MiB,
|
|
SectorSize = 512,
|
|
Sectors = (int)(200 * MiB / 512),
|
|
TableType = PartitionTableType.MBR
|
|
},
|
|
PartitionTable = new PartitionTableGeometry
|
|
{
|
|
Type = PartitionTableType.MBR,
|
|
Size = 512,
|
|
SectorSize = 512,
|
|
HasProtectiveMbr = false,
|
|
MbrSize = 512
|
|
},
|
|
Partitions =
|
|
[
|
|
new PartitionGeometry
|
|
{
|
|
Number = 1,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 512 * 2048,
|
|
Size = 100 * MiB,
|
|
Name = "Primary Partition",
|
|
FilesystemType = FileSystemType.FAT32,
|
|
TableType = PartitionTableType.MBR
|
|
},
|
|
new PartitionGeometry
|
|
{
|
|
Number = 2,
|
|
Type = PartitionType.Primary,
|
|
StartOffset = 512 * 2048 + 100 * MiB,
|
|
Size = 90 * MiB,
|
|
Name = "Secondary Partition",
|
|
FilesystemType = FileSystemType.NTFS,
|
|
TableType = PartitionTableType.MBR
|
|
}
|
|
],
|
|
Filesystems =
|
|
[
|
|
new FilesystemGeometry
|
|
{
|
|
PartitionNumber = 1,
|
|
Type = FileSystemType.FAT32,
|
|
PartitionStartOffset = 512 * 2048,
|
|
BlockSize = 4096,
|
|
Metadata = null
|
|
}
|
|
]
|
|
};
|
|
|
|
// Serialize to JSON
|
|
var json = original.ToJson();
|
|
|
|
// Deserialize from JSON
|
|
var deserialized = GeometryMetadata.FromJson(json);
|
|
|
|
// Verify MBR-specific fields
|
|
Assert.IsNotNull(deserialized, "Deserialized object should not be null.");
|
|
Assert.AreEqual(PartitionTableType.MBR, deserialized!.Disk!.TableType, "Disk TableType should be MBR.");
|
|
Assert.AreEqual(PartitionTableType.MBR, deserialized.PartitionTable!.Type, "PartitionTable Type should be MBR.");
|
|
Assert.AreEqual(512, deserialized.PartitionTable.MbrSize, "PartitionTable MbrSize should be 512.");
|
|
Assert.IsFalse(deserialized.PartitionTable.HasProtectiveMbr, "PartitionTable HasProtectiveMbr should be false.");
|
|
|
|
// Verify all partitions have MBR table type
|
|
foreach (var partition in deserialized.Partitions!)
|
|
{
|
|
Assert.AreEqual(PartitionTableType.MBR, partition.TableType, "All partitions should have MBR TableType.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that deserializing invalid JSON throws an appropriate exception.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_GeometryMetadata_FromJson_InvalidJson_ThrowsException()
|
|
{
|
|
// Test with invalid JSON - should throw JsonException
|
|
var invalidJson = "{invalid json";
|
|
Assert.Throws<System.Text.Json.JsonException>(() => GeometryMetadata.FromJson(invalidJson));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that deserializing empty JSON object creates a minimal GeometryMetadata
|
|
/// with default property values set by the class.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_GeometryMetadata_FromJson_EmptyObject_CreatesDefaultInstance()
|
|
{
|
|
var emptyJson = "{}";
|
|
var result = GeometryMetadata.FromJson(emptyJson);
|
|
|
|
Assert.IsNotNull(result, "Should create an instance from empty JSON object.");
|
|
// Version has a default value of 1 in the class definition
|
|
Assert.AreEqual(1, result!.Version, "Version should have default value (1) from class definition.");
|
|
Assert.IsNull(result.Disk, "Disk should be null.");
|
|
Assert.IsNull(result.PartitionTable, "PartitionTable should be null.");
|
|
Assert.IsNull(result.Partitions, "Partitions should be null.");
|
|
Assert.IsNull(result.Filesystems, "Filesystems should be null.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UnknownFilesystem read/write tests
|
|
|
|
/// <summary>
|
|
/// Tests that ListFilesAsync returns the expected block-based files for an UnknownFilesystem.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_ListFilesAsync_ReturnsBlockBasedFiles()
|
|
{
|
|
// Create a partition on the writable disk
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 10 * MiB;
|
|
var partitionOffset = sectorSize * 2048; // Start at sector 2048
|
|
|
|
// Create a simple partition entry
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
// Create UnknownFilesystem with 1MB block size
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// List files
|
|
var files = new List<IFile>();
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
files.Add(file);
|
|
|
|
// Should have 10 files (10MB partition / 1MB block size)
|
|
Assert.AreEqual(10, files.Count, "Should have 10 block files for a 10MB partition with 1MB blocks.");
|
|
|
|
// Verify each file has correct properties
|
|
for (int i = 0; i < files.Count; i++)
|
|
{
|
|
var file = files[i];
|
|
Assert.IsInstanceOf<UnknownFilesystemFile>(file, $"File {i} should be UnknownFilesystemFile.");
|
|
Assert.IsFalse(file.IsDirectory, $"File {i} should not be a directory.");
|
|
|
|
var unknownFile = (UnknownFilesystemFile)file;
|
|
Assert.AreEqual(i * 1024L * 1024L, unknownFile.Address, $"File {i} should have correct address.");
|
|
|
|
// All files except possibly the last should have full block size
|
|
if (i < files.Count - 1)
|
|
Assert.AreEqual(1024 * 1024, unknownFile.Size, $"File {i} should have full block size.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that write and read round-trip preserves data correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_WriteRead_RoundTrip_DataMatches()
|
|
{
|
|
// Create a partition on the writable disk
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
// Create UnknownFilesystem with 1MB block size
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write data to the file
|
|
var writeData = new byte[1024 * 1024];
|
|
new Random(42).NextBytes(writeData);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
}
|
|
|
|
// Read data back
|
|
byte[] readData;
|
|
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
readData = new byte[writeData.Length];
|
|
var bytesRead = await readStream.ReadAsync(readData.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(writeData.Length, bytesRead, "Should have read all bytes.");
|
|
}
|
|
|
|
// Verify data matches
|
|
Assert.AreEqual(writeData, readData, "Read data should match written data.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BoundsCheck validates ranges correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_BoundsCheck_ValidRange_DoesNotThrow()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Valid ranges should not throw
|
|
Assert.DoesNotThrow(() => fs.BoundsCheck(0, sectorSize), "BoundsCheck should not throw for valid range at start.");
|
|
Assert.DoesNotThrow(() => fs.BoundsCheck(sectorSize, sectorSize), "BoundsCheck should not throw for valid range.");
|
|
Assert.DoesNotThrow(() => fs.BoundsCheck(partitionSize - sectorSize, sectorSize), "BoundsCheck should not throw for valid range at end.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BoundsCheck throws for negative start or size.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_BoundsCheck_NegativeValues_ThrowsArgumentOutOfRange()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Negative start should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(-1, sectorSize), "Negative start should throw.");
|
|
|
|
// Negative size should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(0, -1), "Negative size should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BoundsCheck throws for ranges exceeding partition size.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_BoundsCheck_ExceedsPartitionSize_ThrowsArgumentOutOfRange()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Range exceeding partition size should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(partitionSize - sectorSize, sectorSize * 2), "Range exceeding partition should throw.");
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(partitionSize, sectorSize), "Start at partition end should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BoundsCheck throws for unaligned start or size.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_BoundsCheck_UnalignedValues_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Unaligned start should throw
|
|
Assert.Throws<ArgumentException>(() => fs.BoundsCheck(1, sectorSize), "Unaligned start should throw.");
|
|
|
|
// Unaligned size should throw
|
|
Assert.Throws<ArgumentException>(() => fs.BoundsCheck(0, sectorSize + 1), "Unaligned size should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem constructor throws for unaligned block sizes.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_Constructor_UnalignedBlockSize_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
// Block size not a multiple of sector size should throw
|
|
var unalignedBlockSize = sectorSize + 1;
|
|
var ex = Assert.Throws<ArgumentException>(() => new UnknownFilesystem(partition, blockSize: unalignedBlockSize),
|
|
"Unaligned block size should throw ArgumentException.");
|
|
Assert.That(ex!.ParamName, Is.EqualTo("blockSize"), "Exception should be for blockSize parameter.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem constructor accepts valid block sizes.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_Constructor_ValidBlockSize_CreatesInstance()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
// Valid block sizes should create instance
|
|
Assert.DoesNotThrow(() =>
|
|
{
|
|
using var fs1 = new UnknownFilesystem(partition, blockSize: sectorSize);
|
|
}, "Block size equal to sector size should work.");
|
|
|
|
Assert.DoesNotThrow(() =>
|
|
{
|
|
using var fs2 = new UnknownFilesystem(partition, blockSize: sectorSize * 4);
|
|
}, "Block size multiple of sector size should work.");
|
|
|
|
Assert.DoesNotThrow(() =>
|
|
{
|
|
using var fs3 = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
}, "1MB block size should work.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class to implement IPartition for testing.
|
|
/// </summary>
|
|
private class TestPartition : IPartition
|
|
{
|
|
public long StartOffset { get; init; }
|
|
public long Size { get; init; }
|
|
public IRawDisk? RawDisk { get; init; }
|
|
public int PartitionNumber => 1;
|
|
public string? Name => "TestPartition";
|
|
public Guid? UniqueId => Guid.Empty;
|
|
public PartitionType Type => PartitionType.Primary;
|
|
public FileSystemType FilesystemType => FileSystemType.Unknown;
|
|
public Guid? VolumeGuid => Guid.Empty;
|
|
public IPartitionTable PartitionTable => new TestPartitionTable { RawDisk = RawDisk };
|
|
|
|
public Task<Stream> OpenReadAsync(CancellationToken cancellationToken)
|
|
=> Task.FromException<Stream>(new NotImplementedException());
|
|
|
|
public Task<Stream> OpenWriteAsync(CancellationToken cancellationToken)
|
|
=> Task.FromException<Stream>(new NotImplementedException());
|
|
|
|
public void Dispose() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class to implement IPartitionTable for testing.
|
|
/// </summary>
|
|
private class TestPartitionTable : IPartitionTable
|
|
{
|
|
public IRawDisk? RawDisk { get; init; }
|
|
public PartitionTableType TableType => PartitionTableType.Unknown;
|
|
public long Size => 0;
|
|
|
|
public IAsyncEnumerable<IPartition> EnumeratePartitions(CancellationToken cancellationToken)
|
|
=> AsyncEnumerable.Empty<IPartition>();
|
|
|
|
public Task<IPartition?> GetPartitionAsync(int partitionNumber, CancellationToken cancellationToken)
|
|
=> Task.FromResult<IPartition?>(null);
|
|
|
|
public Task<Stream> GetProtectiveMbrAsync(CancellationToken cancellationToken)
|
|
=> Task.FromException<Stream>(new NotImplementedException());
|
|
|
|
public Task<Stream> GetPartitionTableDataAsync(CancellationToken cancellationToken)
|
|
=> Task.FromException<Stream>(new NotImplementedException());
|
|
|
|
public void Dispose() { }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UnknownFilesystemStream Unaligned Tests
|
|
|
|
/// <summary>
|
|
/// Tests that writing partial data (less than block size) is correctly flushed on dispose.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_WritePartialData_FlushesOnDispose()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write partial data (less than block size)
|
|
var writeData = new byte[1024]; // 1KB, much less than 1MB block
|
|
new Random(42).NextBytes(writeData);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Read back and verify - the written data should be preserved
|
|
byte[] readData;
|
|
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
readData = new byte[writeData.Length];
|
|
var bytesRead = await readStream.ReadAsync(readData.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(writeData.Length, bytesRead, "Should have read all written bytes.");
|
|
}
|
|
|
|
// Verify data matches
|
|
Assert.AreEqual(writeData, readData, "Partial data should be correctly flushed and readable.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests writing at position 0, seeking to middle, writing more data, and verifying both regions.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_WriteSeekWrite_BothRegionsPreserved()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write at position 0, seek to middle, write more data
|
|
var firstWriteData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
|
|
var secondWriteData = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };
|
|
var middlePosition = 1000;
|
|
|
|
using (var writeStream = await fs.OpenReadWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
// Write at position 0
|
|
await writeStream.WriteAsync(firstWriteData.AsMemory(), CancellationToken.None);
|
|
|
|
// Seek to middle position
|
|
writeStream.Seek(middlePosition, SeekOrigin.Begin);
|
|
|
|
// Write more data
|
|
await writeStream.WriteAsync(secondWriteData.AsMemory(), CancellationToken.None);
|
|
}
|
|
|
|
// Read back and verify both regions
|
|
byte[] readData;
|
|
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
readData = new byte[middlePosition + secondWriteData.Length];
|
|
var bytesRead = await readStream.ReadAsync(readData.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(middlePosition + secondWriteData.Length, bytesRead, "Should have read all data.");
|
|
}
|
|
|
|
// Verify first region (position 0-4)
|
|
for (int i = 0; i < firstWriteData.Length; i++)
|
|
Assert.AreEqual(firstWriteData[i], readData[i], $"Byte at position {i} should match first write.");
|
|
|
|
// Verify middle region (position 1000-1003)
|
|
for (int i = 0; i < secondWriteData.Length; i++)
|
|
Assert.AreEqual(secondWriteData[i], readData[middlePosition + i], $"Byte at position {middlePosition + i} should match second write.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests reading after seeking to a non-zero position.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_ReadAfterSeek_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write test data
|
|
var writeData = new byte[4096]; // 4KB of test data
|
|
for (int i = 0; i < writeData.Length; i++)
|
|
writeData[i] = (byte)(i & 0xFF);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Read after seeking to non-zero position
|
|
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
// Seek to position 100
|
|
readStream.Seek(100, SeekOrigin.Begin);
|
|
|
|
// Read remaining data
|
|
var readBuffer = new byte[writeData.Length - 100];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(writeData.Length - 100, bytesRead, "Should have read remaining bytes after seek.");
|
|
|
|
// Verify data matches expected values
|
|
for (int i = 0; i < bytesRead; i++)
|
|
{
|
|
Assert.AreEqual((byte)((i + 100) & 0xFF), readBuffer[i], $"Byte at position {100 + i} should match.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that writing data that doesn't fill the entire stream preserves the written data
|
|
/// while the remaining bytes retain their previous values.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_WritePartial_PreservesData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
// Use a small block size for this test
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// First, initialize the entire block with zeros by writing zeros
|
|
var zeroData = new byte[blockSize];
|
|
using (var zeroStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await zeroStream.WriteAsync(zeroData.AsMemory(), CancellationToken.None);
|
|
|
|
// Now write partial data at the beginning
|
|
var writeData = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Read the entire block and verify written data and remaining zeros
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
var readBuffer = new byte[blockSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(blockSize, bytesRead, "Should have read the full block.");
|
|
|
|
// Verify written data
|
|
for (int i = 0; i < writeData.Length; i++)
|
|
Assert.AreEqual(writeData[i], readBuffer[i], $"Byte at position {i} should match written data.");
|
|
|
|
// Verify remaining bytes are still zero
|
|
for (int i = writeData.Length; i < blockSize; i++)
|
|
Assert.AreEqual(0, readBuffer[i], $"Byte at position {i} should be zero.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the lazy-load behavior: first read triggers disk read, subsequent reads use buffer.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_LazyLoad_FirstReadTriggersDiskRead()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write test data first
|
|
var writeData = new byte[2048];
|
|
new Random(123).NextBytes(writeData);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Now test lazy loading - first read should trigger disk read
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
|
|
// First small read (triggers lazy load)
|
|
var firstReadBuffer = new byte[512];
|
|
var bytesRead1 = await readStream.ReadAsync(firstReadBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(512, bytesRead1, "First read should return 512 bytes.");
|
|
|
|
// Second read should use cached buffer (no disk read)
|
|
var secondReadBuffer = new byte[512];
|
|
var bytesRead2 = await readStream.ReadAsync(secondReadBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(512, bytesRead2, "Second read should return 512 bytes from buffer.");
|
|
|
|
// Verify both reads returned correct data
|
|
for (int i = 0; i < 512; i++)
|
|
{
|
|
Assert.AreEqual(writeData[i], firstReadBuffer[i], $"First read: byte at position {i} should match.");
|
|
Assert.AreEqual(writeData[i + 512], secondReadBuffer[i], $"Second read: byte at position {i + 512} should match.");
|
|
}
|
|
|
|
// Seek back and read again - should still use buffer
|
|
readStream.Seek(0, SeekOrigin.Begin);
|
|
var thirdReadBuffer = new byte[512];
|
|
var bytesRead3 = await readStream.ReadAsync(thirdReadBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(512, bytesRead3, "Third read after seek should return 512 bytes.");
|
|
|
|
for (int i = 0; i < 512; i++)
|
|
Assert.AreEqual(writeData[i], thirdReadBuffer[i], $"Third read: byte at position {i} should match after seek.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests seeking with SeekOrigin.Current works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_SeekCurrent_UpdatesPosition()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write test data
|
|
var writeData = new byte[4096];
|
|
for (int i = 0; i < writeData.Length; i++)
|
|
writeData[i] = (byte)(i & 0xFF);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Test Seek with SeekOrigin.Current
|
|
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
// Read first 100 bytes
|
|
var buffer1 = new byte[100];
|
|
var bytesRead1 = await readStream.ReadAsync(buffer1.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(100, bytesRead1, "First read should return 100 bytes.");
|
|
Assert.AreEqual(100, readStream.Position, "Position should be 100 after first read.");
|
|
|
|
// Seek forward 100 bytes from current
|
|
var newPos = readStream.Seek(100, SeekOrigin.Current);
|
|
Assert.AreEqual(200, newPos, "Position should be 200 after Seek(Current, 100).");
|
|
Assert.AreEqual(200, readStream.Position, "Stream.Position should match returned position.");
|
|
|
|
// Read next 100 bytes
|
|
var buffer2 = new byte[100];
|
|
var bytesRead2 = await readStream.ReadAsync(buffer2.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(100, bytesRead2, "Second read should return 100 bytes.");
|
|
|
|
// Verify data at position 200
|
|
for (int i = 0; i < 100; i++)
|
|
Assert.AreEqual((byte)((200 + i) & 0xFF), buffer2[i], $"Byte at position {200 + i} should match.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests seeking with SeekOrigin.End works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_SeekEnd_UpdatesPosition()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write test data
|
|
var writeData = new byte[blockSize];
|
|
for (int i = 0; i < writeData.Length; i++)
|
|
writeData[i] = (byte)(i & 0xFF);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Test Seek with SeekOrigin.End
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
|
|
// Seek to 100 bytes from end
|
|
var newPos = readStream.Seek(-100, SeekOrigin.End);
|
|
Assert.AreEqual(blockSize - 100, newPos, "Position should be blockSize - 100.");
|
|
Assert.AreEqual(blockSize - 100, readStream.Position, "Stream.Position should match.");
|
|
|
|
// Read remaining bytes
|
|
var buffer = new byte[100];
|
|
var bytesRead = await readStream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(100, bytesRead, "Should read 100 bytes.");
|
|
|
|
// Verify last 100 bytes
|
|
for (int i = 0; i < 100; i++)
|
|
Assert.AreEqual((byte)((blockSize - 100 + i) & 0xFF), buffer[i], $"Byte at position {blockSize - 100 + i} should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that reading past the end of the stream returns 0 bytes.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_ReadPastEnd_ReturnsZero()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write small amount of test data
|
|
var writeData = new byte[100];
|
|
new Random(42).NextBytes(writeData);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Test reading past end
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
|
|
// Seek to near end
|
|
readStream.Seek(blockSize - 10, SeekOrigin.Begin);
|
|
|
|
// Try to read 100 bytes when only 10 remain
|
|
var buffer = new byte[100];
|
|
var bytesRead = await readStream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(10, bytesRead, "Should only read remaining 10 bytes.");
|
|
|
|
// Try to read again - should return 0
|
|
bytesRead = await readStream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(0, bytesRead, "Should return 0 when at end of stream.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that unaligned writes within the stream work correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_UnalignedWrite_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 8192;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// First, zero-fill the block so gap bytes are deterministic
|
|
using (var zeroStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await zeroStream.WriteAsync(new byte[blockSize].AsMemory(), CancellationToken.None);
|
|
|
|
// Write at unaligned positions using read-write stream (pre-loads zeroed data)
|
|
using (var writeStream = await fs.OpenReadWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
// Write 3 bytes at position 0
|
|
await writeStream.WriteAsync(new byte[] { 0xAA, 0xBB, 0xCC }.AsMemory(), CancellationToken.None);
|
|
|
|
// Seek to unaligned position 5
|
|
writeStream.Seek(5, SeekOrigin.Begin);
|
|
await writeStream.WriteAsync(new byte[] { 0xDD, 0xEE }.AsMemory(), CancellationToken.None);
|
|
|
|
// Seek to another unaligned position 10
|
|
writeStream.Seek(10, SeekOrigin.Begin);
|
|
await writeStream.WriteAsync(new byte[] { 0x11, 0x22, 0x33, 0x44 }.AsMemory(), CancellationToken.None);
|
|
}
|
|
|
|
// Read back and verify
|
|
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
var readBuffer = new byte[14];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(14, bytesRead, "Should read 14 bytes.");
|
|
|
|
// Verify written data
|
|
Assert.AreEqual(0xAA, readBuffer[0], "Byte 0 should be 0xAA");
|
|
Assert.AreEqual(0xBB, readBuffer[1], "Byte 1 should be 0xBB");
|
|
Assert.AreEqual(0xCC, readBuffer[2], "Byte 2 should be 0xCC");
|
|
// Bytes 3-4 should be zero
|
|
Assert.AreEqual(0, readBuffer[3], "Byte 3 should be 0");
|
|
Assert.AreEqual(0, readBuffer[4], "Byte 4 should be 0");
|
|
Assert.AreEqual(0xDD, readBuffer[5], "Byte 5 should be 0xDD");
|
|
Assert.AreEqual(0xEE, readBuffer[6], "Byte 6 should be 0xEE");
|
|
// Bytes 7-9 should be zero
|
|
Assert.AreEqual(0, readBuffer[7], "Byte 7 should be 0");
|
|
Assert.AreEqual(0, readBuffer[8], "Byte 8 should be 0");
|
|
Assert.AreEqual(0, readBuffer[9], "Byte 9 should be 0");
|
|
Assert.AreEqual(0x11, readBuffer[10], "Byte 10 should be 0x11");
|
|
Assert.AreEqual(0x22, readBuffer[11], "Byte 11 should be 0x22");
|
|
Assert.AreEqual(0x33, readBuffer[12], "Byte 12 should be 0x33");
|
|
Assert.AreEqual(0x44, readBuffer[13], "Byte 13 should be 0x44");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 12. PooledBuffer and PooledMemoryStream Tests
|
|
|
|
/// <summary>
|
|
/// Tests that PooledBuffer rents a buffer from the ArrayPool and returns it on dispose.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledBuffer_RentAndReturn_BufferRecycled()
|
|
{
|
|
// Rent a buffer using PooledBuffer
|
|
using var pooledBuffer = new PooledBuffer(1024);
|
|
byte[] bufferArray = pooledBuffer.Array;
|
|
var bufferLength = pooledBuffer.Length;
|
|
|
|
// Verify the buffer was rented
|
|
Assert.IsNotNull(bufferArray, "Buffer array should not be null.");
|
|
Assert.AreEqual(1024, bufferLength, "Buffer length should match requested size.");
|
|
Assert.GreaterOrEqual(bufferArray.Length, 1024, "Underlying array should be at least the requested size.");
|
|
|
|
// Write some data to verify we have a valid buffer
|
|
bufferArray[0] = 0xAB;
|
|
bufferArray[1023] = 0xCD;
|
|
|
|
// Verify Memory and Span properties work correctly
|
|
var memory = pooledBuffer.Memory;
|
|
var span = pooledBuffer.Span;
|
|
|
|
Assert.AreEqual(1024, memory.Length, "Memory length should match requested size.");
|
|
Assert.AreEqual(1024, span.Length, "Span length should match requested size.");
|
|
Assert.AreEqual(0xAB, memory.Span[0], "Memory should reflect buffer changes.");
|
|
Assert.AreEqual(0xCD, span[1023], "Span should reflect buffer changes.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledBuffer handles different sizes correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledBuffer_DifferentSizes_WorksCorrectly()
|
|
{
|
|
// Test small buffer
|
|
using (var small = new PooledBuffer(16))
|
|
{
|
|
Assert.AreEqual(16, small.Length, "Small buffer length should be correct.");
|
|
Assert.GreaterOrEqual(small.Array.Length, 16, "Small buffer array should be at least requested size.");
|
|
}
|
|
|
|
// Test large buffer (above LOH threshold of ~85KB)
|
|
using (var large = new PooledBuffer(128 * 1024))
|
|
{
|
|
Assert.AreEqual(128 * 1024, large.Length, "Large buffer length should be correct.");
|
|
Assert.GreaterOrEqual(large.Array.Length, 128 * 1024, "Large buffer array should be at least requested size.");
|
|
}
|
|
|
|
// Test zero-size buffer
|
|
using (var empty = new PooledBuffer(0))
|
|
{
|
|
Assert.AreEqual(0, empty.Length, "Empty buffer length should be 0.");
|
|
Assert.IsNotNull(empty.Array, "Empty buffer array should still be rented.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream reads correctly and returns buffer on dispose.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_Read_ReturnsCorrectData()
|
|
{
|
|
// Rent a buffer and populate it with test data
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(1024))
|
|
{
|
|
buffer = pooledBuffer.Array;
|
|
for (int i = 0; i < 1024; i++)
|
|
buffer[i] = (byte)(i & 0xFF);
|
|
}
|
|
|
|
// Create a PooledMemoryStream with the buffer
|
|
using var stream = new PooledMemoryStream(buffer, 1024);
|
|
|
|
// Verify stream properties
|
|
Assert.IsTrue(stream.CanRead, "Stream should be readable.");
|
|
Assert.IsTrue(stream.CanSeek, "Stream should be seekable.");
|
|
Assert.IsFalse(stream.CanWrite, "Stream should not be writable.");
|
|
Assert.AreEqual(1024, stream.Length, "Stream length should be correct.");
|
|
Assert.AreEqual(0, stream.Position, "Initial position should be 0.");
|
|
|
|
// Read data from the stream
|
|
var readBuffer = new byte[256];
|
|
var bytesRead = stream.Read(readBuffer, 0, 256);
|
|
|
|
Assert.AreEqual(256, bytesRead, "Should have read 256 bytes.");
|
|
Assert.AreEqual(256, stream.Position, "Position should be updated.");
|
|
|
|
// Verify the data
|
|
for (int i = 0; i < 256; i++)
|
|
Assert.AreEqual((byte)(i & 0xFF), readBuffer[i], $"Byte at position {i} should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream seek operations work correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_Seek_BeginCurrentEnd_WorksCorrectly()
|
|
{
|
|
// Rent a buffer and populate it
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(512))
|
|
{
|
|
buffer = pooledBuffer.Array;
|
|
for (int i = 0; i < 512; i++)
|
|
buffer[i] = (byte)(i & 0xFF);
|
|
}
|
|
|
|
using (var stream = new PooledMemoryStream(buffer, 512))
|
|
{
|
|
// Test SeekOrigin.Begin
|
|
var pos = stream.Seek(100, SeekOrigin.Begin);
|
|
Assert.AreEqual(100, pos, "Seek from Begin should return correct position.");
|
|
Assert.AreEqual(100, stream.Position, "Position should be 100.");
|
|
|
|
// Read a byte to verify position
|
|
var readBuffer = new byte[1];
|
|
var bytesRead = stream.Read(readBuffer, 0, 1);
|
|
Assert.AreEqual(1, bytesRead, "Should read 1 byte.");
|
|
Assert.AreEqual(100, readBuffer[0], "Should read byte at position 100.");
|
|
|
|
// Test SeekOrigin.Current
|
|
pos = stream.Seek(50, SeekOrigin.Current);
|
|
Assert.AreEqual(151, pos, "Seek from Current should return correct position.");
|
|
Assert.AreEqual(151, stream.Position, "Position should be 151.");
|
|
|
|
bytesRead = stream.Read(readBuffer, 0, 1);
|
|
Assert.AreEqual(1, bytesRead, "Should read 1 byte.");
|
|
Assert.AreEqual(151, readBuffer[0], "Should read byte at position 151.");
|
|
|
|
// Test SeekOrigin.End
|
|
pos = stream.Seek(-10, SeekOrigin.End);
|
|
Assert.AreEqual(502, pos, "Seek from End should return correct position.");
|
|
Assert.AreEqual(502, stream.Position, "Position should be 502.");
|
|
|
|
bytesRead = stream.Read(readBuffer, 0, 1);
|
|
Assert.AreEqual(1, bytesRead, "Should read 1 byte.");
|
|
// 502 & 0xFF = 246 (byte overflow)
|
|
Assert.AreEqual(246, readBuffer[0], "Should read byte at position 502 (502 & 0xFF = 246).");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that seeking beyond stream bounds throws ArgumentOutOfRangeException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_SeekOutOfBounds_ThrowsArgumentOutOfRange()
|
|
{
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(100))
|
|
buffer = pooledBuffer.Array;
|
|
|
|
using var stream = new PooledMemoryStream(buffer, 100);
|
|
|
|
// Seek before beginning
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(-1, SeekOrigin.Begin), "Seek before beginning should throw.");
|
|
|
|
// Seek past end
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(101, SeekOrigin.Begin), "Seek past end should throw.");
|
|
|
|
// Seek before beginning using Current
|
|
stream.Seek(50, SeekOrigin.Begin);
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(-51, SeekOrigin.Current), "Seek before beginning from current should throw.");
|
|
|
|
// Seek past end using End
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(1, SeekOrigin.End), "Seek past end from end should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that reading past the end of PooledMemoryStream returns 0 bytes.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_ReadPastEnd_ReturnsZero()
|
|
{
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(100))
|
|
{
|
|
buffer = pooledBuffer.Array;
|
|
// Fill with test data
|
|
for (int i = 0; i < 100; i++)
|
|
buffer[i] = (byte)(i & 0xFF);
|
|
}
|
|
|
|
using var stream = new PooledMemoryStream(buffer, 100);
|
|
|
|
// Seek to near end
|
|
stream.Seek(95, SeekOrigin.Begin);
|
|
|
|
// Read 10 bytes when only 5 remain
|
|
var readBuffer = new byte[10];
|
|
var bytesRead = stream.Read(readBuffer, 0, 10);
|
|
|
|
Assert.AreEqual(5, bytesRead, "Should only read remaining 5 bytes.");
|
|
Assert.AreEqual(100, stream.Position, "Position should be at end.");
|
|
|
|
// Read again - should return 0
|
|
bytesRead = stream.Read(readBuffer, 0, 10);
|
|
Assert.AreEqual(0, bytesRead, "Should return 0 when at end of stream.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream returns buffer to pool on dispose.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_Dispose_ReturnsBufferToPool()
|
|
{
|
|
// This test verifies that the stream can be disposed without errors
|
|
// and that the buffer is returned to the pool (which we can't directly verify,
|
|
// but we can ensure disposal works correctly)
|
|
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(256))
|
|
buffer = pooledBuffer.Array;
|
|
|
|
var stream = new PooledMemoryStream(buffer, 256);
|
|
|
|
// Use the stream
|
|
var readBuffer = new byte[64];
|
|
var bytesRead = stream.Read(readBuffer, 0, 64);
|
|
Assert.AreEqual(64, bytesRead, "Should read 64 bytes.");
|
|
|
|
// Dispose
|
|
stream.Dispose();
|
|
|
|
// After dispose, attempting to read should throw ObjectDisposedException
|
|
Assert.Throws<ObjectDisposedException>(() => { var _ = stream.Read(readBuffer, 0, 64); }, "Reading after dispose should throw.");
|
|
|
|
// Note: Seek doesn't throw ObjectDisposedException in this implementation
|
|
// because it doesn't access the buffer directly (only _position and _length)
|
|
|
|
// CanRead, CanSeek should return false after dispose
|
|
Assert.IsFalse(stream.CanRead, "CanRead should be false after dispose.");
|
|
Assert.IsFalse(stream.CanSeek, "CanSeek should be false after dispose.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream write and set length operations throw NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_WriteAndSetLength_ThrowsNotSupported()
|
|
{
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(100))
|
|
buffer = pooledBuffer.Array;
|
|
|
|
using var stream = new PooledMemoryStream(buffer, 100);
|
|
|
|
// Write should throw
|
|
Assert.Throws<NotSupportedException>(() => stream.Write(new byte[10], 0, 10), "Write should throw NotSupportedException.");
|
|
|
|
// SetLength should throw
|
|
Assert.Throws<NotSupportedException>(() => stream.SetLength(50), "SetLength should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream handles empty buffers correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_EmptyBuffer_WorksCorrectly()
|
|
{
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(0))
|
|
buffer = pooledBuffer.Array;
|
|
|
|
using var stream = new PooledMemoryStream(buffer, 0);
|
|
|
|
Assert.AreEqual(0, stream.Length, "Length should be 0.");
|
|
Assert.AreEqual(0, stream.Position, "Position should be 0.");
|
|
|
|
// Read should return 0
|
|
var readBuffer = new byte[10];
|
|
var bytesRead = stream.Read(readBuffer, 0, 10);
|
|
Assert.AreEqual(0, bytesRead, "Read from empty stream should return 0.");
|
|
|
|
// Seek to 0 should work
|
|
var pos = stream.Seek(0, SeekOrigin.Begin);
|
|
Assert.AreEqual(0, pos, "Seek to 0 should work.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledBuffer can be used multiple times in succession.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledBuffer_MultipleInstances_WorkIndependently()
|
|
{
|
|
// Create multiple PooledBuffers
|
|
using var buffer1 = new PooledBuffer(64);
|
|
using var buffer2 = new PooledBuffer(128);
|
|
using var buffer3 = new PooledBuffer(256);
|
|
|
|
// Write different data to each
|
|
buffer1.Array[0] = 0x11;
|
|
buffer2.Array[0] = 0x22;
|
|
buffer3.Array[0] = 0x33;
|
|
|
|
// Verify each buffer has its own data
|
|
Assert.AreEqual(0x11, buffer1.Array[0], "Buffer1 should have its own data.");
|
|
Assert.AreEqual(0x22, buffer2.Array[0], "Buffer2 should have its own data.");
|
|
Assert.AreEqual(0x33, buffer3.Array[0], "Buffer3 should have its own data.");
|
|
|
|
// Verify lengths
|
|
Assert.AreEqual(64, buffer1.Length, "Buffer1 length should be correct.");
|
|
Assert.AreEqual(128, buffer2.Length, "Buffer2 length should be correct.");
|
|
Assert.AreEqual(256, buffer3.Length, "Buffer3 length should be correct.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region RestoreProvider Path Parsing Tests
|
|
|
|
/// <summary>
|
|
/// Creates a RestoreProvider with mock partitions and filesystems for testing path parsing.
|
|
/// Uses reflection to set up the internal state.
|
|
/// </summary>
|
|
private static RestoreProvider CreateRestoreProviderForPathParsingTests()
|
|
{
|
|
// Create a RestoreProvider using the default constructor (for metadata loading)
|
|
var provider = new RestoreProvider();
|
|
|
|
// Create mock partition tables
|
|
var gptTable = new MockPartitionTable(PartitionTableType.GPT);
|
|
var mbrTable = new MockPartitionTable(PartitionTableType.MBR);
|
|
|
|
// Create mock partitions
|
|
var partitions = new List<IPartition>
|
|
{
|
|
new MockPartition(gptTable, 1, PartitionType.Primary, 1048576, 20971520, "EFI System", FileSystemType.FAT32),
|
|
new MockPartition(gptTable, 2, PartitionType.Primary, 22020096, 41943040, "Windows OS", FileSystemType.NTFS),
|
|
new MockPartition(mbrTable, 1, PartitionType.Primary, 1048576, 10485760, "MBR Partition 1", FileSystemType.FAT32),
|
|
new MockPartition(mbrTable, 2, PartitionType.Primary, 11534336, 20971520, "MBR Partition 2", FileSystemType.NTFS)
|
|
};
|
|
|
|
// Create mock filesystems
|
|
var filesystems = new List<IFilesystem>
|
|
{
|
|
new MockFilesystem(partitions[0], FileSystemType.FAT32),
|
|
new MockFilesystem(partitions[1], FileSystemType.NTFS),
|
|
new MockFilesystem(partitions[2], FileSystemType.FAT32),
|
|
new MockFilesystem(partitions[3], FileSystemType.NTFS)
|
|
};
|
|
|
|
// Use reflection to set the private fields
|
|
var partitionsField = typeof(RestoreProvider).GetField("_partitions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
var filesystemsField = typeof(RestoreProvider).GetField("_filesystems", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
partitionsField?.SetValue(provider, partitions);
|
|
filesystemsField?.SetValue(provider, filesystems);
|
|
|
|
return provider;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mock implementation of IPartitionTable for testing.
|
|
/// </summary>
|
|
private class MockPartitionTable(PartitionTableType tableType) : IPartitionTable
|
|
{
|
|
public IRawDisk? RawDisk => null;
|
|
public PartitionTableType TableType { get; } = tableType;
|
|
|
|
public IAsyncEnumerable<IPartition> EnumeratePartitions(CancellationToken cancellationToken)
|
|
{
|
|
return AsyncEnumerable.Empty<IPartition>();
|
|
}
|
|
|
|
public Task<IPartition?> GetPartitionAsync(int partitionNumber, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<IPartition?>(null);
|
|
}
|
|
|
|
public Task<Stream> GetProtectiveMbrAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<Stream> GetPartitionTableDataAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public void Dispose() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mock implementation of IPartition for testing.
|
|
/// </summary>
|
|
private class MockPartition(IPartitionTable table, int number, PartitionType type, long startOffset, long size, string? name, FileSystemType fsType) : IPartition
|
|
{
|
|
public IPartitionTable PartitionTable { get; } = table;
|
|
public int PartitionNumber { get; } = number;
|
|
public PartitionType Type { get; } = type;
|
|
public long StartOffset { get; } = startOffset;
|
|
public long Size { get; } = size;
|
|
public string? Name { get; } = name;
|
|
public FileSystemType FilesystemType { get; } = fsType;
|
|
public Guid? VolumeGuid { get; } = Guid.NewGuid();
|
|
|
|
public Task<Stream> OpenReadAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<Stream> OpenWriteAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public void Dispose() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mock implementation of IFilesystem for testing.
|
|
/// </summary>
|
|
private class MockFilesystem(IPartition partition, FileSystemType type) : IFilesystem
|
|
{
|
|
public IPartition Partition { get; } = partition;
|
|
public FileSystemType Type { get; } = type;
|
|
|
|
public Task<object?> GetFilesystemMetadataAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<object?>(null);
|
|
}
|
|
|
|
public IAsyncEnumerable<IFile> ListFilesAsync(CancellationToken cancellationToken)
|
|
{
|
|
return AsyncEnumerable.Empty<IFile>();
|
|
}
|
|
|
|
public IAsyncEnumerable<IFile> ListFilesAsync(IFile directory, CancellationToken cancellationToken)
|
|
{
|
|
return AsyncEnumerable.Empty<IFile>();
|
|
}
|
|
|
|
public Task<Stream> OpenReadStreamAsync(IFile file, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<Stream> OpenWriteStreamAsync(IFile file, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<Stream> OpenReadWriteStreamAsync(IFile file, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<long> GetFileLengthAsync(IFile file, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(0L);
|
|
}
|
|
|
|
public Task<Stream> OpenReadStreamAsync(string path, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<Stream> OpenWriteStreamAsync(string path, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<Stream> OpenReadWriteStreamAsync(string path, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<Stream>(new MemoryStream());
|
|
}
|
|
|
|
public Task<long> GetFileLengthAsync(string path, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(0L);
|
|
}
|
|
|
|
public void Dispose() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePartition correctly parses a valid GPT partition segment.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePartition_ValidGPTPartition_ReturnsCorrectPartition()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
|
|
// Test parsing "part_GPT_1"
|
|
var result = parsePartitionMethod!.Invoke(provider, ["part_GPT_1"]);
|
|
Assert.IsNotNull(result, "Should return a partition.");
|
|
|
|
var partition = (IPartition)result!;
|
|
Assert.AreEqual(1, partition.PartitionNumber, "Partition number should be 1.");
|
|
Assert.AreEqual(PartitionTableType.GPT, partition.PartitionTable.TableType, "Partition table type should be GPT.");
|
|
Assert.AreEqual("EFI System", partition.Name, "Partition name should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePartition correctly parses a valid MBR partition segment.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePartition_ValidMBRPartition_ReturnsCorrectPartition()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
|
|
// Test parsing "part_MBR_2"
|
|
var result = parsePartitionMethod!.Invoke(provider, ["part_MBR_2"]);
|
|
Assert.IsNotNull(result, "Should return a partition.");
|
|
|
|
var partition = (IPartition)result!;
|
|
Assert.AreEqual(2, partition.PartitionNumber, "Partition number should be 2.");
|
|
Assert.AreEqual(PartitionTableType.MBR, partition.PartitionTable.TableType, "Partition table type should be MBR.");
|
|
Assert.AreEqual("MBR Partition 2", partition.Name, "Partition name should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePartition throws InvalidOperationException for malformed partition segments.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePartition_MalformedSegment_ThrowsInvalidOperationException()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
|
|
// Test with too few parts
|
|
var ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parsePartitionMethod!.Invoke(provider, ["part_GPT"]),
|
|
"Should throw for malformed segment.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
|
|
// Test with invalid partition table type
|
|
ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parsePartitionMethod!.Invoke(provider, ["part_INVALID_1"]),
|
|
"Should throw for invalid table type.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
|
|
// Test with non-numeric partition number
|
|
ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parsePartitionMethod!.Invoke(provider, ["part_GPT_ABC"]),
|
|
"Should throw for non-numeric partition number.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePartition throws InvalidOperationException for non-existent partitions.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePartition_NonExistentPartition_ThrowsInvalidOperationException()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
|
|
// Test with partition number that doesn't exist
|
|
var ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parsePartitionMethod!.Invoke(provider, ["part_GPT_99"]),
|
|
"Should throw for non-existent partition.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParseFilesystem correctly parses a valid NTFS filesystem segment.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParseFilesystem_ValidNTFS_ReturnsCorrectFilesystem()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
var parseFilesystemMethod = typeof(RestoreProvider).GetMethod("ParseFilesystem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
Assert.IsNotNull(parseFilesystemMethod, "ParseFilesystem method should exist.");
|
|
|
|
// Get a partition first
|
|
var partitionResult = parsePartitionMethod!.Invoke(provider, ["part_GPT_2"]);
|
|
Assert.IsNotNull(partitionResult, "Should return a partition.");
|
|
var partition = (IPartition)partitionResult!;
|
|
|
|
// Test parsing "fs_NTFS"
|
|
var result = parseFilesystemMethod!.Invoke(provider, [partition, "fs_NTFS"]);
|
|
Assert.IsNotNull(result, "Should return a filesystem.");
|
|
|
|
var filesystem = (IFilesystem)result!;
|
|
Assert.AreEqual(FileSystemType.NTFS, filesystem.Type, "Filesystem type should be NTFS.");
|
|
Assert.AreEqual(partition.PartitionNumber, filesystem.Partition.PartitionNumber, "Partition number should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParseFilesystem correctly parses a valid FAT32 filesystem segment.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParseFilesystem_ValidFAT32_ReturnsCorrectFilesystem()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
var parseFilesystemMethod = typeof(RestoreProvider).GetMethod("ParseFilesystem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
Assert.IsNotNull(parseFilesystemMethod, "ParseFilesystem method should exist.");
|
|
|
|
// Get a partition first
|
|
var partitionResult = parsePartitionMethod!.Invoke(provider, ["part_GPT_1"]);
|
|
Assert.IsNotNull(partitionResult, "Should return a partition.");
|
|
var partition = (IPartition)partitionResult!;
|
|
|
|
// Test parsing "fs_FAT32"
|
|
var result = parseFilesystemMethod!.Invoke(provider, [partition, "fs_FAT32"]);
|
|
Assert.IsNotNull(result, "Should return a filesystem.");
|
|
|
|
var filesystem = (IFilesystem)result!;
|
|
Assert.AreEqual(FileSystemType.FAT32, filesystem.Type, "Filesystem type should be FAT32.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParseFilesystem throws InvalidOperationException for malformed filesystem segments.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParseFilesystem_MalformedSegment_ThrowsInvalidOperationException()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
var parseFilesystemMethod = typeof(RestoreProvider).GetMethod("ParseFilesystem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
Assert.IsNotNull(parseFilesystemMethod, "ParseFilesystem method should exist.");
|
|
|
|
// Get a partition first
|
|
var partitionResult = parsePartitionMethod!.Invoke(provider, ["part_GPT_1"]);
|
|
Assert.IsNotNull(partitionResult, "Should return a partition.");
|
|
var partition = (IPartition)partitionResult!;
|
|
|
|
// Test with too few parts
|
|
var ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parseFilesystemMethod!.Invoke(provider, [partition, "fs"]),
|
|
"Should throw for malformed segment.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
|
|
// Test with invalid filesystem type
|
|
ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parseFilesystemMethod!.Invoke(provider, [partition, "fs_INVALID"]),
|
|
"Should throw for invalid filesystem type.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParseFilesystem throws InvalidOperationException for non-existent filesystems.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParseFilesystem_NonExistentFilesystem_ThrowsInvalidOperationException()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePartitionMethod = typeof(RestoreProvider).GetMethod("ParsePartition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
var parseFilesystemMethod = typeof(RestoreProvider).GetMethod("ParseFilesystem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePartitionMethod, "ParsePartition method should exist.");
|
|
Assert.IsNotNull(parseFilesystemMethod, "ParseFilesystem method should exist.");
|
|
|
|
// Get a partition that doesn't have an Ext4 filesystem
|
|
var partitionResult = parsePartitionMethod!.Invoke(provider, ["part_GPT_1"]);
|
|
Assert.IsNotNull(partitionResult, "Should return a partition.");
|
|
var partition = (IPartition)partitionResult!;
|
|
|
|
// Test with filesystem type that doesn't exist for this partition
|
|
var ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parseFilesystemMethod!.Invoke(provider, [partition, "fs_Ext4"]),
|
|
"Should throw for non-existent filesystem.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePath correctly identifies a geometry file path.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePath_GeometryFile_ReturnsGeometryType()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePathMethod = typeof(RestoreProvider).GetMethod("ParsePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePathMethod, "ParsePath method should exist.");
|
|
|
|
// Test with geometry.json file
|
|
var result = parsePathMethod!.Invoke(provider, ["geometry.json"]);
|
|
Assert.IsNotNull(result, "Should return a tuple.");
|
|
|
|
var tuple = ((string Type, IPartition? Partition, IFilesystem? Filesystem))result!;
|
|
Assert.AreEqual("geometry", tuple.Type, "Type should be 'geometry'.");
|
|
Assert.IsNull(tuple.Partition, "Partition should be null for geometry.");
|
|
Assert.IsNull(tuple.Filesystem, "Filesystem should be null for geometry.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePath correctly identifies a disk-level path.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePath_DiskPath_ReturnsDiskType()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePathMethod = typeof(RestoreProvider).GetMethod("ParsePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePathMethod, "ParsePath method should exist.");
|
|
|
|
// Test with root path
|
|
var result = parsePathMethod!.Invoke(provider, ["/"]);
|
|
Assert.IsNotNull(result, "Should return a tuple.");
|
|
|
|
var tuple = ((string Type, IPartition? Partition, IFilesystem? Filesystem))result!;
|
|
Assert.AreEqual("disk", tuple.Type, "Type should be 'disk'.");
|
|
Assert.IsNull(tuple.Partition, "Partition should be null for disk.");
|
|
Assert.IsNull(tuple.Filesystem, "Filesystem should be null for disk.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePath correctly identifies a partition path.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePath_PartitionPath_ReturnsPartitionType()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePathMethod = typeof(RestoreProvider).GetMethod("ParsePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePathMethod, "ParsePath method should exist.");
|
|
|
|
// Test with partition path (no filesystem segment)
|
|
var result = parsePathMethod!.Invoke(provider, ["part_GPT_1"]);
|
|
Assert.IsNotNull(result, "Should return a tuple.");
|
|
|
|
var tuple = ((string Type, IPartition? Partition, IFilesystem? Filesystem))result!;
|
|
Assert.AreEqual("partition", tuple.Type, "Type should be 'partition'.");
|
|
Assert.IsNotNull(tuple.Partition, "Partition should not be null.");
|
|
Assert.AreEqual(1, tuple.Partition!.PartitionNumber, "Partition number should be 1.");
|
|
Assert.IsNull(tuple.Filesystem, "Filesystem should be null for partition-only path.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePath correctly identifies a file path with partition and filesystem.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePath_FilePath_ReturnsFileType()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePathMethod = typeof(RestoreProvider).GetMethod("ParsePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePathMethod, "ParsePath method should exist.");
|
|
|
|
// Test with full file path
|
|
var result = parsePathMethod!.Invoke(provider, ["part_GPT_1/fs_FAT32/test/file.txt"]);
|
|
Assert.IsNotNull(result, "Should return a tuple.");
|
|
|
|
var tuple = ((string Type, IPartition? Partition, IFilesystem? Filesystem))result!;
|
|
Assert.AreEqual("file", tuple.Type, "Type should be 'file'.");
|
|
Assert.IsNotNull(tuple.Partition, "Partition should not be null.");
|
|
Assert.IsNotNull(tuple.Filesystem, "Filesystem should not be null.");
|
|
Assert.AreEqual(1, tuple.Partition!.PartitionNumber, "Partition number should be 1.");
|
|
Assert.AreEqual(FileSystemType.FAT32, tuple.Filesystem!.Type, "Filesystem type should be FAT32.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePath handles MBR partition paths correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePath_MBRPartitionPath_ReturnsCorrectPartition()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePathMethod = typeof(RestoreProvider).GetMethod("ParsePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePathMethod, "ParsePath method should exist.");
|
|
|
|
// Test with MBR partition path
|
|
var result = parsePathMethod!.Invoke(provider, ["part_MBR_2/fs_NTFS/data/file.dat"]);
|
|
Assert.IsNotNull(result, "Should return a tuple.");
|
|
|
|
var tuple = ((string Type, IPartition? Partition, IFilesystem? Filesystem))result!;
|
|
Assert.AreEqual("file", tuple.Type, "Type should be 'file'.");
|
|
Assert.IsNotNull(tuple.Partition, "Partition should not be null.");
|
|
Assert.IsNotNull(tuple.Filesystem, "Filesystem should not be null.");
|
|
Assert.AreEqual(2, tuple.Partition!.PartitionNumber, "Partition number should be 2.");
|
|
Assert.AreEqual(PartitionTableType.MBR, tuple.Partition.PartitionTable.TableType, "Table type should be MBR.");
|
|
Assert.AreEqual(FileSystemType.NTFS, tuple.Filesystem!.Type, "Filesystem type should be NTFS.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePath handles paths with different separators correctly on Windows.
|
|
/// On Windows, both forward slash and backslash are valid path separators.
|
|
/// On Linux/macOS, backslash is a valid filename character, not a separator.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePath_DifferentSeparators_NormalizesCorrectly()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePathMethod = typeof(RestoreProvider).GetMethod("ParsePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePathMethod, "ParsePath method should exist.");
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
// On Windows, backslash is a path separator
|
|
var result = parsePathMethod!.Invoke(provider, ["part_GPT_1\\fs_FAT32\\test\\file.txt"]);
|
|
Assert.IsNotNull(result, "Should return a tuple.");
|
|
|
|
var tuple = ((string Type, IPartition? Partition, IFilesystem? Filesystem))result!;
|
|
Assert.AreEqual("file", tuple.Type, "Type should be 'file'.");
|
|
Assert.IsNotNull(tuple.Partition, "Partition should not be null.");
|
|
Assert.IsNotNull(tuple.Filesystem, "Filesystem should not be null.");
|
|
}
|
|
else
|
|
{
|
|
// On Linux/macOS, backslash is a filename character, not a separator
|
|
// The entire string becomes one segment, which fails partition parsing
|
|
var ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parsePathMethod!.Invoke(provider, ["part_GPT_1\\fs_FAT32\\test\\file.txt"]),
|
|
"Should throw for backslash path on non-Windows platforms.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ParsePath throws appropriate exceptions for invalid paths.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RestoreProvider_ParsePath_InvalidPartitionSegment_ThrowsException()
|
|
{
|
|
using var provider = CreateRestoreProviderForPathParsingTests();
|
|
var parsePathMethod = typeof(RestoreProvider).GetMethod("ParsePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
|
|
Assert.IsNotNull(parsePathMethod, "ParsePath method should exist.");
|
|
|
|
// Test with invalid partition segment format - this should still parse as file type
|
|
// but will fail when trying to parse the partition
|
|
var ex = Assert.Throws<TargetInvocationException>(() =>
|
|
parsePathMethod!.Invoke(provider, ["part_INVALID/fs_NTFS/file.txt"]),
|
|
"Should throw for invalid partition segment.");
|
|
Assert.IsInstanceOf<InvalidOperationException>(ex!.InnerException, "Inner exception should be InvalidOperationException.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Case and Boundary Tests
|
|
|
|
/// <summary>
|
|
/// Tests reading at offset 0 (first sector) returns correct data.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadAtOffsetZero_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern to sector 0 (first sector)
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)(i & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(0, writeBuffer, CancellationToken.None);
|
|
|
|
// Read at offset 0
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(0, sectorSize, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data at offset 0 should match what was written.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests writing at offset 0 (first sector) works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteAtOffsetZero_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write at offset 0
|
|
var writeBuffer = new byte[sectorSize];
|
|
new Random(123).NextBytes(writeBuffer);
|
|
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(0, writeBuffer, CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesWritten, "Should have written one sector.");
|
|
|
|
// Read back and verify
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data at offset 0 should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests reading at the last sector of the disk returns correct data.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadAtLastSector_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
var lastSector = (diskSize / sectorSize) - 1;
|
|
|
|
// Write known pattern to last sector
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)((i + 100) & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(lastSector, writeBuffer, CancellationToken.None);
|
|
|
|
// Read at last sector offset
|
|
var lastSectorOffset = lastSector * sectorSize;
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(lastSectorOffset, sectorSize, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data at last sector should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests writing at the last sector of the disk works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteAtLastSector_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
var lastSector = (diskSize / sectorSize) - 1;
|
|
|
|
// Write at last sector
|
|
var writeBuffer = new byte[sectorSize];
|
|
new Random(456).NextBytes(writeBuffer);
|
|
|
|
var bytesWritten = await _writableRawDisk.WriteSectorsAsync(lastSector, writeBuffer, CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesWritten, "Should have written one sector at last position.");
|
|
|
|
// Read back and verify
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(lastSector, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data at last sector should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests reading exactly one sector returns correct data.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadExactlyOneSector_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write known pattern to sector 5
|
|
var writeBuffer = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)((i * 3) & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(5, writeBuffer, CancellationToken.None);
|
|
|
|
// Read exactly one sector using ReadBytesAsync
|
|
var offset = 5 * sectorSize;
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(offset, sectorSize, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read exactly one sector.");
|
|
Assert.AreEqual(sectorSize, stream.Length, "Stream length should be exactly one sector.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data should match exactly.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests writing exactly one sector works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteExactlyOneSector_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write exactly one sector using WriteBytesAsync
|
|
var writeBuffer = new byte[sectorSize];
|
|
new Random(789).NextBytes(writeBuffer);
|
|
|
|
var offset = 10 * sectorSize;
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(offset, writeBuffer, CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesWritten, "Should have written exactly one sector.");
|
|
|
|
// Read back and verify
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(10, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, "Data should match exactly.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests reading the full disk size works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadFullDiskSize_ReturnsCorrectData()
|
|
{
|
|
// Use a smaller test disk for this test to avoid memory issues
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
|
|
// Read full disk in chunks to verify it works
|
|
const int chunkSectors = 10;
|
|
var chunkSize = chunkSectors * sectorSize;
|
|
var totalSectors = diskSize / sectorSize;
|
|
var sectorsRead = 0L;
|
|
|
|
for (long sector = 0; sector < totalSectors; sector += chunkSectors)
|
|
{
|
|
var sectorsToRead = (int)Math.Min(chunkSectors, totalSectors - sector);
|
|
using var stream = await _writableRawDisk.ReadSectorsAsync(sector, sectorsToRead, CancellationToken.None);
|
|
|
|
var buffer = new byte[sectorsToRead * sectorSize];
|
|
var bytesRead = await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorsToRead * sectorSize, bytesRead, $"Should have read {sectorsToRead} sectors at offset {sector}.");
|
|
sectorsRead += sectorsToRead;
|
|
}
|
|
|
|
Assert.AreEqual(totalSectors, sectorsRead, "Should have read all sectors.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests writing the full disk size works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteFullDiskSize_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
|
|
// Write a unique pattern to each sector and verify
|
|
var totalSectors = diskSize / sectorSize;
|
|
const int testSectors = 20; // Test a subset to keep test fast
|
|
|
|
for (long sector = 0; sector < Math.Min(testSectors, totalSectors); sector++)
|
|
{
|
|
// Create unique pattern for this sector
|
|
var writeBuffer = new byte[sectorSize];
|
|
var patternByte = (byte)(sector & 0xFF);
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeBuffer[i] = (byte)((patternByte + i) & 0xFF);
|
|
|
|
await _writableRawDisk.WriteSectorsAsync(sector, writeBuffer, CancellationToken.None);
|
|
|
|
// Read back and verify
|
|
using var readStream = await _writableRawDisk.ReadSectorsAsync(sector, 1, CancellationToken.None);
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(sectorSize, bytesRead, $"Should read sector {sector}.");
|
|
Assert.AreEqual(writeBuffer, readBuffer, $"Data at sector {sector} should match.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests writing empty data (0 bytes) returns 0 without error.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_WriteEmptyData_ReturnsZero()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Write empty data
|
|
var emptyData = Array.Empty<byte>();
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(sectorSize, emptyData, CancellationToken.None);
|
|
|
|
Assert.AreEqual(0, bytesWritten, "Writing empty data should return 0 bytes written.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests reading empty data (0 bytes) returns 0 without error.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_ReadEmptyData_ReturnsZero()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Read 0 bytes
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(sectorSize, 0, CancellationToken.None);
|
|
var buffer = Array.Empty<byte>();
|
|
var bytesRead = await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(0, bytesRead, "Reading 0 bytes should return 0.");
|
|
Assert.AreEqual(0, stream.Length, "Stream length should be 0 for empty read.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that a disk with minimum possible size still supports basic operations.
|
|
/// Creates a small test disk and verifies read/write operations work.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_MinimumSizeDisk_OperationsWork()
|
|
{
|
|
// The minimum practical disk size is at least a few sectors
|
|
// We'll use the writable disk which is already created at 100 MiB
|
|
// but verify operations work at the boundaries
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
|
|
// Minimum operations: read/write first sector
|
|
var firstSectorData = new byte[sectorSize];
|
|
new Random(111).NextBytes(firstSectorData);
|
|
await _writableRawDisk.WriteSectorsAsync(0, firstSectorData, CancellationToken.None);
|
|
|
|
using var firstReadStream = await _writableRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
|
|
var firstReadBuffer = new byte[sectorSize];
|
|
var firstBytesRead = await firstReadStream.ReadAsync(firstReadBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, firstBytesRead, "Should read exactly one sector.");
|
|
Assert.AreEqual(firstSectorData, firstReadBuffer, "First sector should be readable/writable.");
|
|
|
|
// Minimum operations: read/write last sector
|
|
var lastSector = (diskSize / sectorSize) - 1;
|
|
var lastSectorData = new byte[sectorSize];
|
|
new Random(222).NextBytes(lastSectorData);
|
|
await _writableRawDisk.WriteSectorsAsync(lastSector, lastSectorData, CancellationToken.None);
|
|
|
|
using var lastReadStream = await _writableRawDisk.ReadSectorsAsync(lastSector, 1, CancellationToken.None);
|
|
var lastReadBuffer = new byte[sectorSize];
|
|
var lastBytesRead = await lastReadStream.ReadAsync(lastReadBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, lastBytesRead, "Should read exactly one sector.");
|
|
Assert.AreEqual(lastSectorData, lastReadBuffer, "Last sector should be readable/writable.");
|
|
|
|
// Verify disk size is reported correctly
|
|
Assert.Greater(diskSize, 0, "Disk size should be greater than 0.");
|
|
Assert.AreEqual(0, diskSize % sectorSize, "Disk size should be sector-aligned.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests boundary condition: reading at exactly the disk size boundary should fail gracefully.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RawDisk_ReadAtDiskSizeBoundary_ThrowsInvalidOperationException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
|
|
// Attempting to read at or beyond disk size should throw InvalidOperationException
|
|
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
|
{
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(diskSize, sectorSize, CancellationToken.None);
|
|
var buffer = new byte[sectorSize];
|
|
var _ = await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
|
|
}, "Reading at disk boundary should throw InvalidOperationException.");
|
|
|
|
StringAssert.Contains("beyond disk size", ex!.Message, "Exception message should mention beyond disk size.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests boundary condition: writing at exactly the disk size boundary should fail gracefully.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_RawDisk_WriteAtDiskSizeBoundary_ThrowsInvalidOperationException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var diskSize = _writableRawDisk.Size;
|
|
|
|
// Attempting to write at or beyond disk size should throw InvalidOperationException
|
|
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
|
{
|
|
var writeData = new byte[sectorSize];
|
|
await _writableRawDisk.WriteBytesAsync(diskSize, writeData, CancellationToken.None);
|
|
}, "Writing at disk boundary should throw InvalidOperationException.");
|
|
|
|
StringAssert.Contains("beyond disk size", ex!.Message, "Exception message should mention beyond disk size.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests reading/writing a single byte at various boundary positions.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_RawDisk_SingleByteAtBoundaries_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
|
|
// Test positions: first byte of disk, last byte of first sector,
|
|
// first byte of second sector, and various other positions
|
|
var testPositions = new[]
|
|
{
|
|
0L, // First byte of disk
|
|
sectorSize - 1, // Last byte of first sector
|
|
sectorSize, // First byte of second sector
|
|
sectorSize + 1, // Second byte of second sector
|
|
(2 * sectorSize) - 1, // Last byte of second sector
|
|
2 * sectorSize // First byte of third sector
|
|
};
|
|
|
|
for (int i = 0; i < testPositions.Length; i++)
|
|
{
|
|
var position = testPositions[i];
|
|
var testByte = (byte)(0xA0 + i);
|
|
|
|
// Write single byte
|
|
var writeData = new[] { testByte };
|
|
var bytesWritten = await _writableRawDisk.WriteBytesAsync(position, writeData, CancellationToken.None);
|
|
Assert.AreEqual(1, bytesWritten, $"Should write 1 byte at position {position}.");
|
|
|
|
// Read back the byte
|
|
using var stream = await _writableRawDisk.ReadBytesAsync(position, 1, CancellationToken.None);
|
|
var readBuffer = new byte[1];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
|
|
Assert.AreEqual(1, bytesRead, $"Should read 1 byte at position {position}.");
|
|
Assert.AreEqual(testByte, readBuffer[0], $"Byte at position {position} should match.");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Coverage increasing tests
|
|
|
|
[Test]
|
|
public async Task Test_GPT_GetProtectiveMbrAsync_ReturnsValidMbr()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
|
|
// Read enough sectors to parse GPT
|
|
var sectorsToRead = 34;
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, sectorsToRead, CancellationToken.None);
|
|
var diskBytes = new byte[sectorSize * sectorsToRead];
|
|
await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
// Parse GPT
|
|
var gpt = new GPT(s_gptRawDisk);
|
|
var parsed = await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
Assert.IsTrue(parsed, "GPT should parse successfully.");
|
|
|
|
// Get protective MBR
|
|
using var mbrStream = await gpt.GetProtectiveMbrAsync(CancellationToken.None);
|
|
Assert.IsNotNull(mbrStream, "Protective MBR stream should not be null.");
|
|
Assert.Greater(mbrStream.Length, 0, "Protective MBR stream should have data.");
|
|
|
|
// Read MBR bytes and verify boot signature
|
|
var mbrBytes = new byte[mbrStream.Length];
|
|
var bytesRead = await mbrStream.ReadAsync(mbrBytes.AsMemory(), CancellationToken.None);
|
|
Assert.Greater(bytesRead, 0, "Should have read MBR bytes.");
|
|
|
|
// Verify MBR boot signature (0x55AA at offset 510-511)
|
|
if (bytesRead >= 512)
|
|
{
|
|
Assert.AreEqual(0x55, mbrBytes[510], "MBR boot signature first byte should be 0x55.");
|
|
Assert.AreEqual(0xAA, mbrBytes[511], "MBR boot signature second byte should be 0xAA.");
|
|
}
|
|
|
|
gpt.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_GPT_GetPartitionTableDataAsync_ReturnsValidData()
|
|
{
|
|
var sectorSize = s_gptRawDisk!.SectorSize;
|
|
|
|
// Read enough sectors to parse GPT
|
|
var sectorsToRead = 34;
|
|
using var stream = await s_gptRawDisk.ReadSectorsAsync(0, sectorsToRead, CancellationToken.None);
|
|
var diskBytes = new byte[sectorSize * sectorsToRead];
|
|
await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None);
|
|
|
|
// Parse GPT with raw disk reference
|
|
var gpt = new GPT(s_gptRawDisk);
|
|
var parsed = await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None);
|
|
Assert.IsTrue(parsed, "GPT should parse successfully.");
|
|
|
|
// Get partition table data
|
|
using var ptStream = await gpt.GetPartitionTableDataAsync(CancellationToken.None);
|
|
Assert.IsNotNull(ptStream, "Partition table data stream should not be null.");
|
|
Assert.Greater(ptStream.Length, 0, "Partition table data should have content.");
|
|
|
|
// Read the data and verify it contains the GPT signature
|
|
var ptBytes = new byte[ptStream.Length];
|
|
var bytesRead = await ptStream.ReadAsync(ptBytes.AsMemory(), CancellationToken.None);
|
|
Assert.Greater(bytesRead, sectorSize, "Should have read more than one sector of partition table data.");
|
|
|
|
// Verify GPT signature "EFI PART" at sector 1 (offset 512)
|
|
var expectedSignature = "EFI PART"u8.ToArray();
|
|
for (int i = 0; i < 8; i++)
|
|
Assert.AreEqual(expectedSignature[i], ptBytes[sectorSize + i], $"GPT signature byte {i} should match.");
|
|
|
|
gpt.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_GetFilesystemMetadataAsync_ReturnsCorrectMetadata()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 1024 * 1024;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
var metadata = await fs.GetFilesystemMetadataAsync(CancellationToken.None);
|
|
Assert.IsNotNull(metadata, "Metadata should not be null.");
|
|
Assert.IsInstanceOf<UnkownFilesystemMetadata>(metadata, "Metadata should be UnkownFilesystemMetadata.");
|
|
|
|
var fsMetadata = (UnkownFilesystemMetadata)metadata!;
|
|
Assert.AreEqual(blockSize, fsMetadata.BlockSize, "BlockSize should match the configured value.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_GetFileLengthAsync_ReturnsCorrectSize()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Get first file
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
var length = await fs.GetFileLengthAsync(firstFile!, CancellationToken.None);
|
|
Assert.AreEqual(firstFile!.Size, length, "GetFileLengthAsync should return the file's size.");
|
|
Assert.AreEqual(1024 * 1024, length, "First file should be 1MB (full block).");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_SetLength_ThrowsNotSupported()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None);
|
|
Assert.Throws<NotSupportedException>(() => writeStream.SetLength(100),
|
|
"SetLength should throw NotSupportedException on UnknownFilesystemStream.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_WriteOnReadOnly_ThrowsNotSupported()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
Assert.IsFalse(readStream.CanWrite, "Read stream should not be writable.");
|
|
Assert.Throws<NotSupportedException>(() => readStream.Write(new byte[10], 0, 10),
|
|
"Write on read-only stream should throw NotSupportedException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_ReadOnWriteOnly_ThrowsNotSupported()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None);
|
|
Assert.IsFalse(writeStream.CanRead, "Write stream should not be readable.");
|
|
Assert.Throws<NotSupportedException>(() => { var _ = writeStream.Read(new byte[10], 0, 10); },
|
|
"Read on write-only stream should throw NotSupportedException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_DisposeAsync_FlushesDirtyData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = sectorSize * 2;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write data using DisposeAsync (await using)
|
|
var writeData = new byte[blockSize];
|
|
new Random(99).NextBytes(writeData);
|
|
|
|
await using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
}
|
|
|
|
// Read back and verify data was flushed via DisposeAsync
|
|
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
|
|
{
|
|
var readData = new byte[blockSize];
|
|
var bytesRead = await readStream.ReadAsync(readData.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(blockSize, bytesRead, "Should have read the full block.");
|
|
Assert.AreEqual(writeData, readData, "Data should match after DisposeAsync flush.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Test_UnknownFilesystem_DoubleDispose_DoesNotThrow()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
fs.Dispose();
|
|
|
|
// Second dispose should not throw
|
|
Assert.DoesNotThrow(() => fs.Dispose(), "Double dispose should not throw.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_UnknownFilesystem_Type_ReturnsUnknown()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
Assert.AreEqual(FileSystemType.Unknown, fs.Type, "Type should be Unknown.");
|
|
Assert.AreEqual(partition, fs.Partition, "Partition should match the one passed to constructor.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_PositionSetter_ValidatesBounds()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
|
|
// Valid position
|
|
readStream.Position = 100;
|
|
Assert.AreEqual(100, readStream.Position, "Position should be 100.");
|
|
|
|
// Position at end (equal to size) should be valid
|
|
readStream.Position = blockSize;
|
|
Assert.AreEqual(blockSize, readStream.Position, "Position at end should be valid.");
|
|
|
|
// Negative position should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => readStream.Position = -1,
|
|
"Negative position should throw ArgumentOutOfRangeException.");
|
|
|
|
// Position beyond size should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => readStream.Position = blockSize + 1,
|
|
"Position beyond size should throw ArgumentOutOfRangeException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_Flush_DoesNotThrow()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None);
|
|
Assert.DoesNotThrow(() => writeStream.Flush(), "Flush should be a no-op and not throw.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PartitionWriteStream_Flush_DoesNotThrow()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
|
|
|
|
Assert.DoesNotThrow(() => stream.Flush(), "Flush should be a no-op and not throw.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_WriteBeyondSize_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None);
|
|
|
|
// Try to write more data than the stream size
|
|
var largeData = new byte[blockSize + 100];
|
|
Assert.Throws<ArgumentException>(() => writeStream.Write(largeData, 0, largeData.Length),
|
|
"Writing beyond stream size should throw ArgumentException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_SyncRead_ReturnsCorrectData()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write test data
|
|
var writeData = new byte[blockSize];
|
|
for (int i = 0; i < blockSize; i++)
|
|
writeData[i] = (byte)(i & 0xFF);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Read using synchronous Read method
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
var readBuffer = new byte[256];
|
|
var bytesRead = readStream.Read(readBuffer, 0, 256);
|
|
|
|
Assert.AreEqual(256, bytesRead, "Sync Read should return 256 bytes.");
|
|
for (int i = 0; i < 256; i++)
|
|
Assert.AreEqual((byte)(i & 0xFF), readBuffer[i], $"Byte at position {i} should match.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_SyncWrite_DataMatches()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
// Write using synchronous Write method
|
|
var writeData = new byte[100];
|
|
new Random(42).NextBytes(writeData);
|
|
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
writeStream.Write(writeData, 0, writeData.Length);
|
|
|
|
// Read back and verify
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
var readBuffer = new byte[100];
|
|
var bytesRead = readStream.Read(readBuffer, 0, 100);
|
|
|
|
Assert.AreEqual(100, bytesRead, "Should read 100 bytes.");
|
|
Assert.AreEqual(writeData, readBuffer, "Sync-written data should match on read-back.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PooledMemoryStream_Flush_DoesNotThrow()
|
|
{
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(100))
|
|
buffer = pooledBuffer.Array;
|
|
|
|
using var stream = new PooledMemoryStream(buffer, 100);
|
|
Assert.DoesNotThrow(() => stream.Flush(), "Flush should be a no-op and not throw.");
|
|
}
|
|
|
|
[Test]
|
|
public void Test_PooledBuffer_SpanAndMemory_ReturnCorrectSize()
|
|
{
|
|
using var buffer = new PooledBuffer(512);
|
|
|
|
Assert.AreEqual(512, buffer.Span.Length, "Span length should match requested size.");
|
|
Assert.AreEqual(512, buffer.Memory.Length, "Memory length should match requested size.");
|
|
|
|
// Write via Span, read via Memory
|
|
buffer.Span[0] = 0xAA;
|
|
buffer.Span[511] = 0xBB;
|
|
|
|
Assert.AreEqual(0xAA, buffer.Memory.Span[0], "Memory should reflect Span writes.");
|
|
Assert.AreEqual(0xBB, buffer.Memory.Span[511], "Memory should reflect Span writes.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable stores the correct table type and raw disk reference.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_Constructor_SetsProperties()
|
|
{
|
|
var metadata = new GeometryMetadata
|
|
{
|
|
Disk = new DiskGeometry { SectorSize = 512, Size = 50 * MiB },
|
|
PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.GPT }
|
|
};
|
|
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.AreEqual(PartitionTableType.GPT, table.TableType, "TableType should be GPT.");
|
|
Assert.AreEqual(_writableRawDisk, table.RawDisk, "RawDisk should match the provided disk.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.EnumeratePartitions throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_EnumeratePartitions_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.Throws<NotSupportedException>(() =>
|
|
{
|
|
// Force enumeration to trigger the throw
|
|
var enumerator = table.EnumeratePartitions(CancellationToken.None).GetAsyncEnumerator();
|
|
enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult();
|
|
}, "EnumeratePartitions should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetPartitionAsync throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_GetPartitionAsync_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetPartitionAsync(1, CancellationToken.None);
|
|
}, "GetPartitionAsync should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetProtectiveMbrAsync throws NotSupportedException for GPT.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_GetProtectiveMbrAsync_GPT_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetProtectiveMbrAsync(CancellationToken.None);
|
|
}, "GetProtectiveMbrAsync should throw NotSupportedException for GPT.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetProtectiveMbrAsync throws NotSupportedException for MBR.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_GetProtectiveMbrAsync_MBR_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.MBR);
|
|
|
|
var ex = Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetProtectiveMbrAsync(CancellationToken.None);
|
|
}, "GetProtectiveMbrAsync should throw NotSupportedException for MBR.");
|
|
|
|
StringAssert.Contains("MBR", ex!.Message, "Exception message should mention MBR.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetPartitionTableDataAsync throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_GetPartitionTableDataAsync_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetPartitionTableDataAsync(CancellationToken.None);
|
|
}, "GetPartitionTableDataAsync should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.Dispose can be called multiple times without error.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_DoubleDispose_DoesNotThrow()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
table.Dispose();
|
|
Assert.DoesNotThrow(() => table.Dispose(), "Double dispose should not throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BasePartition.OpenReadAsync returns a readable stream.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_BasePartition_OpenReadAsync_ReturnsReadableStream()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 10L * sectorSize;
|
|
var partitionSize = 4L * sectorSize;
|
|
|
|
// Write known data to the partition area
|
|
var writeData = new byte[sectorSize];
|
|
for (int i = 0; i < sectorSize; i++)
|
|
writeData[i] = (byte)(i & 0xFF);
|
|
await _writableRawDisk.WriteBytesAsync(partitionOffset, writeData, CancellationToken.None);
|
|
|
|
var partition = new BasePartition
|
|
{
|
|
PartitionNumber = 1,
|
|
Type = PartitionType.Primary,
|
|
PartitionTable = new TestPartitionTable { RawDisk = _writableRawDisk },
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var stream = await partition.OpenReadAsync(CancellationToken.None);
|
|
Assert.IsNotNull(stream, "OpenReadAsync should return a stream.");
|
|
Assert.IsTrue(stream.CanRead, "Stream should be readable.");
|
|
|
|
var readBuffer = new byte[sectorSize];
|
|
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(sectorSize, bytesRead, "Should read one sector.");
|
|
Assert.AreEqual(writeData, readBuffer, "Data should match what was written.");
|
|
|
|
partition.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BasePartition.OpenWriteAsync returns a writable PartitionWriteStream.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_BasePartition_OpenWriteAsync_ReturnsWritableStream()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionOffset = 10L * sectorSize;
|
|
var partitionSize = 4L * sectorSize;
|
|
|
|
var partition = new BasePartition
|
|
{
|
|
PartitionNumber = 1,
|
|
Type = PartitionType.Primary,
|
|
PartitionTable = new TestPartitionTable { RawDisk = _writableRawDisk },
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var stream = await partition.OpenWriteAsync(CancellationToken.None);
|
|
Assert.IsNotNull(stream, "OpenWriteAsync should return a stream.");
|
|
Assert.IsTrue(stream.CanWrite, "Stream should be writable.");
|
|
Assert.IsInstanceOf<PartitionWriteStream>(stream, "Stream should be a PartitionWriteStream.");
|
|
|
|
partition.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetProtectiveMbrAsync throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownPartitionTable_GetProtectiveMbrAsync_ThrowsNotSupported()
|
|
{
|
|
using var table = new UnknownPartitionTable(null);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetProtectiveMbrAsync(CancellationToken.None);
|
|
}, "GetProtectiveMbrAsync should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionTableDataAsync returns empty data.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionTableDataAsync_ReturnsEmptyStream()
|
|
{
|
|
using var table = new UnknownPartitionTable(null);
|
|
|
|
using var stream = await table.GetPartitionTableDataAsync(CancellationToken.None);
|
|
Assert.IsNotNull(stream, "Stream should not be null.");
|
|
Assert.AreEqual(0, stream.Length, "Stream should be empty.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.Dispose can be called multiple times.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownPartitionTable_DoubleDispose_DoesNotThrow()
|
|
{
|
|
var table = new UnknownPartitionTable(null);
|
|
table.Dispose();
|
|
Assert.DoesNotThrow(() => table.Dispose(), "Double dispose should not throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionAsync returns null for invalid partition numbers.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionAsync_InvalidNumber_ReturnsNull()
|
|
{
|
|
using var table = new UnknownPartitionTable(null);
|
|
|
|
var result = await table.GetPartitionAsync(2, CancellationToken.None);
|
|
Assert.IsNull(result, "Should return null for partition number != 1.");
|
|
|
|
var result0 = await table.GetPartitionAsync(0, CancellationToken.None);
|
|
Assert.IsNull(result0, "Should return null for partition number 0.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionAsync returns null when no raw disk is set.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionAsync_NullDisk_ReturnsNull()
|
|
{
|
|
using var table = new UnknownPartitionTable(null);
|
|
|
|
var result = await table.GetPartitionAsync(1, CancellationToken.None);
|
|
Assert.IsNull(result, "Should return null when raw disk is null.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionAsync returns a valid partition when disk is set.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionAsync_ValidDisk_ReturnsPartition()
|
|
{
|
|
using var table = new UnknownPartitionTable(_writableRawDisk);
|
|
|
|
var result = await table.GetPartitionAsync(1, CancellationToken.None);
|
|
Assert.IsNotNull(result, "Should return a partition when disk is available.");
|
|
Assert.AreEqual(1, result!.PartitionNumber, "Partition number should be 1.");
|
|
Assert.AreEqual(_writableRawDisk.Size, result.Size, "Partition size should equal disk size.");
|
|
Assert.AreEqual(0, result.StartOffset, "Partition start offset should be 0.");
|
|
|
|
result.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream.Position setter validates bounds correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_PositionSetter_ValidatesBounds()
|
|
{
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(100))
|
|
buffer = pooledBuffer.Array;
|
|
|
|
using var stream = new PooledMemoryStream(buffer, 100);
|
|
|
|
// Valid position
|
|
stream.Position = 50;
|
|
Assert.AreEqual(50, stream.Position, "Position should be 50.");
|
|
|
|
// Position at end
|
|
stream.Position = 100;
|
|
Assert.AreEqual(100, stream.Position, "Position at end should be valid.");
|
|
|
|
// Position at start
|
|
stream.Position = 0;
|
|
Assert.AreEqual(0, stream.Position, "Position at start should be valid.");
|
|
|
|
// Negative position should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = -1,
|
|
"Negative position should throw.");
|
|
|
|
// Position beyond length should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = 101,
|
|
"Position beyond length should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream.Seek with invalid origin throws ArgumentOutOfRangeException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PooledMemoryStream_Seek_InvalidOrigin_ThrowsArgumentOutOfRange()
|
|
{
|
|
byte[] buffer;
|
|
using (var pooledBuffer = new PooledBuffer(100))
|
|
buffer = pooledBuffer.Array;
|
|
|
|
using var stream = new PooledMemoryStream(buffer, 100);
|
|
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(0, (SeekOrigin)99),
|
|
"Invalid SeekOrigin should throw ArgumentOutOfRangeException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PartitionWriteStream.Position setter validates bounds correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PartitionWriteStream_PositionSetter_ValidatesBounds()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 2 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Valid position
|
|
stream.Position = 100;
|
|
Assert.AreEqual(100, stream.Position, "Position should be 100.");
|
|
|
|
// Position at max size
|
|
stream.Position = partitionSize;
|
|
Assert.AreEqual(partitionSize, stream.Position, "Position at max size should be valid.");
|
|
|
|
// Negative position should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = -1,
|
|
"Negative position should throw.");
|
|
|
|
// Position beyond max size should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = partitionSize + 1,
|
|
"Position beyond max size should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PartitionWriteStream.Seek with invalid origin throws ArgumentOutOfRangeException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PartitionWriteStream_Seek_InvalidOrigin_ThrowsArgumentOutOfRange()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
|
|
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(0, (SeekOrigin)99),
|
|
"Invalid SeekOrigin should throw ArgumentOutOfRangeException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PartitionWriteStream.SetLength adjusts position when it exceeds new length.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PartitionWriteStream_SetLength_AdjustsPosition()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 4 * sectorSize;
|
|
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
|
|
|
|
// Write some data to move position
|
|
stream.Write(new byte[200], 0, 200);
|
|
Assert.AreEqual(200, stream.Position, "Position should be 200.");
|
|
|
|
// Set length to less than current position
|
|
stream.SetLength(100);
|
|
Assert.AreEqual(100, stream.Length, "Length should be 100.");
|
|
Assert.AreEqual(100, stream.Position, "Position should be adjusted to new length.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based OpenReadStreamAsync works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenReadStreamAsync_ByPath_ReturnsStream()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 1024 * 1024;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
// Write data first using IFile-based API
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
var writeData = new byte[100];
|
|
new Random(42).NextBytes(writeData);
|
|
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
|
|
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
|
|
|
|
// Read using path-based API
|
|
var path = $"root/part_Unknown_1/fs_Unknown/{((UnknownFilesystemFile)firstFile!).Address:X}";
|
|
using var readStream = await fs.OpenReadStreamAsync(path, CancellationToken.None);
|
|
Assert.IsNotNull(readStream, "Path-based OpenReadStreamAsync should return a stream.");
|
|
|
|
var readBuffer = new byte[100];
|
|
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
|
|
Assert.AreEqual(100, bytesRead, "Should read 100 bytes.");
|
|
Assert.AreEqual(writeData, readBuffer, "Data should match.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based OpenWriteStreamAsync works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenWriteStreamAsync_ByPath_ReturnsStream()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Write using path-based API
|
|
var path = $"root/part_Unknown_1/fs_Unknown/0";
|
|
using var writeStream = await fs.OpenWriteStreamAsync(path, CancellationToken.None);
|
|
Assert.IsNotNull(writeStream, "Path-based OpenWriteStreamAsync should return a stream.");
|
|
Assert.IsTrue(writeStream.CanWrite, "Stream should be writable.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based OpenReadWriteStreamAsync works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenReadWriteStreamAsync_ByPath_ReturnsStream()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Open read-write using path-based API
|
|
var path = $"root/part_Unknown_1/fs_Unknown/0";
|
|
using var stream = await fs.OpenReadWriteStreamAsync(path, CancellationToken.None);
|
|
Assert.IsNotNull(stream, "Path-based OpenReadWriteStreamAsync should return a stream.");
|
|
Assert.IsTrue(stream.CanRead, "Stream should be readable.");
|
|
Assert.IsTrue(stream.CanWrite, "Stream should be writable.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based GetFileLengthAsync returns correct size.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_GetFileLengthAsync_ByPath_ReturnsCorrectSize()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 1024 * 1024;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
// Get file length using path-based API
|
|
var path = $"root/part_Unknown_1/fs_Unknown/0";
|
|
var length = await fs.GetFileLengthAsync(path, CancellationToken.None);
|
|
Assert.AreEqual(blockSize, length, "File length should equal block size for first block.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.GetFileLengthAsync throws for non-UnknownFilesystemFile (including directories).
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_GetFileLengthAsync_NonUnknownFile_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// A directory file that is not UnknownFilesystemFile should throw "does not belong"
|
|
var dirFile = new MockDirectoryFile();
|
|
|
|
var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
|
|
{
|
|
await fs.GetFileLengthAsync(dirFile, CancellationToken.None);
|
|
}, "GetFileLengthAsync should throw for non-UnknownFilesystemFile.");
|
|
|
|
StringAssert.Contains("does not belong", ex!.Message, "Exception message should mention file doesn't belong.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.GetFileLengthAsync throws for non-UnknownFilesystemFile.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_GetFileLengthAsync_WrongFileType_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Create a mock file that is not UnknownFilesystemFile
|
|
var wrongFile = new MockNonUnknownFile();
|
|
|
|
var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
|
|
{
|
|
await fs.GetFileLengthAsync(wrongFile, CancellationToken.None);
|
|
}, "GetFileLengthAsync should throw for non-UnknownFilesystemFile.");
|
|
|
|
StringAssert.Contains("does not belong", ex!.Message, "Exception message should mention file doesn't belong.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystemStream.Seek with invalid origin throws ArgumentException.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_Seek_InvalidOrigin_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 4096);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var stream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
|
|
Assert.Throws<ArgumentException>(() => stream.Seek(0, (SeekOrigin)99),
|
|
"Invalid SeekOrigin should throw ArgumentException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystemStream.Seek beyond bounds throws ArgumentOutOfRangeException.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_Seek_BeyondBounds_ThrowsArgumentOutOfRange()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
var blockSize = 4096;
|
|
using var fs = new UnknownFilesystem(partition, blockSize: blockSize);
|
|
|
|
IFile? firstFile = null;
|
|
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
|
|
{
|
|
firstFile = file;
|
|
break;
|
|
}
|
|
Assert.IsNotNull(firstFile, "Should have at least one file.");
|
|
|
|
using var stream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
|
|
// Seek before beginning
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(-1, SeekOrigin.Begin),
|
|
"Seek before beginning should throw.");
|
|
|
|
// Seek past end
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(blockSize + 1, SeekOrigin.Begin),
|
|
"Seek past end should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.ListFilesAsync with a non-UnknownFilesystemFile directory throws ArgumentException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_ListFilesAsync_WithNonUnknownFile_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// ListFilesAsync with a non-UnknownFilesystemFile should throw
|
|
var dirFile = new MockDirectoryFile();
|
|
Assert.ThrowsAsync<ArgumentException>(async () =>
|
|
{
|
|
await foreach (var file in fs.ListFilesAsync(dirFile, CancellationToken.None))
|
|
{ }
|
|
}, "ListFilesAsync should throw for non-UnknownFilesystemFile.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.ListFilesAsync with an UnknownFilesystemFile (non-directory) throws ArgumentException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownFilesystem_ListFilesAsync_WithNonDirectory_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// UnknownFilesystemFile.IsDirectory is always false, so this should throw "not a directory"
|
|
var nonDirFile = new UnknownFilesystemFile { Address = 0, Size = 1024 * 1024 };
|
|
Assert.ThrowsAsync<ArgumentException>(async () =>
|
|
{
|
|
await foreach (var file in fs.ListFilesAsync(nonDirFile, CancellationToken.None))
|
|
{ }
|
|
}, "ListFilesAsync should throw for non-directory UnknownFilesystemFile.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.ParsePathToAddress throws for invalid path format.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenReadStreamAsync_InvalidPath_ThrowsArgumentException()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
var partitionSize = 5 * MiB;
|
|
var partitionOffset = sectorSize * 2048;
|
|
|
|
var partition = new TestPartition
|
|
{
|
|
StartOffset = partitionOffset,
|
|
Size = partitionSize,
|
|
RawDisk = _writableRawDisk
|
|
};
|
|
|
|
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
|
|
|
|
// Path with too few segments should throw
|
|
Assert.ThrowsAsync<ArgumentException>(async () =>
|
|
{
|
|
await fs.OpenReadStreamAsync("invalid", CancellationToken.None);
|
|
}, "Invalid path should throw ArgumentException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mock directory file for testing.
|
|
/// </summary>
|
|
private class MockDirectoryFile : IFile
|
|
{
|
|
public string? Path => "/dir";
|
|
public long? Address => null;
|
|
public long Size => 0;
|
|
public bool IsDirectory => true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mock non-UnknownFilesystemFile for testing.
|
|
/// </summary>
|
|
private class MockNonUnknownFile : IFile
|
|
{
|
|
public string? Path => "/file.txt";
|
|
public long? Address => 0;
|
|
public long Size => 100;
|
|
public bool IsDirectory => false;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|