diff --git a/InfoPrint/Features/MainFeature.cs b/InfoPrint/Features/MainFeature.cs index 43d3c8072..388f31899 100644 --- a/InfoPrint/Features/MainFeature.cs +++ b/InfoPrint/Features/MainFeature.cs @@ -38,6 +38,9 @@ internal sealed class MainFeature : Feature private const string _jsonName = "json"; internal readonly FlagInput JsonInput = new(_jsonName, ["-j", "--json"], "Print info as JSON"); + private const string _recursiveName = "recursive"; + internal readonly FlagInput RecursiveInput = new(_recursiveName, ["-r", "--recursive"], "Recursively print info from embedded files"); + #endregion /// @@ -74,6 +77,7 @@ public MainFeature() Add(HashInput); Add(FileOnlyInput); Add(JsonInput); + Add(RecursiveInput); } /// @@ -84,9 +88,7 @@ public override bool Execute() Hash = GetBoolean(_hashName); FileOnly = GetBoolean(_fileOnlyName); Json = GetBoolean(_jsonName); - - // TODO: Add flag for this once there are examples of it - Recursive = false; + Recursive = GetBoolean(_recursiveName); // Loop through the input paths for (int i = 0; i < Inputs.Count; i++) diff --git a/SabreTools.Wrappers/ISO9660.Extraction.cs b/SabreTools.Wrappers/ISO9660.Extraction.cs index 3e29afb11..da5f0d707 100644 --- a/SabreTools.Wrappers/ISO9660.Extraction.cs +++ b/SabreTools.Wrappers/ISO9660.Extraction.cs @@ -127,8 +127,9 @@ private bool ExtractExtent(int extentLocation, Encoding encoding, int blockLengt succeeded &= ExtractFile(dr, encoding, blockLength, false, outputDirectory, includeDebug); // Also extract from BigEndian values if ambiguous - if (!dr.ExtentLocation.IsValid) - succeeded &= ExtractFile(dr, encoding, blockLength, true, outputDirectory, includeDebug); + // TODO: How to treat files with same name but different location? + // if (!dr.ExtentLocation.IsValid) + // succeeded &= ExtractFile(dr, encoding, blockLength, true, outputDirectory, includeDebug); } } @@ -151,7 +152,7 @@ private bool ExtractFile(DirectoryRecord dr, Encoding encoding, int blockLength, int extentLocation = bigEndian ? dr.ExtentLocation.BigEndian : dr.ExtentLocation.LittleEndian; // Check that the file hasn't been extracted already - if (extractedFiles.ContainsKey(dr.ExtentLocation)) + if (extractedFiles.ContainsKey(extentLocation)) return true; // TODO: Decode properly (Use VD's separator characters and encoding) @@ -183,7 +184,7 @@ private bool ExtractFile(DirectoryRecord dr, Encoding encoding, int blockLength, else if (dr.FileUnitSize != 0 || dr.InterleaveGapSize != 0) { Console.WriteLine($"Extraction of interleaved files is currently not supported: {filename}"); - extractedFiles.Add(dr.ExtentLocation, dr.ExtentLength); + extractedFiles.Add(extentLocation, dr.ExtentLength); return false; } @@ -201,7 +202,7 @@ private bool ExtractFile(DirectoryRecord dr, Encoding encoding, int blockLength, const uint chunkSize = 2048 * 1024; lock (_dataSourceLock) { - long fileOffset = ((long)dr.ExtentLocation + dr.ExtendedAttributeRecordLength) * blockLength; + long fileOffset = ((long)extentLocation + dr.ExtendedAttributeRecordLength) * blockLength; _dataSource.SeekIfPossible(fileOffset, SeekOrigin.Begin); // Get the length, and make sure it won't EOF @@ -224,7 +225,7 @@ private bool ExtractFile(DirectoryRecord dr, Encoding encoding, int blockLength, } // Mark the file as extracted - extractedFiles.Add(dr.ExtentLocation, dr.ExtentLength); + extractedFiles.Add(extentLocation, dr.ExtentLength); } return true; diff --git a/SabreTools.Wrappers/ISO9660.Printing.cs b/SabreTools.Wrappers/ISO9660.Printing.cs index b7cc763f2..8eb5546b2 100644 --- a/SabreTools.Wrappers/ISO9660.Printing.cs +++ b/SabreTools.Wrappers/ISO9660.Printing.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; +using SabreTools.Data.Extensions; using SabreTools.Data.Models.ISO9660; +using SabreTools.IO.Extensions; +using SabreTools.Matching; using SabreTools.Numerics.Extensions; using SabreTools.Text.Extensions; @@ -9,6 +13,15 @@ namespace SabreTools.Wrappers { public partial class ISO9660 : IPrintable { + #region Printing State + + /// + /// List of printed embedded files by their sector offset + /// + private readonly HashSet printedFiles = []; + + #endregion + #if NETCOREAPP /// public string ExportJSON(bool recursive) => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions); @@ -20,6 +33,8 @@ public partial class ISO9660 : IPrintable /// public void PrintInformation(StringBuilder builder, bool recursive) { + printedFiles.Clear(); + builder.AppendLine("ISO 9660 Information:"); builder.AppendLine("-------------------------"); builder.AppendLine(); @@ -32,6 +47,148 @@ public void PrintInformation(StringBuilder builder, bool recursive) Encoding encoding = Encoding.UTF8; Print(builder, Model.PathTableGroups); Print(builder, Model.DirectoryDescriptors, encoding); + + if (recursive) + { + long initialOffset = _dataSource.Position; + + // Determine and validate sector length, default to 2048 + short sectorLength = (short)(Model.SystemArea.Length / 16); + if (sectorLength < 2048 || (sectorLength & (sectorLength - 1)) != 0) + sectorLength = 2048; + + // Loop through all Volume Descriptors to print files from each directory hierarchy + // Note: This will prioritize the last volume descriptor directory hierarchies first (prioritises those filenames) + // This is useful as ASCII filenames are usually in the first VD + for (int i = VolumeDescriptorSet.Length - 1; i >= 0; i--) + { + var vd = VolumeDescriptorSet[i]; + + DirectoryRecord rootDirectoryRecord; + if (vd is PrimaryVolumeDescriptor pvd) + rootDirectoryRecord = pvd.RootDirectoryRecord; + else if (vd is SupplementaryVolumeDescriptor svd) + rootDirectoryRecord = svd.RootDirectoryRecord; + else + continue; + + var blockLength = vd.GetLogicalBlockSize(sectorLength); + + // TODO: Better encoding detection (EscapeSequences) + encoding = Encoding.UTF8; + if (vd is SupplementaryVolumeDescriptor) + encoding = Encoding.BigEndianUnicode; + + RecursivePrint(builder, rootDirectoryRecord.ExtentLocation.LittleEndian, "\\", encoding, blockLength, initialOffset); + if (!rootDirectoryRecord.ExtentLocation.IsValid) + RecursivePrint(builder, rootDirectoryRecord.ExtentLocation.BigEndian, "\\", encoding, blockLength, initialOffset); + } + } + } + + private void RecursivePrint(StringBuilder builder, int sectorNumber, string filePath, Encoding encoding, short blockLength, long initialOffset) + { + // Check that directory exists in model + if (!Model.DirectoryDescriptors.TryGetValue(sectorNumber, out FileExtent? value)) + return; + + // Expect a directory + if (value is not DirectoryExtent dir) + return; + + foreach (var dr in dir.DirectoryRecords) + { + string filename = encoding.GetString(dr.FileIdentifier); + string path = Path.Combine(filePath, filename); + + // Recurse if record is directory +#if NET20 || NET35 + if ((dr.FileFlags & FileFlags.DIRECTORY) != 0) +#else + if (dr.FileFlags.HasFlag(FileFlags.DIRECTORY)) +#endif + { + // Don't recurse up or self + if (dr.FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || dr.FileIdentifier.EqualsExactly(Constants.ParentDirectory)) + continue; + + // Add extent before recursion + if (!printedFiles.Contains(dr.ExtentLocation.LittleEndian)) + { + printedFiles.Add(dr.ExtentLocation.LittleEndian); + RecursivePrint(builder, dr.ExtentLocation.LittleEndian, path, encoding, blockLength, initialOffset); + } + + if (!dr.ExtentLocation.IsValid && !printedFiles.Contains(dr.ExtentLocation.BigEndian)) + { + printedFiles.Add(dr.ExtentLocation.BigEndian); + RecursivePrint(builder, dr.ExtentLocation.BigEndian, path, encoding, blockLength, initialOffset); + } + } + else + { + // Skip multi-extent and interleaved files + if ((dr.FileFlags & FileFlags.MULTI_EXTENT) != 0 || dr.FileUnitSize != 0 || dr.InterleaveGapSize != 0) + continue; + + // Print embedded file from LittleEndian location + if (!printedFiles.Contains(dr.ExtentLocation.LittleEndian)) + { + try + { + long offset = initialOffset + ((long)dr.ExtentLocation.LittleEndian + (long)dr.ExtendedAttributeRecordLength) * (long)blockLength; + var wrapper = GetFileWrapper(offset, filename); + if (wrapper is not null && wrapper is IPrintable printable) + { + // Print info for embedded file + builder.AppendLine($"Information for {path}"); + builder.AppendLine("-------------------------"); + printable.PrintInformation(builder, true); + + printedFiles.Add(dr.ExtentLocation.LittleEndian); + } + } + catch + { + // Ignore the actual error + continue; + } + } + + // Print embedded file from BigEndian location + if (!dr.ExtentLocation.IsValid && !printedFiles.Contains(dr.ExtentLocation.BigEndian)) + { + try + { + long offset = initialOffset + (dr.ExtentLocation.BigEndian + dr.ExtendedAttributeRecordLength) * blockLength; + var wrapper = GetFileWrapper(offset, filename); + if (wrapper is not null && wrapper is IPrintable printable) + { + // Print info for embedded file + builder.AppendLine($"Information for {path}"); + builder.AppendLine("-------------------------"); + printable.PrintInformation(builder, true); + + printedFiles.Add(dr.ExtentLocation.BigEndian); + } + } + catch + { + // Ignore the actual error + continue; + } + } + } + } + } + + private IWrapper? GetFileWrapper(long offset, string filename) + { + _dataSource.Seek(offset, SeekOrigin.Begin); + byte[] magic = _dataSource.PeekBytes(16); + string extension = Path.GetExtension(filename).TrimStart('.'); + WrapperType ft = WrapperFactory.GetFileType(magic, extension); + return WrapperFactory.CreateWrapper(ft, _dataSource); } protected static void Print(StringBuilder builder, byte[] systemArea) @@ -168,7 +325,7 @@ private static void Print(StringBuilder builder, BaseVolumeDescriptor vd) builder.AppendLine(vd.OptionalPathTableLocationM, " Optional Type-M Path Table Location"); builder.AppendLine(" Root Directory Record:"); - Print(builder, vd.RootDirectoryRecord); + Print(builder, vd.RootDirectoryRecord, encoding); builder.AppendLine(encoding.GetString(vd.VolumeSetIdentifier), " Volume Set Identifier"); builder.AppendLine(encoding.GetString(vd.PublisherIdentifier), " Publisher Identifier"); @@ -385,7 +542,7 @@ private static void Print(StringBuilder builder, FileExtent extent, Encoding enc { builder.AppendLine($" Directory Record {recordNum}:"); builder.AppendLine(" -------------------------"); - Print(builder, dir.DirectoryRecords[recordNum]); + Print(builder, dir.DirectoryRecords[recordNum], encoding); builder.AppendLine(); } } @@ -398,7 +555,7 @@ private static void Print(StringBuilder builder, FileExtent extent, Encoding enc builder.AppendLine(); } - private static void Print(StringBuilder builder, DirectoryRecord dr) + private static void Print(StringBuilder builder, DirectoryRecord dr, Encoding encoding) { builder.AppendLine(dr.DirectoryRecordLength, " Directory Record Length"); builder.AppendLine(dr.ExtendedAttributeRecordLength, " Extended Attribute Record Length"); @@ -425,6 +582,9 @@ private static void Print(StringBuilder builder, DirectoryRecord dr) builder.AppendLine(dr.FileIdentifierLength, " File Identifier Length"); builder.AppendLine(dr.FileIdentifier, " File Identifier"); + if (dr.FileIdentifier is not null && dr.FileIdentifier.Length > 0) + builder.AppendLine(encoding.GetString(dr.FileIdentifier), " File Identifier (Decoded)"); + builder.AppendLine(dr.PaddingField, " Padding Field"); if (dr.SystemUse.Length == 0) diff --git a/SabreTools.Wrappers/XDVDFS.Printing.cs b/SabreTools.Wrappers/XDVDFS.Printing.cs index f6a58ef7f..70a502178 100644 --- a/SabreTools.Wrappers/XDVDFS.Printing.cs +++ b/SabreTools.Wrappers/XDVDFS.Printing.cs @@ -1,12 +1,25 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Text; using SabreTools.Data.Models.XDVDFS; +using SabreTools.IO.Extensions; +using SabreTools.Numerics.Extensions; using SabreTools.Text.Extensions; namespace SabreTools.Wrappers { public partial class XDVDFS : IPrintable { + #region Printing State + + /// + /// List of printed embedded files by their sector offset + /// + private readonly HashSet printedFiles = []; + + #endregion + #if NETCOREAPP /// public string ExportJSON(bool recursive) => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions); @@ -18,6 +31,8 @@ public partial class XDVDFS : IPrintable /// public void PrintInformation(StringBuilder builder, bool recursive) { + printedFiles.Clear(); + builder.AppendLine("Xbox DVD Filesystem Information:"); builder.AppendLine("-------------------------"); builder.AppendLine(); @@ -32,6 +47,62 @@ public void PrintInformation(StringBuilder builder, bool recursive) { Print(builder, kvp.Value, kvp.Key); } + + if (recursive) + { + long initialOffset = _dataSource.Position; + RecursivePrint(builder, Model.VolumeDescriptor.RootOffset, "\\", initialOffset); + } + } + + private void RecursivePrint(StringBuilder builder, uint sectorNumber, string filePath, long initialOffset) + { + if (!Model.DirectoryDescriptors.ContainsKey(sectorNumber)) + return; + + foreach (DirectoryRecord dr in Model.DirectoryDescriptors[sectorNumber].DirectoryRecords) + { + string filename = Encoding.UTF8.GetString(dr.Filename); + string path = Path.Combine(filePath, filename); + + // Skip already printed files + if (printedFiles.Contains(dr.ExtentOffset)) + continue; + + // Recurse into directory + if ((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY) + { + // Add directory extent before recursing + printedFiles.Add(dr.ExtentOffset); + + RecursivePrint(builder, dr.ExtentOffset, path, initialOffset); + continue; + } + + // Parse embedded file + try + { + _dataSource.Seek(initialOffset + Constants.SectorSize * dr.ExtentOffset, SeekOrigin.Begin); + byte[] magic = _dataSource.PeekBytes(16); + string extension = Path.GetExtension(filename).TrimStart('.'); + WrapperType ft = WrapperFactory.GetFileType(magic, extension); + var wrapper = WrapperFactory.CreateWrapper(ft, _dataSource); + if (wrapper is null || wrapper is not IPrintable printable) + continue; + + // Print info for embedded file + builder.AppendLine($"Information for {path}"); + builder.AppendLine("-------------------------"); + printable.PrintInformation(builder, true); + + printedFiles.Add(dr.ExtentOffset); + } + catch + { + // Ignore the actual error + continue; + } + } } private static void Print(StringBuilder builder, byte[] reservedArea)