// 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; } } }