diff --git a/Documentation/command-dump.md b/Documentation/command-dump.md index 6f1d0d3..7765ae0 100644 --- a/Documentation/command-dump.md +++ b/Documentation/command-dump.md @@ -12,6 +12,7 @@ UnityDataTool dump [options] |--------|-------------|---------| | `` | Path to file to dump | *(required)* | | `-o, --output-path ` | Output folder | Current folder | +| `--stdout` | Write the dump to stdout (status and errors go to stderr). Mutually exclusive with `-o`. | `false` | | `-f, --output-format ` | Output format | `text` | | `-s, --skip-large-arrays` | Skip dumping large arrays | `false` | | `-i, --objectid ` | Only dump object with this ID | All objects | @@ -55,6 +56,26 @@ Dump the AssetBundle manifest object: UnityDataTool dump mybundle -t AssetBundle ``` +Write the dump to stdout and pipe it through another tool: +```bash +UnityDataTool dump /path/to/file --stdout | grep "ClassID: 114" +``` + +--- + +## Writing to stdout + +Use `--stdout` to send the dump to standard output instead of writing a `.txt` file. Status messages (the `Processing ...` line, errors, and stack traces) are routed to stderr so the dump on stdout is clean for piping or redirecting. + +```bash +UnityDataTool dump /path/to/file --stdout > my-dump.txt +``` + +Restrictions: + +- `--stdout` and `-o` are mutually exclusive. +- For Unity archives that contain more than one SerializedFile, `--stdout` is refused — there is no unambiguous way to deliver multiple files on a single stream. Pass an individual SerializedFile, or omit `--stdout` to get one `.txt` per SerializedFile in the output folder. + --- ## Filtering by Type @@ -124,7 +145,7 @@ UnityDataTool dump /path/to/file.bundle --typetree-data /path/to/typetree.bin ## Output Format -The output is similar to Unity's `binary2text` tool. Each file begins with external references: +The output is similar to Unity's `binary2text` tool. Unfiltered dumps begin with external references (when filtering with `-i` or `-t` this section is omitted — use the [`serialized-file externalrefs`](#) command if you want them separately): ``` External References diff --git a/Documentation/textdumper.md b/Documentation/textdumper.md index 3ef490c..e6488b6 100644 --- a/Documentation/textdumper.md +++ b/Documentation/textdumper.md @@ -5,12 +5,9 @@ file (AssetBundle or SerializedFile) into human-readable yaml-style text file. ## How to use -The library consists of a single class called [TextDumperTool](../TextDumper/TextDumperTool.cs). It has a method named Dump and takes five parameters: -* path (string): path of the data file. -* outputPath (string): path where the output files will be created. -* skipLargeArrays (bool): if true, the content of arrays larger than 1KB won't be dumped. -* objectId (long, optional): if specified and not 0, only the object with this signed 64-bit id will be dumped. If 0 (default), all objects are dumped. -* typeFilter (string, optional): if specified, only objects matching this type are dumped. Accepts a numeric ClassID (e.g. 114) or a type name (e.g. MonoBehaviour, case-insensitive). +The library consists of a single class called [TextDumperTool](../TextDumper/TextDumperTool.cs). Call its `Dump` method, passing a `TextDumperTool.DumpOptions` object that specify the path of the file to dump and various flags and options. + +The library is used to implement the [`UnityDataTool dump` command](command-dump.md). ## How to interpret the output files @@ -18,7 +15,7 @@ There will be one output file per SerializedFile. Depending on the type of the i be more than one output file (e.g. AssetBundles are archives that can contain several SerializedFiles). -The first lines of the output file looks like this: +For an unfiltered dump, the first lines of the output file look like this (when `ObjectId` or `TypeFilter` are set the External References section is omitted, and the output starts directly with the matching object entries): External References path(1): "Library/unity default resources" GUID: 0000000000000000e000000000000000 Type: 0 diff --git a/TextDumper/TextDumperTool.cs b/TextDumper/TextDumperTool.cs index 4a8f7a2..e5e2dd1 100644 --- a/TextDumper/TextDumperTool.cs +++ b/TextDumper/TextDumperTool.cs @@ -9,93 +9,213 @@ namespace UnityDataTools.TextDumper; public class TextDumperTool { StringBuilder m_StringBuilder = new StringBuilder(1024); - bool m_SkipLargeArrays; + DumpOptions m_Options; + string m_TypeFilter; // m_Options.TypeFilter normalized: null when blank/unset, otherwise the user-provided string + int m_FilterTypeId; // > 0 when filtering by Unity ClassID (numeric form of m_TypeFilter); 0 means no ID filter + + TextWriter m_Writer; // Output, either to a file or Console.Out + + // Set during the processed of each Serialized File UnityFileReader m_Reader; SerializedFile m_SerializedFile; - StreamWriter m_Writer; - public int Dump(string path, string outputPath, bool skipLargeArrays, long objectId = 0, string typeFilter = null) + public enum DumpFormat + { + Text, + } + + public class DumpOptions { - if (string.IsNullOrWhiteSpace(typeFilter)) - typeFilter = null; + public DumpFormat Format { get; init; } = DumpFormat.Text; + public string Path { get; init; } + public string OutputPath { get; init; } + public bool SkipLargeArrays { get; init; } + public long ObjectId { get; init; } + public string TypeFilter { get; init; } + public bool ToStdout { get; init; } + } - m_SkipLargeArrays = skipLargeArrays; + public int Dump(DumpOptions options) + { + m_Options = options; + m_TypeFilter = string.IsNullOrWhiteSpace(m_Options.TypeFilter) ? null : m_Options.TypeFilter; + m_FilterTypeId = (m_TypeFilter != null && int.TryParse(m_TypeFilter, out var parsed) && parsed > 0) ? parsed : 0; try { - if (!File.Exists(path)) + if (!File.Exists(m_Options.Path)) { - Console.WriteLine($"Error: File not found: {path}"); + Console.Error.WriteLine($"Error: File not found: {m_Options.Path}"); return 1; } - if (ArchiveDetector.IsUnityArchive(path)) + if (ArchiveDetector.IsUnityArchive(m_Options.Path)) + return DumpArchive(); + + if (YamlSerializedFileDetector.IsYamlSerializedFile(m_Options.Path)) { - // The input is a Unity archive (e.g. AssetBundle); dump each serialized file inside it. - using var archive = UnityFileSystem.MountArchive(path, "/"); - foreach (var node in archive.Nodes) - { - Console.WriteLine($"Processing {node.Path} {node.Size} {node.Flags}"); + Console.Error.WriteLine("Error: The file is a YAML-format SerializedFile, which is not supported."); + Console.Error.WriteLine("UnityDataTool only supports binary-format SerializedFiles."); + return 1; + } - if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) - { - using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(node.Path) + ".txt"), false)) - { - OutputSerializedFile("/" + node.Path, objectId, typeFilter); - } - } - } + if (SerializedFileDetector.TryDetectSerializedFile(m_Options.Path, out _)) + return DumpSerializedFile(); + + Console.Error.WriteLine("Error: The file does not appear to be a valid Unity SerializedFile or Unity Archive."); + Console.Error.WriteLine($"File: {m_Options.Path}"); + return 1; + } + catch (Exception e) + { + Console.Error.WriteLine($"Error: {e.GetType()}: {e.Message}"); + Console.Error.WriteLine(e.StackTrace); + return 1; + } + } + + int DumpSerializedFile() + { + try + { + if (m_Options.ToStdout) + { + m_Writer = Console.Out; + OutputSerializedFile(m_Options.Path); + m_Writer.Flush(); } - else if (YamlSerializedFileDetector.IsYamlSerializedFile(path)) + else { - Console.WriteLine("Error: The file is a YAML-format SerializedFile, which is not supported."); - Console.WriteLine("UnityDataTool only supports binary-format SerializedFiles."); - return 1; + using var writer = new StreamWriter(Path.Combine(m_Options.OutputPath, Path.GetFileName(m_Options.Path) + ".txt"), false); + m_Writer = writer; + OutputSerializedFile(m_Options.Path); } - else if (SerializedFileDetector.TryDetectSerializedFile(path, out _)) + } + catch (SerializedFileOpenException) + { + var hint = SerializedFileDetector.GetOpenFailureHint(m_Options.Path); + if (hint != null) { - // The input is a binary SerializedFile; dump it directly. - try - { - using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(path) + ".txt"), false)) - { - OutputSerializedFile(path, objectId, typeFilter); - } - } - catch (SerializedFileOpenException) - { - var hint = SerializedFileDetector.GetOpenFailureHint(path); - if (hint != null) - { - Console.WriteLine(); - Console.WriteLine(hint); - } - return 1; - } - catch (Exception e) + Console.Error.WriteLine(); + Console.Error.WriteLine(hint); + } + return 1; + } + + return 0; + } + + // For convenience we also support directly dumping serialized files that are inside an archive, + // so that it's not necessary to use `archive extract` if you only want to see values from the object serialization. + int DumpArchive() + { + using var archive = UnityFileSystem.MountArchive(m_Options.Path, "/"); + + if (m_Options.ToStdout) + { + ArchiveNode? singleSerializedFile = null; + int serializedFileCount = 0; + foreach (var node in archive.Nodes) + { + if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) { - Console.WriteLine($"Error: {e.GetType()}: {e.Message}"); - Console.WriteLine(e.StackTrace); - return 1; + ++serializedFileCount; + singleSerializedFile ??= node; } } - else + + if (serializedFileCount == 0) { - Console.WriteLine("Error: The file does not appear to be a valid Unity SerializedFile or Unity Archive."); - Console.WriteLine($"File: {path}"); + Console.Error.WriteLine("Error: Archive contains no SerializedFiles."); return 1; } + + if (serializedFileCount > 1) + { + Console.Error.WriteLine($"Error: --stdout cannot be used with an archive containing multiple SerializedFiles ({serializedFileCount} found)."); + Console.Error.WriteLine("Extract the archive first, or pass an individual SerializedFile as input."); + return 1; + } + + var node2 = singleSerializedFile.Value; + Console.Error.WriteLine($"Processing {node2.Path} {node2.Size} {node2.Flags}"); + m_Writer = Console.Out; + OutputSerializedFile("/" + node2.Path); + m_Writer.Flush(); } - catch (Exception e) + else { - Console.WriteLine($"Error: {e.GetType()}: {e.Message}"); - Console.WriteLine(e.StackTrace); - return 1; + foreach (var node in archive.Nodes) + { + Console.WriteLine($"Processing {node.Path} {node.Size} {node.Flags}"); + + if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) + { + using var writer = new StreamWriter(Path.Combine(m_Options.OutputPath, Path.GetFileName(node.Path) + ".txt"), false); + m_Writer = writer; + OutputSerializedFile("/" + node.Path); + } + } } return 0; } + void OutputSerializedFile(string path) + { + var objectId = m_Options.ObjectId; + bool filtered = objectId != 0 || m_TypeFilter != null; + + using (m_Reader = new UnityFileReader(path, 64 * 1024 * 1024)) + using (m_SerializedFile = UnityFileSystem.OpenSerializedFile(path)) + { + // External references provide context for PPtrs across the whole file. Skip them when a + // filter is in use - the output is about a specific object, and `sf externalrefs` is the + // dedicated command for listing external refs. + if (!filtered) + { + var i = 1; + + m_Writer.WriteLine("External References"); + foreach (var extRef in m_SerializedFile.ExternalReferences) + { + m_Writer.WriteLine($"path({i}): \"{extRef.Path}\" GUID: {extRef.Guid} Type: {(int)extRef.Type}"); + ++i; + } + m_Writer.WriteLine(); + } + + bool dumpedObject = false; + foreach (var obj in m_SerializedFile.Objects) + { + if (objectId != 0 && obj.Id != objectId) + continue; + + if (m_FilterTypeId > 0 && obj.TypeId != m_FilterTypeId) + continue; + + var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); + + if (m_TypeFilter != null && m_FilterTypeId == 0 && !MatchesTypeNameFilter(obj, root)) + continue; + + var offset = obj.Offset; + + m_Writer.Write($"ID: {obj.Id} (ClassID: {obj.TypeId}) "); + RecursiveDump(root, ref offset, 0); + m_Writer.WriteLine(); + dumpedObject = true; + } + + if (filtered && !dumpedObject) + { + if (objectId != 0) + m_Writer.WriteLine($"Object with ID {objectId} not found."); + else + m_Writer.WriteLine($"No objects found matching type \"{m_TypeFilter}\"."); + } + } + } void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex = -1) { @@ -116,7 +236,7 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex } else { - m_StringBuilder.Append(' ', level * 2); + AppendIndent(level); if (level != 0) { @@ -137,7 +257,6 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex m_StringBuilder.Append(node.Type); } - // Basic data type. if (node.IsBasicType) { m_StringBuilder.Append(' '); @@ -183,7 +302,7 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex ((int)node.MetaFlags & (int)TypeTreeMetaFlags.AnyChildUsesAlignBytes) != 0 ) { - offset = (offset + 3) & ~(3); + offset = AlignTo4(offset); } } @@ -200,7 +319,7 @@ void DumpArray(TypeTreeNode node, ref long offset, int level) var arraySize = m_Reader.ReadInt32(offset); offset += 4; - m_StringBuilder.Append(' ', level * 2); + AppendIndent(level); m_StringBuilder.Append("Array"); m_StringBuilder.Append('<'); m_StringBuilder.Append(dataNode.Type); @@ -215,9 +334,9 @@ void DumpArray(TypeTreeNode node, ref long offset, int level) { if (dataNode.IsBasicType) { - m_StringBuilder.Append(' ', (level + 1) * 2); + AppendIndent(level + 1); - if (arraySize > 256 && m_SkipLargeArrays) + if (arraySize > 256 && m_Options.SkipLargeArrays) { m_StringBuilder.Append(""); offset += dataNode.Size * arraySize; @@ -318,8 +437,8 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT if (refTypeNode.Children.Count < 3) throw new Exception("Invalid ReferencedManagedType"); - m_StringBuilder.Append(' ', level * 2); - m_StringBuilder.Append($"rid("); + AppendIndent(level); + m_StringBuilder.Append("rid("); m_StringBuilder.Append(id); m_StringBuilder.Append(") ReferencedObject"); @@ -329,28 +448,17 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT ++level; var refTypeOffset = offset; - var stringSize = m_Reader.ReadInt32(offset); - var className = m_Reader.ReadString(offset + 4, stringSize); - offset += stringSize + 4; - offset = (offset + 3) & ~(3); - - stringSize = m_Reader.ReadInt32(offset); - var namespaceName = m_Reader.ReadString(offset + 4, stringSize); - offset += stringSize + 4; - offset = (offset + 3) & ~(3); - - stringSize = m_Reader.ReadInt32(offset); - var assemblyName = m_Reader.ReadString(offset + 4, stringSize); - offset += stringSize + 4; - offset = (offset + 3) & ~(3); - - if (className == "Terminus" && namespaceName == "UnityEngine.DMAT" && assemblyName == "FAKE_ASM") + var className = ReadPascalStringAndAlign(ref offset); + var namespaceName = ReadPascalStringAndAlign(ref offset); + var assemblyName = ReadPascalStringAndAlign(ref offset); + + if (IsTerminusSentinel(className, namespaceName, assemblyName)) return false; // Not the most efficient way, but it simplifies the code. RecursiveDump(refTypeNode, ref refTypeOffset, level); - m_StringBuilder.Append(' ', level * 2); + AppendIndent(level); m_StringBuilder.Append(referencedTypeDataNode.Name); m_StringBuilder.Append(' '); m_StringBuilder.Append(referencedTypeDataNode.Type); @@ -361,7 +469,7 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT if (id == -1 || id == -2) { - m_StringBuilder.Append(' ', level * 2); + AppendIndent(level); m_StringBuilder.Append(id == -1 ? " unknown" : " null"); m_Writer.WriteLine(m_StringBuilder); @@ -381,67 +489,30 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT return true; } - void OutputSerializedFile(string path, long objectId, string typeFilter) - { - int filterTypeId = 0; - bool filterByTypeId = typeFilter != null && int.TryParse(typeFilter, out filterTypeId); - - using (m_Reader = new UnityFileReader(path, 64 * 1024 * 1024)) - using (m_SerializedFile = UnityFileSystem.OpenSerializedFile(path)) - { - var i = 1; - - m_Writer.WriteLine("External References"); - foreach (var extRef in m_SerializedFile.ExternalReferences) - { - m_Writer.WriteLine($"path({i}): \"{extRef.Path}\" GUID: {extRef.Guid} Type: {(int)extRef.Type}"); - ++i; - } - m_Writer.WriteLine(); - - bool dumpedObject = false; - foreach (var obj in m_SerializedFile.Objects) - { - if (objectId != 0 && obj.Id != objectId) - continue; - - var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); + static long AlignTo4(long offset) => (offset + 3) & ~3L; - if (typeFilter != null) - { - if (filterByTypeId) - { - if (obj.TypeId != filterTypeId) - continue; - } - else - { - var typeName = TypeIdRegistry.GetTypeName(obj.TypeId); - // GetTypeName returns the id as a string when the type is unknown; - // fall back to the TypeTree root node for script types. - if (typeName == obj.TypeId.ToString()) - typeName = root.Type; - if (!string.Equals(typeName, typeFilter, StringComparison.OrdinalIgnoreCase)) - continue; - } - } + void AppendIndent(int level) => m_StringBuilder.Append(' ', level * 2); - var offset = obj.Offset; + string ReadPascalStringAndAlign(ref long offset) + { + var size = m_Reader.ReadInt32(offset); + var value = m_Reader.ReadString(offset + 4, size); + offset = AlignTo4(offset + 4 + size); + return value; + } - m_Writer.Write($"ID: {obj.Id} (ClassID: {obj.TypeId}) "); - RecursiveDump(root, ref offset, 0); - m_Writer.WriteLine(); - dumpedObject = true; - } + // Sentinel record that marks the end of the v1 ReferencedObject sequence. + static bool IsTerminusSentinel(string className, string namespaceName, string assemblyName) => + className == "Terminus" && namespaceName == "UnityEngine.DMAT" && assemblyName == "FAKE_ASM"; - if ((objectId != 0 || typeFilter != null) && !dumpedObject) - { - if (objectId != 0) - m_Writer.WriteLine($"Object with ID {objectId} not found."); - else - m_Writer.WriteLine($"No objects found matching type \"{typeFilter}\"."); - } - } + bool MatchesTypeNameFilter(ObjectInfo obj, TypeTreeNode root) + { + var typeName = TypeIdRegistry.GetTypeName(obj.TypeId); + // GetTypeName returns the id as a string when the type is unknown; + // fall back to the TypeTree root node for script types. + if (typeName == obj.TypeId.ToString()) + typeName = root.Type; + return string.Equals(typeName, m_TypeFilter, StringComparison.OrdinalIgnoreCase); } string ReadValue(TypeTreeNode node, long offset) @@ -473,7 +544,7 @@ string ReadValue(TypeTreeNode node, long offset) return m_Reader.ReadUInt64(offset).ToString(); case TypeCode.SByte: - return m_Reader.ReadUInt8(offset).ToString(); + return m_Reader.ReadInt8(offset).ToString(); case TypeCode.Byte: case TypeCode.Char: @@ -489,28 +560,17 @@ string ReadValue(TypeTreeNode node, long offset) Array ReadBasicTypeArray(TypeTreeNode node, long offset, int arraySize) { - // Special case for boolean arrays. + // bool isn't blittable into Array.CreateInstance(typeof(bool), ...) the way other basic types + // are, so read into a byte buffer and convert. if (node.CSharpType == typeof(bool)) { var tmpArray = new byte[arraySize]; - var boolArray = new bool[arraySize]; - m_Reader.ReadArray(offset, arraySize * node.Size, tmpArray); - - for (int i = 0; i < arraySize; ++i) - { - boolArray[i] = tmpArray[i] != 0; - } - - return boolArray; + return Array.ConvertAll(tmpArray, b => b != 0); } - else - { - var array = Array.CreateInstance(node.CSharpType, arraySize); - m_Reader.ReadArray(offset, arraySize * node.Size, array); - - return array; - } + var array = Array.CreateInstance(node.CSharpType, arraySize); + m_Reader.ReadArray(offset, arraySize * node.Size, array); + return array; } } diff --git a/UnityDataTool.Tests/DumpTests.cs b/UnityDataTool.Tests/DumpTests.cs new file mode 100644 index 0000000..918b4d7 --- /dev/null +++ b/UnityDataTool.Tests/DumpTests.cs @@ -0,0 +1,210 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +public class DumpTests +{ + private string m_TestDataFolder; + private string m_SerializedFilePath; + private string m_ResourceFilePath; + private string m_MultiSerializedFileArchivePath; + + [OneTimeSetUp] + public void OneTimeSetup() + { + m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data"); + m_SerializedFilePath = Path.Combine(m_TestDataFolder, "PlayerWithTypeTrees", "level0"); + m_ResourceFilePath = Path.Combine(m_TestDataFolder, "PlayerWithTypeTrees", "sharedassets0.assets.resS"); + m_MultiSerializedFileArchivePath = Path.Combine(m_TestDataFolder, "PlayerDataCompressed", "data.unity3d"); + } + + [Test] + public async Task Dump_Stdout_DefaultArgs_ContainsExternalReferences() + { + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializedFilePath, "--stdout" })); + } + finally + { + Console.SetOut(currentOut); + } + + var output = sw.ToString(); + Assert.That(output, Does.Contain("External References")); + } + + [Test] + public async Task Dump_Stdout_FilterByObjectId_DumpsGameObject() + { + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializedFilePath, "--stdout", "-i", "1" })); + } + finally + { + Console.SetOut(currentOut); + } + + var output = sw.ToString(); + Assert.That(output, Does.Contain("ID: 1 (ClassID: 1) GameObject")); + Assert.That(output, Does.Contain("m_Name (string) RefHolder")); + } + + [Test] + public async Task Dump_Stdout_FilterByObjectId_DumpsRenderSettings() + { + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializedFilePath, "--stdout", "-i", "3" })); + } + finally + { + Console.SetOut(currentOut); + } + + var output = sw.ToString(); + Assert.That(output, Does.Contain("(ClassID: 104)")); + Assert.That(output, Does.Contain("m_FogColor (ColorRGBA)")); + } + + [Test] + public async Task Dump_Stdout_FilterByObjectId_DoesNotIncludeExternalReferences() + { + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializedFilePath, "--stdout", "-i", "1" })); + } + finally + { + Console.SetOut(currentOut); + } + + var output = sw.ToString(); + Assert.That(output, Does.Not.Contain("External References")); + } + + [Test] + public async Task Dump_Stdout_FilterByType_DoesNotIncludeExternalReferences() + { + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializedFilePath, "--stdout", "-t", "RenderSettings" })); + } + finally + { + Console.SetOut(currentOut); + } + + var output = sw.ToString(); + Assert.That(output, Does.Contain("(ClassID: 104)")); + Assert.That(output, Does.Not.Contain("External References")); + } + + [Test] + public async Task Dump_Stdout_WithOutputPath_ReturnsError() + { + using var swOut = new StringWriter(); + using var swErr = new StringWriter(); + var currentOut = Console.Out; + var currentErr = Console.Error; + try + { + Console.SetOut(swOut); + Console.SetError(swErr); + Assert.AreNotEqual(0, await Program.Main(new string[] { "dump", m_SerializedFilePath, "--stdout", "-o", m_TestDataFolder })); + } + finally + { + Console.SetOut(currentOut); + Console.SetError(currentErr); + } + + Assert.That(swErr.ToString(), Does.Contain("--stdout and -o/--output-path are mutually exclusive.")); + } + + [Test] + public async Task Dump_Stdout_MultipleSerializedFilesArchive_Refused() + { + using var swOut = new StringWriter(); + using var swErr = new StringWriter(); + var currentOut = Console.Out; + var currentErr = Console.Error; + try + { + Console.SetOut(swOut); + Console.SetError(swErr); + Assert.AreNotEqual(0, await Program.Main(new string[] { "dump", m_MultiSerializedFileArchivePath, "--stdout", "-t", "MonoBehaviour" })); + } + finally + { + Console.SetOut(currentOut); + Console.SetError(currentErr); + } + + var err = swErr.ToString(); + Assert.That(err, Does.Contain("--stdout cannot be used with an archive containing multiple SerializedFiles")); + Assert.That(err, Does.Contain("(5 found)")); + } + + [Test] + public async Task Dump_Stdout_FilterByObjectId_NotFound_PrintsNotFoundMessage() + { + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializedFilePath, "--stdout", "-i", "99999999" })); + } + finally + { + Console.SetOut(currentOut); + } + + var output = sw.ToString(); + Assert.That(output, Does.Contain("Object with ID 99999999 not found.")); + } + + [Test] + public async Task Dump_Stdout_InvalidFileType_Fails() + { + using var swOut = new StringWriter(); + using var swErr = new StringWriter(); + var currentOut = Console.Out; + var currentErr = Console.Error; + try + { + Console.SetOut(swOut); + Console.SetError(swErr); + Assert.AreNotEqual(0, await Program.Main(new string[] { "dump", m_ResourceFilePath, "--stdout" })); + } + finally + { + Console.SetOut(currentOut); + Console.SetError(currentErr); + } + + Assert.That(swErr.ToString(), Does.Contain("does not appear to be a valid Unity SerializedFile or Unity Archive")); + } +} diff --git a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs index d5461fe..acffc04 100644 --- a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs +++ b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs @@ -148,6 +148,36 @@ public async Task DumpText_SkipLargeArrays_TextFileCreatedCorrectly( Assert.AreEqual(expected, content); } + [Test] + public async Task DumpText_Stdout_WritesDumpToStdout() + { + var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); + var unwantedOutputFile = Path.Combine(m_TestOutputFolder, "CAB-5d40f7cad7c871cf2ad2af19ac542994.txt"); + + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "dump", path, "--stdout" })); + } + finally + { + Console.SetOut(currentOut); + } + + Assert.IsFalse(File.Exists(unwantedOutputFile), "--stdout should not also write a .txt file"); + + var content = sw.ToString(); + var expected = File.ReadAllText(Path.Combine(Context.ExpectedDataFolder, "dump", "CAB-5d40f7cad7c871cf2ad2af19ac542994.txt")); + + // Normalize line endings. + content = Regex.Replace(content, @"\r\n|\n\r|\r", "\n"); + expected = Regex.Replace(expected, @"\r\n|\n\r|\r", "\n"); + + Assert.AreEqual(expected, content); + } + [Test] public async Task DumpText_TypeFilterByName_OnlyMatchingObjectsDumped() { diff --git a/UnityDataTool/Program.cs b/UnityDataTool/Program.cs index fd5be86..2d5d591 100644 --- a/UnityDataTool/Program.cs +++ b/UnityDataTool/Program.cs @@ -17,245 +17,258 @@ public enum OutputFormat public static class Program { + const string TypeTreeDataDescription = "Path to an external TypeTree data file to load before processing bundles"; + public static async Task Main(string[] args) { UnityFileSystem.Init(); var rootCommand = new RootCommand(); + rootCommand.AddCommand(BuildAnalyzeCommand()); + rootCommand.AddCommand(BuildFindRefsCommand()); + rootCommand.AddCommand(BuildDumpCommand()); + rootCommand.AddCommand(BuildArchiveCommand()); + rootCommand.AddCommand(BuildSerializedFileCommand()); - var typeTreeDataDescription = "Path to an external TypeTree data file to load before processing bundles"; + var r = await rootCommand.InvokeAsync(args); - { - var pathArg = new Argument("path", "The path to the directory containing the files to analyze").ExistingOnly(); - var oOpt = new Option(aliases: new[] { "--output-file", "-o" }, description: "Filename of the output database", getDefaultValue: () => "database.db"); - var sOpt = new Option(aliases: new[] { "--skip-references", "-s" }, description: "Skip CRC and do not extract references"); - var rOpt = new Option(aliases: new[] { "--extract-references", "-r" }) { IsHidden = true }; - var pOpt = new Option(aliases: new[] { "--search-pattern", "-p" }, description: "File search pattern", getDefaultValue: () => "*"); - var vOpt = new Option(aliases: new[] { "--verbose", "-v" }, description: "Verbose output"); - var recurseOpt = new Option(aliases: new[] { "--no-recurse" }, description: "Do not analyze contents of subdirectories inside path"); + UnityFileSystem.Cleanup(); - var dOpt = new Option(aliases: new[] { "--typetree-data", "-d" }, description: typeTreeDataDescription); + return r; + } - var analyzeCommand = new Command("analyze", "Analyze AssetBundles or SerializedFiles.") + static Command BuildAnalyzeCommand() + { + var pathArg = new Argument("path", "The path to the directory containing the files to analyze").ExistingOnly(); + var oOpt = new Option(aliases: new[] { "--output-file", "-o" }, description: "Filename of the output database", getDefaultValue: () => "database.db"); + var sOpt = new Option(aliases: new[] { "--skip-references", "-s" }, description: "Skip CRC and do not extract references"); + var rOpt = new Option(aliases: new[] { "--extract-references", "-r" }) { IsHidden = true }; + var pOpt = new Option(aliases: new[] { "--search-pattern", "-p" }, description: "File search pattern", getDefaultValue: () => "*"); + var vOpt = new Option(aliases: new[] { "--verbose", "-v" }, description: "Verbose output"); + var recurseOpt = new Option(aliases: new[] { "--no-recurse" }, description: "Do not analyze contents of subdirectories inside path"); + var dOpt = new Option(aliases: new[] { "--typetree-data", "-d" }, description: TypeTreeDataDescription); + + var analyzeCommand = new Command("analyze", "Analyze AssetBundles or SerializedFiles.") + { + pathArg, + oOpt, + sOpt, + rOpt, + pOpt, + vOpt, + recurseOpt, + dOpt + }; + + analyzeCommand.AddAlias("analyse"); + analyzeCommand.SetHandler( + (DirectoryInfo di, string o, bool s, bool r, string p, bool v, bool noRecurse, FileInfo d) => { - pathArg, - oOpt, - sOpt, - rOpt, - pOpt, - vOpt, - recurseOpt, - dOpt - }; - - analyzeCommand.AddAlias("analyse"); - analyzeCommand.SetHandler( - (DirectoryInfo di, string o, bool s, bool r, string p, bool v, bool noRecurse, FileInfo d) => - { - var ttResult = LoadTypeTreeDataFile(d); - if (ttResult != 0) return Task.FromResult(ttResult); - return Task.FromResult(HandleAnalyze(di, o, s, r, p, v, noRecurse)); - }, - pathArg, oOpt, sOpt, rOpt, pOpt, vOpt, recurseOpt, dOpt); + var ttResult = LoadTypeTreeDataFile(d); + if (ttResult != 0) return Task.FromResult(ttResult); + return Task.FromResult(HandleAnalyze(di, o, s, r, p, v, noRecurse)); + }, + pathArg, oOpt, sOpt, rOpt, pOpt, vOpt, recurseOpt, dOpt); - rootCommand.AddCommand(analyzeCommand); - } + return analyzeCommand; + } + static Command BuildFindRefsCommand() + { + var pathArg = new Argument("databasePath", "The path to the database generated by the 'analyze' command").ExistingOnly(); + var oOpt = new Option(aliases: new[] { "--output-file", "-o" }, description: "Output file", getDefaultValue: () => "references.txt"); + var iOpt = new Option(aliases: new[] { "--object-id", "-i" }, description: "Object id ('id' column in the database)"); + var nOpt = new Option(aliases: new[] { "--object-name", "-n" }, description: "Object name"); + var tOpt = new Option(aliases: new[] { "--object-type", "-t" }, description: "Optional object type when searching by name"); + var aOpt = new Option(aliases: new[] { "--find-all", "-a" }, description: "Find all reference chains originating from the same asset (instead of only one), can be very slow"); + + var findRefsCommand = new Command("find-refs", "Find reference chains to specified object(s).") { - var pathArg = new Argument("databasePath", "The path to the database generated by the 'analyze' command").ExistingOnly(); - var oOpt = new Option(aliases: new[] { "--output-file", "-o" }, description: "Output file", getDefaultValue: () => "references.txt"); - var iOpt = new Option(aliases: new[] { "--object-id", "-i" }, description: "Object id ('id' column in the database)"); - var nOpt = new Option(aliases: new[] { "--object-name", "-n" }, description: "Object name"); - var tOpt = new Option(aliases: new[] { "--object-type", "-t" }, description: "Optional object type when searching by name"); - var aOpt = new Option(aliases: new[] { "--find-all", "-a" }, description: "Find all reference chains originating from the same asset (instead of only one), can be very slow"); - - var findRefsCommand = new Command("find-refs", "Find reference chains to specified object(s).") - { - pathArg, - oOpt, - aOpt, - nOpt, - tOpt, - iOpt, - }; - - findRefsCommand.SetHandler( - (FileInfo fi, string o, long? i, string n, string t, bool a) => Task.FromResult(HandleFindReferences(fi, o, i, n, t, a)), - pathArg, oOpt, iOpt, nOpt, tOpt, aOpt); - - rootCommand.Add(findRefsCommand); - } + pathArg, + oOpt, + aOpt, + nOpt, + tOpt, + iOpt, + }; + + findRefsCommand.SetHandler( + (FileInfo fi, string o, long? i, string n, string t, bool a) => Task.FromResult(HandleFindReferences(fi, o, i, n, t, a)), + pathArg, oOpt, iOpt, nOpt, tOpt, aOpt); + + return findRefsCommand; + } + static Command BuildDumpCommand() + { + var pathArg = new Argument("filename", "The path of the file to dump").ExistingOnly(); + var fOpt = new Option(aliases: new[] { "--output-format", "-f" }, description: "Output format", getDefaultValue: () => TextDumperTool.DumpFormat.Text); + var sOpt = new Option(aliases: new[] { "--skip-large-arrays", "-s" }, description: "Do not dump large arrays of basic data types"); + var oOpt = new Option(aliases: new[] { "--output-path", "-o" }, description: "Output folder", getDefaultValue: () => new DirectoryInfo(Environment.CurrentDirectory)); + var objectIdOpt = new Option(aliases: new[] { "--objectid", "-i" }, () => 0, "Only dump the object with this signed 64-bit id (default: 0, dump all objects)"); + var typeOpt = new Option(aliases: new[] { "--type", "-t" }, description: "Filter by object type (ClassID number or type name)"); + var stdoutOpt = new Option(aliases: new[] { "--stdout" }, description: "Write the dump to stdout instead of a file. Refused for archives that contain more than one SerializedFile."); + var dOpt = new Option(aliases: new[] { "--typetree-data", "-d" }, description: TypeTreeDataDescription); + + var dumpCommand = new Command("dump", + "Dump serialized objects from a SerializedFile as text.\nFor an archive, dumps the objects from each SerializedFile inside;\nother archive content is ignored (use archive extract for that).") { - var pathArg = new Argument("filename", "The path of the file to dump").ExistingOnly(); - var fOpt = new Option(aliases: new[] { "--output-format", "-f" }, description: "Output format", getDefaultValue: () => DumpFormat.Text); - var sOpt = new Option(aliases: new[] { "--skip-large-arrays", "-s" }, description: "Do not dump large arrays of basic data types"); - var oOpt = new Option(aliases: new[] { "--output-path", "-o" }, description: "Output folder", getDefaultValue: () => new DirectoryInfo(Environment.CurrentDirectory)); - var objectIdOpt = new Option(aliases: new[] { "--objectid", "-i" }, () => 0, "Only dump the object with this signed 64-bit id (default: 0, dump all objects)"); - var typeOpt = new Option(aliases: new[] { "--type", "-t" }, description: "Filter by object type (ClassID number or type name)"); - - var dOpt = new Option(aliases: new[] { "--typetree-data", "-d" }, description: typeTreeDataDescription); - - var dumpCommand = new Command("dump", "Dump the contents of an AssetBundle or SerializedFile.") - { - pathArg, - fOpt, - sOpt, - oOpt, - objectIdOpt, - typeOpt, - dOpt, - }; - dumpCommand.SetHandler( - (FileInfo fi, DumpFormat f, bool s, DirectoryInfo o, long objectId, string type, FileInfo d) => - { - var ttResult = LoadTypeTreeDataFile(d); - if (ttResult != 0) return Task.FromResult(ttResult); - return Task.FromResult(HandleDump(fi, f, s, o, objectId, type)); - }, - pathArg, fOpt, sOpt, oOpt, objectIdOpt, typeOpt, dOpt); - - rootCommand.AddCommand(dumpCommand); - } - + pathArg, + fOpt, + sOpt, + oOpt, + objectIdOpt, + typeOpt, + dOpt, + stdoutOpt, + }; + dumpCommand.AddValidator(commandResult => { - var pathArg = new Argument("filename", "The path of the archive file").ExistingOnly(); - var oOpt = new Option(aliases: new[] { "--output-path", "-o" }, description: "Output directory of the extracted archive", getDefaultValue: () => new DirectoryInfo("archive")); - - var filterOpt = new Option(aliases: new[] { "--filter" }, description: "Case-insensitive substring filter on file paths inside the archive"); - - var extractArchiveCommand = new Command("extract", "Extract an AssetBundle or .data file.") - { - pathArg, - oOpt, - filterOpt, - }; - - extractArchiveCommand.SetHandler( - (FileInfo fi, DirectoryInfo o, string filter) => Task.FromResult(Archive.HandleExtract(fi, o, filter)), - pathArg, oOpt, filterOpt); - - var fOpt = new Option(aliases: new[] { "--format", "-f" }, description: "Output format", getDefaultValue: () => OutputFormat.Text); - - var listArchiveCommand = new Command("list", "List the contents of an AssetBundle or .data file.") - { - pathArg, - fOpt, - }; - - listArchiveCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleList(fi, f)), - pathArg, fOpt); - - var headerArchiveCommand = new Command("header", "Display the header of a Unity Archive file.") + var stdoutResult = commandResult.FindResultFor(stdoutOpt); + var oResult = commandResult.FindResultFor(oOpt); + bool stdoutSet = stdoutResult is { IsImplicit: false }; + bool oExplicit = oResult is { IsImplicit: false }; + if (stdoutSet && oExplicit) { - pathArg, - fOpt, - }; - - headerArchiveCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleHeader(fi, f)), - pathArg, fOpt); - - var blocksArchiveCommand = new Command("blocks", "Display the block list of a Unity Archive file.") - { - pathArg, - fOpt, - }; - - blocksArchiveCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleBlocks(fi, f)), - pathArg, fOpt); - - var infoArchiveCommand = new Command("info", "Display a high-level summary of a Unity Archive file.") + commandResult.ErrorMessage = "--stdout and -o/--output-path are mutually exclusive."; + } + }); + dumpCommand.SetHandler( + (FileInfo fi, TextDumperTool.DumpFormat f, bool s, DirectoryInfo o, long objectId, string type, FileInfo d, bool toStdout) => { - pathArg, - fOpt, - }; - - infoArchiveCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleInfo(fi, f)), - pathArg, fOpt); + var ttResult = LoadTypeTreeDataFile(d); + if (ttResult != 0) return Task.FromResult(ttResult); + var options = new TextDumperTool.DumpOptions + { + Format = f, + Path = fi.FullName, + OutputPath = o.FullName, + SkipLargeArrays = s, + ObjectId = objectId, + TypeFilter = type, + ToStdout = toStdout, + }; + return Task.FromResult(HandleDump(options)); + }, + pathArg, fOpt, sOpt, oOpt, objectIdOpt, typeOpt, dOpt, stdoutOpt); + + return dumpCommand; + } - var archiveCommand = new Command("archive", "Inspect or extract the contents of a Unity archive (AssetBundle or web platform .data file).") - { - extractArchiveCommand, - listArchiveCommand, - headerArchiveCommand, - blocksArchiveCommand, - infoArchiveCommand, - }; - - rootCommand.AddCommand(archiveCommand); - } + static Command BuildArchiveCommand() + { + var pathArg = new Argument("filename", "The path of the archive file").ExistingOnly(); + var oOpt = new Option(aliases: new[] { "--output-path", "-o" }, description: "Output directory of the extracted archive", getDefaultValue: () => new DirectoryInfo("archive")); + var filterOpt = new Option(aliases: new[] { "--filter" }, description: "Case-insensitive substring filter on file paths inside the archive"); + var extractArchiveCommand = new Command("extract", "Extract an AssetBundle or .data file.") { - var pathArg = new Argument("filename", "The path of the SerializedFile").ExistingOnly(); - var fOpt = new Option(aliases: new[] { "--format", "-f" }, description: "Output format", getDefaultValue: () => OutputFormat.Text); - - var externalRefsCommand = new Command("externalrefs", "List external file references in a SerializedFile.") - { - pathArg, - fOpt, - }; - - externalRefsCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleExternalRefs(fi, f)), - pathArg, fOpt); - - var objectListCommand = new Command("objectlist", "List all objects in a SerializedFile.") - { - pathArg, - fOpt, - }; - - objectListCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleObjectList(fi, f)), - pathArg, fOpt); - - var headerCommand = new Command("header", "Show SerializedFile header information.") - { - pathArg, - fOpt, - }; - - headerCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleHeader(fi, f)), - pathArg, fOpt); - - var metadataCommand = new Command("metadata", "Show information from the metadata section of the SerializedFile (use `-f Json` for detailed information).") - { - pathArg, - fOpt, - }; - - metadataCommand.SetHandler( - (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleMetadata(fi, f)), - pathArg, fOpt); - - var serializedFileCommand = new Command("serialized-file", "Inspect a SerializedFile (scene, assets, etc.).") - { - externalRefsCommand, - objectListCommand, - headerCommand, - metadataCommand, - }; - - serializedFileCommand.AddAlias("sf"); - - rootCommand.AddCommand(serializedFileCommand); - } + pathArg, + oOpt, + filterOpt, + }; + extractArchiveCommand.SetHandler( + (FileInfo fi, DirectoryInfo o, string filter) => Task.FromResult(Archive.HandleExtract(fi, o, filter)), + pathArg, oOpt, filterOpt); - var r = await rootCommand.InvokeAsync(args); - - UnityFileSystem.Cleanup(); + var fOpt = new Option(aliases: new[] { "--format", "-f" }, description: "Output format", getDefaultValue: () => OutputFormat.Text); - return r; + var listArchiveCommand = new Command("list", "List the contents of an AssetBundle or .data file.") + { + pathArg, + fOpt, + }; + listArchiveCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleList(fi, f)), + pathArg, fOpt); + + var headerArchiveCommand = new Command("header", "Display the header of a Unity Archive file.") + { + pathArg, + fOpt, + }; + headerArchiveCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleHeader(fi, f)), + pathArg, fOpt); + + var blocksArchiveCommand = new Command("blocks", "Display the block list of a Unity Archive file.") + { + pathArg, + fOpt, + }; + blocksArchiveCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleBlocks(fi, f)), + pathArg, fOpt); + + var infoArchiveCommand = new Command("info", "Display a high-level summary of a Unity Archive file.") + { + pathArg, + fOpt, + }; + infoArchiveCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(Archive.HandleInfo(fi, f)), + pathArg, fOpt); + + return new Command("archive", "Inspect or extract the contents of a Unity archive (AssetBundle or web platform .data file).") + { + extractArchiveCommand, + listArchiveCommand, + headerArchiveCommand, + blocksArchiveCommand, + infoArchiveCommand, + }; } - enum DumpFormat + static Command BuildSerializedFileCommand() { - Text, + var pathArg = new Argument("filename", "The path of the SerializedFile").ExistingOnly(); + var fOpt = new Option(aliases: new[] { "--format", "-f" }, description: "Output format", getDefaultValue: () => OutputFormat.Text); + + var externalRefsCommand = new Command("externalrefs", "List external file references in a SerializedFile.") + { + pathArg, + fOpt, + }; + externalRefsCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleExternalRefs(fi, f)), + pathArg, fOpt); + + var objectListCommand = new Command("objectlist", "List all objects in a SerializedFile.") + { + pathArg, + fOpt, + }; + objectListCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleObjectList(fi, f)), + pathArg, fOpt); + + var headerCommand = new Command("header", "Show SerializedFile header information.") + { + pathArg, + fOpt, + }; + headerCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleHeader(fi, f)), + pathArg, fOpt); + + var metadataCommand = new Command("metadata", "Show information from the metadata section of the SerializedFile (use `-f Json` for detailed information).") + { + pathArg, + fOpt, + }; + metadataCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleMetadata(fi, f)), + pathArg, fOpt); + + var serializedFileCommand = new Command("serialized-file", "Inspect a SerializedFile (scene, assets, etc.).") + { + externalRefsCommand, + objectListCommand, + headerCommand, + metadataCommand, + }; + serializedFileCommand.AddAlias("sf"); + return serializedFileCommand; } static int LoadTypeTreeDataFile(FileInfo typeTreeDataFile) @@ -315,17 +328,8 @@ static int HandleFindReferences(FileInfo databasePath, string outputFile, long? } } - static int HandleDump(FileInfo filename, DumpFormat format, bool skipLargeArrays, DirectoryInfo outputFolder, long objectId = 0, string typeFilter = null) + static int HandleDump(TextDumperTool.DumpOptions options) { - switch (format) - { - case DumpFormat.Text: - { - var textDumper = new TextDumperTool(); - return textDumper.Dump(filename.FullName, outputFolder.FullName, skipLargeArrays, objectId, typeFilter); - } - } - - return 1; + return new TextDumperTool().Dump(options); } }