From 8024322be8d68af5ab48d87d03826d9313b7b205 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Wed, 18 Feb 2026 11:50:19 -0500 Subject: [PATCH 1/6] Improve CanParse method for buildlayout files Previous implementation didn't work for files with line endings. Fix is to only read a fixed lenght into the file. --- .../Parsers/AddressablesBuildLayoutParser.cs | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs b/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs index 9907f33..4ac13e6 100644 --- a/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs +++ b/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs @@ -32,32 +32,21 @@ public bool CanParse(string filename) if (Path.GetExtension(filename) != ".json") return false; - // Read the first line of the JSON file and check if it contains BuildResultHash - string firstLine = ""; + // Read enough content to check if it contains BuildResultHash + // This handles both minified and pretty-printed JSON try { using (StreamReader reader = new StreamReader(filename)) { - firstLine = reader.ReadLine(); - if (firstLine != null) - { - // Remove trailing comma if present and add closing brace to make it valid JSON - if (firstLine.TrimEnd().EndsWith(",")) - { - firstLine = firstLine.TrimEnd().TrimEnd(',') + "}"; - } - - using (JsonTextReader jsonReader = new JsonTextReader(new StringReader(firstLine))) - { - JsonSerializer serializer = new JsonSerializer(); - var jsonObject = serializer.Deserialize(jsonReader); + // Read first 4KB which should be enough to find BuildResultHash near the start + char[] buffer = new char[4096]; + int charsRead = reader.Read(buffer, 0, buffer.Length); + string content = new string(buffer, 0, charsRead); - // If the file has BuildResultHash, process it as an Addressables build - if (jsonObject != null && jsonObject["BuildResultHash"] != null) - { - return true; - } - } + // Check if BuildResultHash appears in the content + if (content.Contains("\"BuildResultHash\"")) + { + return true; } } } From 83d2951d625c8932ea5d248a25d3578fbecf5a85 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Wed, 18 Feb 2026 12:14:07 -0500 Subject: [PATCH 2/6] Issue #48 - Fix reporting of failed analyze files --- Analyzer/SQLite/Parsers/SerializedFileParser.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Analyzer/SQLite/Parsers/SerializedFileParser.cs b/Analyzer/SQLite/Parsers/SerializedFileParser.cs index acd1658..b41daee 100644 --- a/Analyzer/SQLite/Parsers/SerializedFileParser.cs +++ b/Analyzer/SQLite/Parsers/SerializedFileParser.cs @@ -35,7 +35,11 @@ public void Parse(string filename) { // only init our writer if we are actually parsing a file m_Writer.Init(); - ProcessFile(filename, Path.GetDirectoryName(filename)); + bool successful = ProcessFile(filename, Path.GetDirectoryName(filename)); + if (!successful) + { + throw new Exception($"Failed to process file: {filename}"); + } } bool ShouldIgnoreFile(string file) @@ -134,14 +138,10 @@ bool ProcessFile(string file, string rootDirectory) successful = false; } - catch (Exception e) + catch (Exception) { - Console.Error.WriteLine(); - Console.Error.WriteLine($"Error processing file: {file}"); - Console.WriteLine($"{e.GetType()}: {e.Message}"); - if (Verbose) - Console.WriteLine(e.StackTrace); - + // Don't log the error here - it will be logged by AnalyzerTool + // Just mark as unsuccessful and let the exception propagate up via Parse() successful = false; } From 40f7f9c76cb982dd27f2a1392afcc25e18495062 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Wed, 18 Feb 2026 12:38:14 -0500 Subject: [PATCH 3/6] Improve error reporting when there are failures --- Analyzer/AnalyzerTool.cs | 12 +- .../SQLite/Parsers/SerializedFileParser.cs | 107 ++++++++---------- 2 files changed, 53 insertions(+), 66 deletions(-) diff --git a/Analyzer/AnalyzerTool.cs b/Analyzer/AnalyzerTool.cs index 4fd5364..ba7a5f4 100644 --- a/Analyzer/AnalyzerTool.cs +++ b/Analyzer/AnalyzerTool.cs @@ -77,11 +77,15 @@ public int Analyze( catch (Exception e) { EraseProgressLine(); - Console.Error.WriteLine(); - Console.Error.WriteLine($"Error processing file: {file}"); - Console.WriteLine($"{e.GetType()}: {e.Message}"); + var relativePath = Path.GetRelativePath(path, file); + Console.Error.WriteLine($"Failed to process: {relativePath}"); if (m_Verbose) - Console.WriteLine(e.StackTrace); + { + Console.Error.WriteLine($" Exception: {e.GetType().Name}: {e.Message}"); + if (e.InnerException != null) + Console.Error.WriteLine($" Inner: {e.InnerException.Message}"); + Console.Error.WriteLine(e.StackTrace); + } countFailures++; } } diff --git a/Analyzer/SQLite/Parsers/SerializedFileParser.cs b/Analyzer/SQLite/Parsers/SerializedFileParser.cs index b41daee..5aa487e 100644 --- a/Analyzer/SQLite/Parsers/SerializedFileParser.cs +++ b/Analyzer/SQLite/Parsers/SerializedFileParser.cs @@ -35,11 +35,7 @@ public void Parse(string filename) { // only init our writer if we are actually parsing a file m_Writer.Init(); - bool successful = ProcessFile(filename, Path.GetDirectoryName(filename)); - if (!successful) - { - throw new Exception($"Failed to process file: {filename}"); - } + ProcessFile(filename, Path.GetDirectoryName(filename)); } bool ShouldIgnoreFile(string file) @@ -71,81 +67,68 @@ bool ShouldIgnoreFile(string file) ".ini", ".config", ".hash", ".md" }; - bool ProcessFile(string file, string rootDirectory) + void ProcessFile(string file, string rootDirectory) { - bool successful = true; - try + if (IsUnityArchive(file)) { - if (IsUnityArchive(file)) + bool archiveHadErrors = false; + using (UnityArchive archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar)) { - using (UnityArchive archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar)) - { - if (archive == null) - throw new FileLoadException($"Failed to mount archive: {file}"); + if (archive == null) + throw new FileLoadException($"Failed to mount archive: {file}"); - try - { - var assetBundleName = Path.GetRelativePath(rootDirectory, file); + try + { + var assetBundleName = Path.GetRelativePath(rootDirectory, file); - m_Writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length); + m_Writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length); - foreach (var node in archive.Nodes) + foreach (var node in archive.Nodes) + { + if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) { - if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) + try + { + m_Writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file)); + } + catch (Exception e) { - try - { - m_Writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file)); - } - catch (Exception e) - { - // the most likely exception here is Microsoft.Data.Sqlite.SqliteException, - // for example 'UNIQUE constraint failed: serialized_files.id'. - // or 'UNIQUE constraint failed: objects.id' which can happen - // if AssetBundles from different builds are being processed by a single call to Analyze - // or if there is a Unity Data Tool bug. - Console.Error.WriteLine($"Error processing {node.Path} in archive {file}"); - Console.Error.WriteLine(e.Message); - Console.WriteLine(); - - // It is possible some files inside an archive will pass and others will fail, to have a partial analyze. - // Overall that is reported as a failure - successful = false; - } + // the most likely exception here is Microsoft.Data.Sqlite.SqliteException, + // for example 'UNIQUE constraint failed: serialized_files.id'. + // or 'UNIQUE constraint failed: objects.id' which can happen + // if AssetBundles from different builds are being processed by a single call to Analyze + // or if there is a Unity Data Tool bug. + Console.Error.WriteLine($"Error processing {node.Path} in archive {file}"); + Console.Error.WriteLine(e.Message); + Console.WriteLine(); + + // It is possible some files inside an archive will pass and others will fail, to have a partial analyze. + // Overall that is reported as a failure + archiveHadErrors = true; } } } - finally - { - m_Writer.EndAssetBundle(); - } + } + finally + { + m_Writer.EndAssetBundle(); } } - else + + if (archiveHadErrors) { - // This isn't a Unity Archive file. Try to open it as a SerializedFile. - // Unfortunately there is no standard file extension, or clear signature at the start of the file, - // to test if it truly is a SerializedFile. So this will process files that are clearly not unity build files, - // and there is a chance for crashes and freezes if the parser misinterprets the file content. - var relativePath = Path.GetRelativePath(rootDirectory, file); - m_Writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file)); + throw new Exception("One or more files in the archive failed to process"); } } - catch (NotSupportedException) + else { - Console.Error.WriteLine(); - //A "failed to load" error will already be logged by the UnityFileSystem library - - successful = false; - } - catch (Exception) - { - // Don't log the error here - it will be logged by AnalyzerTool - // Just mark as unsuccessful and let the exception propagate up via Parse() - successful = false; + // This isn't a Unity Archive file. Try to open it as a SerializedFile. + // Unfortunately there is no standard file extension, or clear signature at the start of the file, + // to test if it truly is a SerializedFile. So this will process files that are clearly not unity build files, + // and there is a chance for crashes and freezes if the parser misinterprets the file content. + var relativePath = Path.GetRelativePath(rootDirectory, file); + m_Writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file)); } - - return successful; } private static bool IsUnityArchive(string filePath) From b6bd8e27f7a65e337d8616a3452fdd72a3bf6883 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Wed, 18 Feb 2026 12:53:04 -0500 Subject: [PATCH 4/6] Add test for issue #48 error reporting --- .../UnityDataToolPlayerDataTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs index 2c3cb83..04fa0e2 100644 --- a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs +++ b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs @@ -85,4 +85,39 @@ public async Task DumpText_PlayerData_TextFileCreatedCorrectly() Assert.AreEqual(expected, content); } + + [Test] + public async Task Analyze_PlayerDataNoTypeTree_ReportsFailureCorrectly() + { + // Test for issue #48: Files that fail to process should be counted as failures, not successes + var testDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "PlayerNoTypeTree"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + 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); + + // Analyze should return 0 even if files fail (non-zero would be a critical error) + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", testDataFolder, "-p", "level0" })); + + var output = swOut.ToString() + swErr.ToString(); + + // Check that the filename appears in the error output + Assert.That(output, Does.Contain("level0"), "Expected 'level0' to appear in error output"); + + // Check that the summary line correctly reports the failure + Assert.That(output, Does.Contain("Failed files: 1"), "Expected 'Failed files: 1' in summary"); + Assert.That(output, Does.Contain("Successfully processed files: 0"), "Expected 'Successfully processed files: 0' in summary"); + } + finally + { + Console.SetOut(currentOut); + Console.SetError(currentErr); + } + } } From f46adc431a05a8f4fef2799816d2995f4c0b7a78 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski <86242170+SkowronskiAndrew@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:50:06 -0500 Subject: [PATCH 5/6] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Analyzer/SQLite/Parsers/SerializedFileParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Analyzer/SQLite/Parsers/SerializedFileParser.cs b/Analyzer/SQLite/Parsers/SerializedFileParser.cs index 5aa487e..27b994e 100644 --- a/Analyzer/SQLite/Parsers/SerializedFileParser.cs +++ b/Analyzer/SQLite/Parsers/SerializedFileParser.cs @@ -98,9 +98,9 @@ void ProcessFile(string file, string rootDirectory) // or 'UNIQUE constraint failed: objects.id' which can happen // if AssetBundles from different builds are being processed by a single call to Analyze // or if there is a Unity Data Tool bug. - Console.Error.WriteLine($"Error processing {node.Path} in archive {file}"); + Console.Error.WriteLine($"Error processing {node.Path} in archive {assetBundleName}"); Console.Error.WriteLine(e.Message); - Console.WriteLine(); + Console.Error.WriteLine(); // It is possible some files inside an archive will pass and others will fail, to have a partial analyze. // Overall that is reported as a failure From 908cbbd4928ff1446e4342cf04f2033c941ac068 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Wed, 18 Feb 2026 16:55:20 -0500 Subject: [PATCH 6/6] Code review comment - Remove unused variable from test --- UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs index 04fa0e2..f1ba0f3 100644 --- a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs +++ b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs @@ -91,7 +91,6 @@ public async Task Analyze_PlayerDataNoTypeTree_ReportsFailureCorrectly() { // Test for issue #48: Files that fail to process should be counted as failures, not successes var testDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "PlayerNoTypeTree"); - var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); using var swOut = new StringWriter(); using var swErr = new StringWriter();