// 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.
#nullable enable
using System;
using System.Buffers;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Vanara.PInvoke;
namespace Duplicati.Library.WindowsModules;
///
/// Read-only, non-seekable stream that returns only the primary
/// data stream of by using BackupRead.
///
[SupportedOSPlatform("windows")]
public sealed class BackupDataStream : Stream
{
///
/// The file handle to the opened file with FILE_FLAG_BACKUP_SEMANTICS.
///
private readonly Kernel32.SafeHFILE _file;
///
/// The internal buffer used to read data from the file.
///
private readonly byte[] _buffer;
///
/// The size of the internal buffer in bytes.
///
private readonly uint _bufferSize;
///
/// The handle to the pinned buffer, used to pass it to BackupRead.
///
private readonly GCHandle _bufHandle;
///
/// Pointer to the pinned buffer, used to pass it to BackupRead.
///
private readonly IntPtr _bufPtr;
///
/// The current position in the internal buffer.
///
private int _bufPos, _bufLen;
///
/// The context for BackupRead, used to maintain state between calls.
///
private IntPtr _context;
///
/// Indicates whether the current stream is inside the primary data stream.
/// The first chunk of every stream starts with a WIN32_STREAM_ID header,
///
private bool _insideData;
///
/// The total number of bytes read from the file so far.
///
private long _bytesReadTotal;
///
/// The initial data length of the file
///
private readonly long _length;
///
/// The size of the data in the stream
///
private long _dataSize;
///
/// Flag indicating if we know the stream size
///
private bool _dataSizeKnown;
///
/// Creates a new for the specified file path.
/// Note that the call context must have the SeBackupPrivilege enabled.
///
/// The path to the file to read
public BackupDataStream(string path)
: this(path, 64 * 1024)
{
}
///
/// Creates a new for the specified file path.
/// Note that the call context must have the SeBackupPrivilege enabled.
///
/// The path to the file to read
/// The buffer size, if not using the default
public BackupDataStream(string path, int bufferSize)
{
if (bufferSize < 4 * 1024)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
_bufferSize = (uint)bufferSize;
_buffer = ArrayPool.Shared.Rent(bufferSize);
// Open the file with FILE_FLAG_BACKUP_SEMANTICS
_file = Kernel32.CreateFile(
lpFileName: path,
dwDesiredAccess: Kernel32.FileAccess.FILE_READ_DATA,
dwShareMode: FileShare.ReadWrite | FileShare.Delete,
lpSecurityAttributes: null,
dwCreationDisposition: FileMode.Open,
dwFlagsAndAttributes: FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS,
hTemplateFile: default);
if (_file.IsInvalid)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
if (!Kernel32.GetFileSizeEx(_file, out _length))
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
// Pin the buffer once
_bufHandle = GCHandle.Alloc(_buffer, GCHandleType.Pinned);
_bufPtr = _bufHandle.AddrOfPinnedObject();
}
///
protected override void Dispose(bool disposing)
{
try
{
if (_context != IntPtr.Zero)
Kernel32.BackupRead(_file, IntPtr.Zero, 0, out _, true, false, ref _context);
_file.Dispose();
}
finally
{
if (_bufHandle.IsAllocated) _bufHandle.Free();
ArrayPool.Shared.Return(_buffer);
base.Dispose(disposing);
}
}
///
public override bool CanRead => true;
///
public override bool CanSeek => false;
///
public override bool CanWrite => false;
///
public override long Length => _dataSizeKnown ? _dataSize : _length;
///
public override long Position
{
get => _bytesReadTotal;
set => throw new NotSupportedException();
}
///
public override int Read(byte[] dest, int offset, int count)
{
if (dest is null) throw new ArgumentNullException(nameof(dest));
if ((uint)offset > dest.Length) throw new ArgumentOutOfRangeException(nameof(offset));
if ((uint)count > dest.Length - offset)
throw new ArgumentOutOfRangeException(nameof(count));
if (count == 0) return 0;
int copied = 0;
while (count > 0)
{
// Buffer empty? – Refill from BackupRead
if (_bufPos == _bufLen && !FillBuffer())
break; // EOF
int take = Math.Min(_bufLen - _bufPos, count);
Buffer.BlockCopy(_buffer, _bufPos, dest, offset, take);
_bufPos += take;
offset += take;
count -= take;
copied += take;
_bytesReadTotal += take;
}
return copied;
}
///
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
///
public override void SetLength(long value) => throw new NotSupportedException();
///
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
///
public override void Flush() { /* no-op */ }
///
/// Fills the internal buffer with data from the file using BackupRead.
///
/// true if the buffer was filled with data, false if there are no more data to read.
private bool FillBuffer()
{
_bufPos = 0;
_bufLen = 0;
while (true)
{
if (!Kernel32.BackupRead(_file, _bufPtr, _bufferSize, out var read, false, false, ref _context))
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
if (read == 0)
return false;
// First chunk of every stream starts with a WIN32_STREAM_ID header
if (!_insideData)
{
var hdr = Marshal.PtrToStructure(_bufPtr);
// Skip streams that aren't BACKUP_DATA (primary data stream)
if (hdr.dwStreamId != Kernel32.BACKUP_STREAM_ID.BACKUP_DATA)
{
var sz = (ulong)hdr.Size;
Kernel32.BackupSeek(_file, (uint)sz, (uint)(sz >> 32), out _, out _, ref _context);
continue; // look at next stream
}
_insideData = true;
_dataSize = hdr.Size;
_dataSizeKnown = true;
// Remove the header + stream name bytes from current buffer
int headerBytes = 20 + (int)hdr.dwStreamNameSize;
_bufLen = (int)read - headerBytes;
if (_bufLen > 0)
Buffer.BlockCopy(_buffer, headerBytes, _buffer, 0, _bufLen);
}
else
{
// subsequent chunks are raw data
_bufLen = (int)read;
}
// may be zero for sparse holes
return _bufLen > 0;
}
}
}