diff --git a/XvdTool.Streaming/HttpFileStream.cs b/XvdTool.Streaming/HttpFileStream.cs index 89e5483..3957d4d 100644 --- a/XvdTool.Streaming/HttpFileStream.cs +++ b/XvdTool.Streaming/HttpFileStream.cs @@ -1,7 +1,5 @@ using System.Diagnostics; -using System.Net; using System.Net.Http.Headers; -using System; namespace XvdTool.Streaming; diff --git a/XvdTool.Streaming/Program.cs b/XvdTool.Streaming/Program.cs index 6bd8500..a178ebf 100644 --- a/XvdTool.Streaming/Program.cs +++ b/XvdTool.Streaming/Program.cs @@ -61,7 +61,7 @@ internal abstract class XvdCommand : Command where T : XvdCommandSettings { protected StreamedXvdFile XvdFile = default!; - protected void Initialize(XvdCommandSettings settings) + protected void Initialize(XvdCommandSettings settings, bool requiresWriting) { Debug.Assert(settings.XvcPath != null, "settings.XvcPath != null"); @@ -69,7 +69,7 @@ protected void Initialize(XvdCommandSettings settings) XvdFile = path.StartsWith("http") ? StreamedXvdFile.OpenFromUrl(path) - : StreamedXvdFile.OpenFromFile(path); + : StreamedXvdFile.OpenFromFile(path, requiresWriting); XvdFile.Parse(); } @@ -89,7 +89,7 @@ internal abstract class CryptoCommand : XvdCommand where T : CryptoCommand protected bool Initialize(CryptoCommandSettings settings, out KeyEntry entry) { - base.Initialize(settings); + base.Initialize(settings, requiresWriting: true); Debug.Assert(XvdFile != null, "XvdFile != null"); @@ -158,7 +158,7 @@ public sealed class Settings : XvdCommandSettings public override int Execute(CommandContext context, Settings settings) { - Initialize(settings); + Initialize(settings, requiresWriting: false); Debug.Assert(XvdFile != null, "XvdFile != null"); @@ -241,7 +241,7 @@ public sealed class Settings : XvdCommandSettings; public override int Execute(CommandContext context, Settings settings) { - Initialize(settings); + Initialize(settings, requiresWriting: false); Debug.Assert(XvdFile != null, "XvdFile != null"); diff --git a/XvdTool.Streaming/StreamedXvdFile.LocalImpl.cs b/XvdTool.Streaming/StreamedXvdFile.LocalImpl.cs index d488729..4161d1a 100644 --- a/XvdTool.Streaming/StreamedXvdFile.LocalImpl.cs +++ b/XvdTool.Streaming/StreamedXvdFile.LocalImpl.cs @@ -118,8 +118,6 @@ private void LocalDecryptData(in KeyEntry key, bool recalculateHashes) { // TODO } - - memoryFile.Dispose(); } // ReSharper restore AccessToDisposedClosure @@ -155,8 +153,7 @@ private void LocalDecryptSection(ProgressTask progressTask, MemoryMappedFile mem using var directAccessor = memoryFile.CreateDirectAccessor( (long)(offset + mappedPageOffset * XvdFile.PAGE_SIZE), - (long)pageCountThisOffset * XvdFile.PAGE_SIZE, - MemoryMappedFileAccess.ReadWrite); + (long)pageCountThisOffset * XvdFile.PAGE_SIZE); for (uint i = 0; i < pageCountThisOffset; i++) { @@ -195,14 +192,13 @@ private Span CacheDataUnits(ulong startPage, ulong count) _stream.Position = hashPageOffset; - int read; for (ulong i = 0; i < count; i++) { if (refreshCache) { refreshCache = false; - read = _stream.Read(pageCache); + var read = _stream.Read(pageCache); Debug.Assert(read == pageCache.Length, "read == pageCache.Length"); } @@ -269,11 +265,11 @@ private bool LocalVerifyDataHashesTask(ProgressContext ctx) var task = ctx.AddTask("Verifying hashes", maxValue: (long) dataBlockCount * (int) XvdFile.PAGE_SIZE); - int read; for (ulong i = 0; i < dataBlockCount; i++) { task.Increment(XvdFile.PAGE_SIZE); + int read; if (refreshCache) { refreshCache = false; diff --git a/XvdTool.Streaming/StreamedXvdFile.cs b/XvdTool.Streaming/StreamedXvdFile.cs index c8535be..0f00a00 100644 --- a/XvdTool.Streaming/StreamedXvdFile.cs +++ b/XvdTool.Streaming/StreamedXvdFile.cs @@ -1,7 +1,9 @@ using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; +using DiscUtils; +using DiscUtils.Ntfs; +using DiscUtils.Partitions; using LibXboxOne; using Spectre.Console; using Spectre.Console.Rendering; @@ -35,6 +37,9 @@ public partial class StreamedXvdFile : IDisposable private XvdSegmentMetadataSegment[] _segments; private string[] _segmentPaths; + private bool _hasPartitionFiles; + private (string Path, ulong Size)[] _partitionFileEntries; + // XVD header extracted infos private bool _isXvc; private bool _dataIntegrity; @@ -50,6 +55,8 @@ public partial class StreamedXvdFile : IDisposable private ulong _hashTreeOffset; private ulong _userDataOffset; private ulong _xvcInfoOffset; + private ulong _dynamicHeaderOffset; + private ulong _driveDataOffset; private const string SegmentMetadataFilename = "SegmentMetadata.bin"; @@ -58,16 +65,16 @@ private StreamedXvdFile(Stream stream) _stream = stream; _reader = new BinaryReader(stream); - _xvcRegions = Array.Empty(); - _xvcUpdateSegments = Array.Empty(); - _xvcRegionSpecifiers = Array.Empty(); - _xvcRegionPresenceInfo = Array.Empty(); + _xvcRegions = []; + _xvcUpdateSegments = []; + _xvcRegionSpecifiers = []; + _xvcRegionPresenceInfo = []; - _userDataPackages = new Dictionary(); - _userDataPackageContents = new Dictionary(); + _userDataPackages = []; + _userDataPackageContents = []; - _segments = Array.Empty(); - _segmentPaths = Array.Empty(); + _segments = []; + _segmentPaths = []; } public static StreamedXvdFile OpenFromUrl(string url) @@ -75,12 +82,12 @@ public static StreamedXvdFile OpenFromUrl(string url) return new StreamedXvdFile(HttpFileStream.Open(url)); } - public static StreamedXvdFile OpenFromFile(string filePath) + public static StreamedXvdFile OpenFromFile(string filePath, bool writing = true) { if (!File.Exists(filePath)) throw new InvalidOperationException("File does not exist"); - return new StreamedXvdFile(File.Open(filePath, FileMode.Open, FileAccess.ReadWrite)); + return new StreamedXvdFile(File.Open(filePath, FileMode.Open, writing ? FileAccess.ReadWrite : FileAccess.Read, FileShare.Read)); } public void Parse() @@ -99,6 +106,11 @@ public void Parse() { ParseXvcInfo(); } + + if (!_hasSegmentMetadata && _header.Type == XvdType.Fixed && OperatingSystem.IsWindows()) + { + ParseNtfsPartition(); + } } private void ParseHeader() @@ -112,14 +124,15 @@ private void ParseHeader() _resiliency = _header.VolumeFlags.HasFlag(XvdVolumeFlags.ResiliencyEnabled); _encrypted = !_header.VolumeFlags.HasFlag(XvdVolumeFlags.EncryptionDisabled); - _hashTreePageCount = - XvdMath.CalculateNumberHashPages(out _hashTreeLevels, _header.NumberOfHashedPages, _resiliency); + _hashTreePageCount = XvdMath.CalculateNumberHashPages(out _hashTreeLevels, _header.NumberOfHashedPages, _resiliency); _hashEntryLength = _encrypted ? (int)XvdFile.HASH_ENTRY_LENGTH_ENCRYPTED : (int)XvdFile.HASH_ENTRY_LENGTH; _mutableDataOffset = XvdMath.PageNumberToOffset(_header.EmbeddedXvdPageCount) + _embeddedXvdOffset; _hashTreeOffset = _header.MutableDataLength + _mutableDataOffset; _userDataOffset = (_dataIntegrity ? XvdMath.PageNumberToOffset(_hashTreePageCount) : 0) + _hashTreeOffset; _xvcInfoOffset = XvdMath.PageNumberToOffset(_header.UserDataPageCount) + _userDataOffset; + _dynamicHeaderOffset = XvdMath.PageNumberToOffset(_header.XvcInfoPageCount) + _xvcInfoOffset; + _driveDataOffset = XvdMath.PageNumberToOffset(_header.DynamicHeaderPageCount) + _dynamicHeaderOffset; } private void ParseUserData() @@ -208,16 +221,12 @@ private void ParseXvcInfo() if (_xvcInfo.Version >= 1) { - Debug.Assert(int.MaxValue > _xvcInfo.RegionCount, "int.MaxValue > _xvcInfo.RegionCount"); - _xvcRegions = xvcInfoReader.ReadStructArray((int)_xvcInfo.RegionCount); - - Debug.Assert(int.MaxValue > _xvcInfo.UpdateSegmentCount, "int.MaxValue > _xvcInfo.UpdateSegmentCount"); - _xvcUpdateSegments = xvcInfoReader.ReadStructArray((int) _xvcInfo.UpdateSegmentCount); + _xvcRegions = xvcInfoReader.ReadStructArray(checked((int)_xvcInfo.RegionCount)); + _xvcUpdateSegments = xvcInfoReader.ReadStructArray(checked((int)_xvcInfo.UpdateSegmentCount)); if (_xvcInfo.Version >= 2) { - Debug.Assert(int.MaxValue > _xvcInfo.RegionSpecifierCount, "int.MaxValue > _xvcInfo.RegionSpecifierCount"); - _xvcRegionSpecifiers = xvcInfoReader.ReadStructArray((int) _xvcInfo.RegionSpecifierCount); + _xvcRegionSpecifiers = xvcInfoReader.ReadStructArray(checked((int)_xvcInfo.RegionSpecifierCount)); if (_header.MutableDataPageCount > 0) { @@ -229,6 +238,76 @@ private void ParseXvcInfo() } } + private void ParseNtfsPartition() + { + if (!OperatingSystem.IsWindows()) + { + ConsoleLogger.WriteInfoLine("Skipping GPT/MBR partition parsing due to not running on Windows."); + return; + } + + var driveSize = checked((long)_header.DriveSize); + + using var fsStream = + new StreamedXvdFileSystemStream( + driveSize, + checked((long)_driveDataOffset), + 0, + checked((long)_xvcInfoOffset), + _stream); + + PartitionTable? partitionTable; + + try + { + partitionTable = + new GuidPartitionTable(fsStream, Geometry.FromCapacity(driveSize, (int)XvdFile.SECTOR_SIZE)); + } + catch (Exception) + { + partitionTable = null; + } + + if (partitionTable == null) + { + try + { + partitionTable = + new BiosPartitionTable(fsStream, Geometry.FromCapacity(driveSize, (int)XvdFile.SECTOR_SIZE)); + } + catch (Exception) + { + partitionTable = null; + } + } + + if (partitionTable == null) + { + ConsoleLogger.WriteErrLine("Failed to drive contents as either GPT or MBR."); + return; + } + + var partionTable = new BiosPartitionTable(fsStream, Geometry.FromCapacity(driveSize, (int)XvdFile.SECTOR_SIZE)); + if (partionTable.Partitions.Count == 0) + { + ConsoleLogger.WriteInfoLine("File does not contain a partition."); + return; + } + + if (partionTable.Partitions.Count > 1) + { + ConsoleLogger.WriteInfoLine($"File contains [white bold]{partionTable.Partitions.Count}[/] partitions."); + } + + using var partiton = new NtfsFileSystem(partionTable.Partitions[0].Open()); + + _hasPartitionFiles = true; + _partitionFileEntries = partiton.Root + .GetFiles("*.*", SearchOption.AllDirectories) + .Select(x => (x.FullName, (ulong)x.Length)) + .ToArray(); + } + public Guid GetKeyId() { if (!_encrypted || !_hasXvcInfo || !_xvcInfo.IsAnyKeySet) @@ -278,7 +357,7 @@ public void DecryptData(in KeyEntry key, bool recalculateHashes) if (!_encrypted) { ConsoleLogger.WriteInfoLine("Skipping decryption as the file is [green bold]not encrypted[/]."); - //return; + return; } LocalDecryptData(key, recalculateHashes); @@ -457,7 +536,6 @@ private void ExtractRegion( var currentPageNumber = 0; var totalPageNumber = (long) XvdMath.OffsetToPageNumber(regionLength); - int read; while (_segments.Length > currentSegment && totalPageNumber > currentPageNumber) { var fileSize = _segments[currentSegment].FileSize; @@ -476,6 +554,7 @@ private void ExtractRegion( { var currentFileSectionLength = (int)Math.Min(remainingFileSize, XvdFile.PAGE_SIZE); + int read; if (refreshHashCache) { _stream.Position = totalHashCacheOffset; @@ -569,11 +648,6 @@ public string PrintInfo(bool showAllFiles = false) { AnsiConsole.Record(); - static Rows StringToRows(string text) - { - return new Rows(text.Split(Environment.NewLine).Select(x => new Text(x.Trim()))); - } - var xvdHeaderPanel = new Panel(StringToRows(_header.ToString(false))) .Header("XVD Header") .RoundedBorder() @@ -723,7 +797,9 @@ static Rows StringToRows(string text) .RoundedBorder() .Expand(); - for (int i = 0; i < (showAllFiles ? _segments.Length : Math.Min(_segments.Length, 0x1000)); i++) + for (int i = 0; + i < (showAllFiles ? _segments.Length : Math.Min(_segments.Length, 0x1000)); + i++) { var segment = _segments[i]; var updateSegment = _xvcUpdateSegments[i]; @@ -741,16 +817,55 @@ static Rows StringToRows(string text) if (!showAllFiles && _segments.Length > 0x1000) { segmentTable.AddEmptyRow(); - segmentTable.AddRow(new Markup("[red bold][/]")); + segmentTable.AddRow(new Markup("[red bold][/]")); } AnsiConsole.Write(segmentTable); } } + if (_hasPartitionFiles && !_hasSegmentMetadata) + { + var fileSystemFilesTable = new Table() + .Title("Partition Files") + .AddColumns( + new TableColumn("File Size"), + new TableColumn("Size in Bytes"), + new TableColumn("File Path") + ) + .RoundedBorder() + .Expand(); + + for (int i = 0; + i < (showAllFiles ? _partitionFileEntries.Length : Math.Min(_partitionFileEntries.Length, 0x1000)); + i++) + { + var file = _partitionFileEntries[i]; + + fileSystemFilesTable.AddRow( + new Markup($"[green]0x{file.Size:x16}[/]"), + new Markup($"[green]{ToFileSize(file.Size)}[/]"), + new Markup($"[aqua underline]{file.Path}[/]") + ); + } + + if (!showAllFiles && _segments.Length > 0x1000) + { + fileSystemFilesTable.AddEmptyRow(); + fileSystemFilesTable.AddRow(new Markup("[red bold][/]")); + } + + AnsiConsole.Write(fileSystemFilesTable); + } + return AnsiConsole.ExportText(); - string ToFileSize(ulong size) + static Rows StringToRows(string text) + { + return new Rows(text.Split(Environment.NewLine).Select(x => new Text(x.Trim()))); + } + + static string ToFileSize(ulong size) { if (size < 1024) return $"{size} B"; @@ -769,5 +884,6 @@ public void Dispose() { _reader.Dispose(); _stream.Dispose(); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/XvdTool.Streaming/StreamedXvdFileSystemStream.cs b/XvdTool.Streaming/StreamedXvdFileSystemStream.cs new file mode 100644 index 0000000..32e8a93 --- /dev/null +++ b/XvdTool.Streaming/StreamedXvdFileSystemStream.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; + +namespace XvdTool.Streaming; + +public class StreamedXvdFileSystemStream(long length, long driveOffset, long staticDataLength, long dynamicOffset, Stream baseStream) : Stream +{ + public override long Length { get; } = length; + public override long Position { get; set; } + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + + private readonly long _driveOffset = driveOffset; + private readonly long _staticDataLength = staticDataLength; + private readonly long _dynamicOffset = dynamicOffset; + private readonly Stream _fileStream = baseStream; + + public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + public override int Read(Span buffer) + { + var readPosition = _driveOffset + Position; + + _fileStream.Position = readPosition; + var count = _fileStream.Read(buffer); + Position += count; + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + { + Position = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => offset + Position, + SeekOrigin.End => Length + offset, + _ => throw new UnreachableException() + }; + + return Position; + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override void Flush() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/XvdTool.Streaming/XvdTool.Streaming.csproj b/XvdTool.Streaming/XvdTool.Streaming.csproj index ca4d336..b69c1b1 100644 --- a/XvdTool.Streaming/XvdTool.Streaming.csproj +++ b/XvdTool.Streaming/XvdTool.Streaming.csproj @@ -9,6 +9,7 @@ +