2026-02-04 13:03:47 +01:00
using System ;
2026-02-15 14:47:07 +01:00
using System.Buffers ;
2026-02-04 13:03:47 +01:00
using System.Collections.Generic ;
using System.IO ;
using System.Runtime.CompilerServices ;
using System.Threading ;
using System.Threading.Tasks ;
2026-02-06 12:53:33 +01:00
using Duplicati.Proprietary.DiskImage.Disk ;
2026-03-05 15:30:36 +01:00
using Duplicati.Proprietary.DiskImage.General ;
2026-02-04 13:03:47 +01:00
using Duplicati.Proprietary.DiskImage.Partition ;
namespace Duplicati.Proprietary.DiskImage.Filesystem ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Metadata for unknown filesystems, storing basic block size information.
/// </summary>
2026-02-04 13:03:47 +01:00
public sealed record UnkownFilesystemMetadata
{
2026-02-15 12:45:59 +01:00
/// <summary>
/// Gets the block size used for reading/writing data.
/// </summary>
2026-02-04 13:03:47 +01:00
public int BlockSize { get ; init ; }
}
2026-02-15 12:45:59 +01:00
/// <summary>
/// Represents a file in an unknown filesystem.
/// Used for raw block-based access when filesystem type cannot be determined.
/// </summary>
2026-02-04 13:03:47 +01:00
public class UnknownFilesystemFile : IFile
{
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public string? Path { get ; init ; }
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public long? Address { get ; init ; }
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public long Size { get ; init ; }
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public bool IsDirectory => false ;
2026-03-12 15:06:05 +01:00
/// <inheritdoc />
2026-03-13 13:42:38 +00:00
public DateTime ? LastModified => DateTime . UtcNow ;
2026-02-04 13:03:47 +01:00
}
2026-02-15 12:45:59 +01:00
/// <summary>
/// Represents an unknown or unsupported filesystem.
/// Provides raw block-level access to partition data when the filesystem type cannot be determined.
/// </summary>
2026-02-19 18:16:36 +01:00
internal class UnknownFilesystem : IFilesystem
2026-02-04 13:03:47 +01:00
{
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-03-13 12:49:13 +01:00
public FileSystemType Type { get ; }
2026-02-04 13:03:47 +01:00
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public IPartition Partition { get => m_partition ; }
2026-02-15 12:45:59 +01:00
/// <summary>
/// Gets or sets the block size used for reading/writing data. Must be a multiple of the partition sector size.
/// </summary>
2026-02-04 13:03:47 +01:00
private int m_blockSize ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Flag to indicate whether the object has been disposed. Used to prevent multiple disposals and access after disposal.
/// </summary>
2026-02-04 13:03:47 +01:00
private bool m_disposed = false ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// The parent partition containing this filesystem.
/// </summary>
2026-02-04 13:03:47 +01:00
private readonly IPartition m_partition ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="UnknownFilesystem"/> class.
/// </summary>
/// <param name="partition">The parent partition.</param>
/// <param name="blockSize">The block size for reading/writing (default is 1MB).</param>
2026-03-13 12:49:13 +01:00
/// <param name="overrideType">Optional filesystem type to report (defaults to Unknown). Used when falling back from a specific filesystem type.</param>
2026-02-15 12:45:59 +01:00
/// <exception cref="ArgumentException">Thrown if block size is invalid.</exception>
2026-03-13 12:49:13 +01:00
public UnknownFilesystem ( IPartition partition , int blockSize = 1024 * 1024 , FileSystemType overrideType = FileSystemType . Unknown )
2026-02-04 13:03:47 +01:00
{
m_partition = partition ;
if ( blockSize < 0 || blockSize % partition . PartitionTable . RawDisk ?. SectorSize != 0 )
throw new ArgumentException ( "Block size must be non-negative and a multiple of the partition sector size." , nameof ( blockSize ));
m_blockSize = blockSize ;
2026-03-13 12:49:13 +01:00
Type = overrideType ;
2026-02-04 13:03:47 +01:00
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public void Dispose ()
{
if ( m_disposed )
return ;
m_disposed = true ;
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public Task < object? > GetFilesystemMetadataAsync ( CancellationToken cancellationToken )
{
var cfg = new UnkownFilesystemMetadata
{
BlockSize = m_blockSize ,
};
return Task . FromResult < object? >( cfg );
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public async IAsyncEnumerable < IFile > ListFilesAsync ([ EnumeratorCancellation ] CancellationToken cancellationToken )
{
long totalSize = m_partition . Size ;
long blockSize = m_blockSize ;
long blockCount = ( totalSize + blockSize - 1 ) / blockSize ;
for ( long i = 0 ; i < blockCount ; i ++)
{
2026-02-10 19:11:17 +01:00
long address = i * blockSize ;
2026-02-04 13:03:47 +01:00
long size = Math . Min ( blockSize , totalSize - i * blockSize );
2026-02-10 12:30:43 +01:00
2026-02-13 16:42:37 +01:00
if ( size > 0 )
yield return new UnknownFilesystemFile ()
{
Address = address ,
Size = size
};
2026-02-04 13:03:47 +01:00
}
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-04 13:03:47 +01:00
public async IAsyncEnumerable < IFile > ListFilesAsync ( IFile directory , [ EnumeratorCancellation ] CancellationToken cancellationToken )
{
2026-02-10 12:30:43 +01:00
ArgumentNullException . ThrowIfNull ( directory );
2026-02-04 13:03:47 +01:00
if ( directory is not UnknownFilesystemFile )
throw new ArgumentException ( "The specified directory does not belong to this filesystem." , nameof ( directory ));
if (! directory . IsDirectory )
throw new ArgumentException ( "The specified file is not a directory." , nameof ( directory ));
yield break ;
}
2026-02-15 12:45:59 +01:00
/// <summary>
/// Parses a file path to extract the corresponding block address for raw access.
/// </summary>
/// <param name="path">The file path to parse.</param>
/// <returns>The block address corresponding to the file path.</returns>
/// <exception cref="ArgumentException">Thrown if the path format is invalid.</exception>
2026-02-10 12:30:43 +01:00
private static long ParsePathToAddress ( string path )
{
// Path format: root/part_{PartitionTableType}_{PartitionNumber}/fs_{FileSystemType}/{Address}
// We need to extract the part after the last slash
2026-03-11 15:02:23 +01:00
var parts = path . Split ([ '/' , '\\' ], StringSplitOptions . RemoveEmptyEntries );
2026-02-12 16:54:53 +01:00
if ( parts . Length < 4 )
2026-02-10 12:30:43 +01:00
throw new ArgumentException ( "Invalid path format." , nameof ( path ));
return Convert . ToInt64 ( parts [^ 1 ], 16 );
}
2026-02-15 12:45:59 +01:00
/// <summary>
/// Checks if the specified range is valid for the partition.
/// </summary>
/// <param name="start">The starting address of the range.</param>
/// <param name="size">The size of the range.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the start or size is negative or exceeds the partition size.</exception>
/// <exception cref="ArgumentException">Thrown if the start or size is not a multiple of the sector size.</exception>
2026-02-10 12:30:43 +01:00
public void BoundsCheck ( long start , long size )
{
if ( start < 0 || size < 0 )
throw new ArgumentOutOfRangeException ( "Start and size must be non-negative." );
if ( start + size > m_partition . Size )
throw new ArgumentOutOfRangeException ( "The specified range exceeds the partition size." );
var sectorSize = m_partition . PartitionTable . RawDisk ?. SectorSize ?? 512 ;
if ( start % sectorSize != 0 || size % sectorSize != 0 )
throw new ArgumentException ( $"Start and size must be multiples of the sector size ({sectorSize} bytes)." );
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public async Task < Stream > OpenReadStreamAsync ( IFile file , CancellationToken cancellationToken )
{
if ( file is not UnknownFilesystemFile unknownFile )
throw new ArgumentException ( "The specified file does not belong to this filesystem." , nameof ( file ));
if ( file . IsDirectory )
throw new ArgumentException ( "The specified file is a directory." , nameof ( file ));
long address = unknownFile . Address ?? throw new ArgumentException ( "File address is required." , nameof ( file ));
long size = unknownFile . Size ;
2026-02-10 19:11:17 +01:00
BoundsCheck ( address , size );
2026-02-10 12:30:43 +01:00
2026-02-10 19:11:17 +01:00
return new UnknownFilesystemStream ( m_partition . PartitionTable . RawDisk !, m_partition . StartOffset + address , size , readEnabled : true , writeEnabled : false );
2026-02-10 12:30:43 +01:00
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public async Task < Stream > OpenReadStreamAsync ( string path , CancellationToken cancellationToken )
2026-02-04 13:03:47 +01:00
{
2026-02-10 12:30:43 +01:00
long address = ParsePathToAddress ( path );
long size = Math . Min (( long ) m_blockSize , m_partition . Size - address );
return await OpenReadStreamAsync ( new UnknownFilesystemFile { Address = address , Size = size }, cancellationToken );
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public async Task < Stream > OpenWriteStreamAsync ( IFile file , CancellationToken cancellationToken )
{
if ( file is not UnknownFilesystemFile unknownFile )
2026-02-04 13:03:47 +01:00
throw new ArgumentException ( "The specified file does not belong to this filesystem." , nameof ( file ));
2026-02-10 12:30:43 +01:00
if ( file . IsDirectory )
throw new ArgumentException ( "The specified file is a directory." , nameof ( file ));
long address = unknownFile . Address ?? throw new ArgumentException ( "File address is required." , nameof ( file ));
2026-03-13 14:11:17 +00:00
long requestedSize = unknownFile . Size ;
2026-02-04 13:03:47 +01:00
2026-03-13 14:11:17 +00:00
BoundsCheck ( address , requestedSize );
// Calculate actual available size based on target partition
// This ensures we fail fast if the target is too small
long actualSize = Math . Min ( requestedSize , m_partition . Size - address );
if ( actualSize < requestedSize )
throw new IOException ( $"Target partition is too small to write block at address 0x{address:X}. " +
$"Requested size: {requestedSize} bytes, Available: {actualSize} bytes. " +
$"The target disk may be smaller than the source disk." );
2026-02-04 13:03:47 +01:00
2026-03-13 14:11:17 +00:00
return new UnknownFilesystemStream ( m_partition . PartitionTable . RawDisk !, m_partition . StartOffset + address , actualSize , readEnabled : false , writeEnabled : true );
2026-02-04 13:03:47 +01:00
}
2026-02-06 12:53:33 +01:00
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public async Task < Stream > OpenWriteStreamAsync ( string path , CancellationToken cancellationToken )
2026-02-06 12:53:33 +01:00
{
2026-02-10 12:30:43 +01:00
long address = ParsePathToAddress ( path );
long size = Math . Min (( long ) m_blockSize , m_partition . Size - address );
2026-02-06 12:53:33 +01:00
2026-02-10 12:30:43 +01:00
return await OpenWriteStreamAsync ( new UnknownFilesystemFile { Address = address , Size = size }, cancellationToken );
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public async Task < Stream > OpenReadWriteStreamAsync ( IFile file , CancellationToken cancellationToken )
{
if ( file is not UnknownFilesystemFile unknownFile )
throw new ArgumentException ( "The specified file does not belong to this filesystem." , nameof ( file ));
if ( file . IsDirectory )
throw new ArgumentException ( "The specified file is a directory." , nameof ( file ));
long address = unknownFile . Address ?? throw new ArgumentException ( "File address is required." , nameof ( file ));
2026-03-13 14:11:17 +00:00
long requestedSize = unknownFile . Size ;
2026-02-10 12:30:43 +01:00
2026-03-13 14:11:17 +00:00
BoundsCheck ( address , requestedSize );
// Calculate actual available size based on target partition
// This ensures we fail fast if the target is too small
long actualSize = Math . Min ( requestedSize , m_partition . Size - address );
if ( actualSize < requestedSize )
throw new IOException ( $"Target partition is too small to write block at address 0x{address:X}. " +
$"Requested size: {requestedSize} bytes, Available: {actualSize} bytes. " +
$"The target disk may be smaller than the source disk." );
2026-02-10 12:30:43 +01:00
2026-03-13 14:11:17 +00:00
return new UnknownFilesystemStream ( m_partition . PartitionTable . RawDisk !, m_partition . StartOffset + address , actualSize , readEnabled : true , writeEnabled : true );
2026-02-10 12:30:43 +01:00
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public async Task < Stream > OpenReadWriteStreamAsync ( string path , CancellationToken cancellationToken )
{
long address = ParsePathToAddress ( path );
long size = Math . Min (( long ) m_blockSize , m_partition . Size - address );
return await OpenReadWriteStreamAsync ( new UnknownFilesystemFile { Address = address , Size = size }, cancellationToken );
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public Task < long > GetFileLengthAsync ( IFile file , CancellationToken cancellationToken )
{
if ( file is not UnknownFilesystemFile unknownFile )
throw new ArgumentException ( "The specified file does not belong to this filesystem." , nameof ( file ));
if ( file . IsDirectory )
throw new ArgumentException ( "The specified file is a directory." , nameof ( file ));
return Task . FromResult ( unknownFile . Size );
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-10 12:30:43 +01:00
public Task < long > GetFileLengthAsync ( string path , CancellationToken cancellationToken )
{
long address = ParsePathToAddress ( path );
long totalSize = m_partition . Size ;
2026-02-10 19:11:17 +01:00
long size = Math . Min (( long ) m_blockSize , totalSize - address );
2026-02-10 12:30:43 +01:00
return Task . FromResult ( size );
2026-02-06 12:53:33 +01:00
}
/// <summary>
/// A stream that writes data directly to a specific address on the raw disk.
/// </summary>
2026-02-15 13:27:37 +01:00
private class UnknownFilesystemStream : Stream
2026-02-06 12:53:33 +01:00
{
2026-02-15 12:45:59 +01:00
/// <summary>
/// The raw disk to read from.
/// </summary>
2026-02-15 13:27:37 +01:00
private readonly IRawDisk _disk ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// The starting address on the disk for this stream.
/// </summary>
2026-02-15 13:27:37 +01:00
private readonly long _address ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// The size of the stream in bytes.
/// </summary>
2026-02-15 13:27:37 +01:00
private readonly long _size ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// The buffer used to store data before writing to disk.
2026-02-15 14:47:07 +01:00
/// Rented from ArrayPool to avoid LOH allocations for large buffers.
2026-02-15 12:45:59 +01:00
/// </summary>
2026-02-15 13:27:37 +01:00
private readonly byte [] _buffer ;
2026-02-15 14:47:07 +01:00
/// <summary>
/// Indicates whether the buffer was rented from the ArrayPool and needs to be returned.
/// </summary>
private readonly bool _bufferRented ;
2026-02-15 13:27:37 +01:00
/// <summary>
/// The current position within the buffer.
/// </summary>
private long _position ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Indicates whether the data in the buffer is valid (has been read from disk).
/// </summary>
2026-02-15 13:27:37 +01:00
private bool _validData ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Indicates whether the stream has been disposed.
/// </summary>
2026-02-15 13:27:37 +01:00
private bool _disposed ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Indicates whether the stream supports reading.
/// </summary>
2026-02-15 13:27:37 +01:00
private readonly bool _readEnabled ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Indicates whether the stream supports writing.
/// </summary>
2026-02-15 13:27:37 +01:00
private readonly bool _writeEnabled ;
/// <summary>
/// Indicates whether the buffer has been modified (dirty).
/// </summary>
private bool _dirty ;
/// <summary>
/// Initializes a new instance of the <see cref="UnknownFilesystemStream"/> class.
/// </summary>
/// <param name="disk">The raw disk to read from/write to.</param>
/// <param name="address">The starting address on the disk for this stream.</param>
/// <param name="size">The size of the stream in bytes.</param>
/// <param name="readEnabled">Whether reading is enabled.</param>
/// <param name="writeEnabled">Whether writing is enabled.</param>
public UnknownFilesystemStream ( IRawDisk disk , long address , long size , bool readEnabled , bool writeEnabled )
{
_disk = disk ;
_address = address ;
_size = size ;
_readEnabled = readEnabled ;
_writeEnabled = writeEnabled ;
2026-02-15 14:47:07 +01:00
// Rent buffer from ArrayPool to avoid LOH allocations for large buffers (typically 1 MB)
_buffer = ArrayPool < byte >. Shared . Rent (( int ) size );
_bufferRented = true ;
2026-02-15 13:27:37 +01:00
_position = 0 ;
_validData = false ;
_disposed = false ;
_dirty = false ;
}
2026-02-06 12:53:33 +01:00
2026-02-10 12:30:43 +01:00
public override bool CanRead => _readEnabled ;
public override bool CanWrite => _writeEnabled ;
2026-02-06 12:53:33 +01:00
public override bool CanSeek => true ;
2026-02-10 19:11:17 +01:00
public override long Length => _size ;
2026-02-06 12:53:33 +01:00
public override long Position
{
2026-02-15 13:27:37 +01:00
get => _position ;
set
{
if ( value < 0 || value > _size )
throw new ArgumentOutOfRangeException ( nameof ( value ));
_position = value ;
}
}
public override void Flush ()
{
// No-op - data is written on dispose
2026-02-06 12:53:33 +01:00
}
2026-02-15 13:27:37 +01:00
public override long Seek ( long offset , SeekOrigin origin )
{
long newPosition = origin switch
{
SeekOrigin . Begin => offset ,
SeekOrigin . Current => _position + offset ,
SeekOrigin . End => _size + offset ,
_ => throw new ArgumentException ( "Invalid seek origin" , nameof ( origin ))
};
if ( newPosition < 0 || newPosition > _size )
throw new ArgumentOutOfRangeException ( nameof ( offset ));
_position = newPosition ;
return _position ;
}
public override void SetLength ( long value )
{
throw new NotSupportedException ( "Cannot change the length of this stream." );
}
2026-02-10 12:30:43 +01:00
public override int Read ( byte [] buffer , int offset , int count )
{
if (! _readEnabled )
throw new NotSupportedException ( "Read is not supported on this stream." );
2026-02-15 13:27:37 +01:00
if ( buffer == null )
throw new ArgumentNullException ( nameof ( buffer ));
if ( offset < 0 )
throw new ArgumentOutOfRangeException ( nameof ( offset ));
if ( count < 0 )
throw new ArgumentOutOfRangeException ( nameof ( count ));
if ( buffer . Length - offset < count )
throw new ArgumentException ( "Invalid offset and count for buffer length" );
2026-02-10 19:11:17 +01:00
if (! _validData )
{
// Load the data from disk into the buffer on the first read
2026-02-15 14:47:07 +01:00
// Use _size to limit the read to the actual data size, not the potentially larger rented buffer
_disk . ReadBytesAsync ( _address , _buffer . AsMemory ( 0 , ( int ) _size ), CancellationToken . None ). GetAwaiter (). GetResult ();
2026-02-10 19:11:17 +01:00
_validData = true ;
}
2026-02-15 13:27:37 +01:00
int bytesToRead = ( int ) Math . Min ( count , _size - _position );
if ( bytesToRead <= 0 )
return 0 ;
2026-02-10 19:11:17 +01:00
2026-02-15 13:27:37 +01:00
Buffer . BlockCopy ( _buffer , ( int ) _position , buffer , offset , bytesToRead );
_position += bytesToRead ;
return bytesToRead ;
2026-02-10 12:30:43 +01:00
}
public override void Write ( byte [] buffer , int offset , int count )
{
if (! _writeEnabled )
throw new NotSupportedException ( "Write is not supported on this stream." );
2026-02-15 13:27:37 +01:00
if ( buffer == null )
throw new ArgumentNullException ( nameof ( buffer ));
if ( offset < 0 )
throw new ArgumentOutOfRangeException ( nameof ( offset ));
if ( count < 0 )
throw new ArgumentOutOfRangeException ( nameof ( count ));
if ( buffer . Length - offset < count )
throw new ArgumentException ( "Invalid offset and count for buffer length" );
2026-03-13 14:11:17 +00:00
if ( _position >= _size )
throw new IOException ( $"Cannot write at position {_position}: stream has ended (size: {_size}). The target partition may be smaller than the source." );
2026-02-15 13:27:37 +01:00
Buffer . BlockCopy ( buffer , offset , _buffer , ( int ) _position , count );
_position += count ;
2026-02-10 19:11:17 +01:00
_validData = true ;
2026-02-15 13:27:37 +01:00
_dirty = true ;
}
public override async Task < int > ReadAsync ( byte [] buffer , int offset , int count , CancellationToken cancellationToken )
{
if (! _readEnabled )
throw new NotSupportedException ( "Read is not supported on this stream." );
if ( buffer == null )
throw new ArgumentNullException ( nameof ( buffer ));
if ( offset < 0 )
throw new ArgumentOutOfRangeException ( nameof ( offset ));
if ( count < 0 )
throw new ArgumentOutOfRangeException ( nameof ( count ));
if ( buffer . Length - offset < count )
throw new ArgumentException ( "Invalid offset and count for buffer length" );
if (! _validData )
{
// Load the data from disk into the buffer on the first read
2026-02-15 14:47:07 +01:00
// Use _size to limit the read to the actual data size, not the potentially larger rented buffer
await _disk . ReadBytesAsync ( _address , _buffer . AsMemory ( 0 , ( int ) _size ), cancellationToken );
2026-02-15 13:27:37 +01:00
_validData = true ;
}
int bytesToRead = ( int ) Math . Min ( count , _size - _position );
if ( bytesToRead <= 0 )
return 0 ;
Buffer . BlockCopy ( _buffer , ( int ) _position , buffer , offset , bytesToRead );
_position += bytesToRead ;
return bytesToRead ;
}
public override async Task WriteAsync ( byte [] buffer , int offset , int count , CancellationToken cancellationToken )
{
if (! _writeEnabled )
throw new NotSupportedException ( "Write is not supported on this stream." );
if ( buffer == null )
throw new ArgumentNullException ( nameof ( buffer ));
if ( offset < 0 )
throw new ArgumentOutOfRangeException ( nameof ( offset ));
if ( count < 0 )
throw new ArgumentOutOfRangeException ( nameof ( count ));
if ( buffer . Length - offset < count )
throw new ArgumentException ( "Invalid offset and count for buffer length" );
2026-03-13 14:11:17 +00:00
if ( _position >= _size )
throw new IOException ( $"Cannot write at position {_position}: stream has ended (size: {_size}). The target partition may be smaller than the source." );
2026-02-15 13:27:37 +01:00
Buffer . BlockCopy ( buffer , offset , _buffer , ( int ) _position , count );
_position += count ;
_validData = true ;
_dirty = true ;
await Task . CompletedTask ;
2026-02-10 12:30:43 +01:00
}
2026-02-06 12:53:33 +01:00
protected override void Dispose ( bool disposing )
{
if (! _disposed )
{
2026-02-15 14:47:07 +01:00
if ( disposing )
2026-02-06 12:53:33 +01:00
{
2026-02-15 14:47:07 +01:00
if ( _writeEnabled && _dirty )
{
// Write all buffered data to disk
_disk . WriteBytesAsync ( _address , _buffer . AsMemory ( 0 , ( int ) _size ), CancellationToken . None ). GetAwaiter (). GetResult ();
}
// Return the rented buffer to the pool
if ( _bufferRented )
{
ArrayPool < byte >. Shared . Return ( _buffer );
}
2026-02-06 12:53:33 +01:00
}
_disposed = true ;
}
base . Dispose ( disposing );
}
2026-02-15 13:27:37 +01:00
/// <summary>
/// Asynchronously releases the unmanaged resources used by the <see cref="UnknownFilesystemStream"/>.
/// </summary>
public override async ValueTask DisposeAsync ()
{
if (! _disposed )
{
if ( _writeEnabled && _dirty )
{
// Write all buffered data to disk
await _disk . WriteBytesAsync ( _address , _buffer . AsMemory ( 0 , ( int ) _size ), CancellationToken . None );
}
2026-02-15 14:47:07 +01:00
// Return the rented buffer to the pool
if ( _bufferRented )
{
ArrayPool < byte >. Shared . Return ( _buffer );
}
2026-02-15 13:27:37 +01:00
_disposed = true ;
}
}
2026-02-06 12:53:33 +01:00
}
2026-02-04 13:03:47 +01:00
}