Skip to content

Commit 877fd30

Browse files
Detect and specific warning for text format SerializedFiles
1 parent d2a99cd commit 877fd30

4 files changed

Lines changed: 242 additions & 0 deletions

File tree

Analyzer.Tests/FileDetectionTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,64 @@ public void TryDetectSerializedFile_NonExistentFile_ReturnsFalse()
168168

169169
#endregion
170170

171+
#region YAML SerializedFile Detection Tests
172+
173+
[Test]
174+
public void IsYamlSerializedFile_ValidYamlAsset_ReturnsTrue()
175+
{
176+
var testFile = Path.Combine(m_TestDataPath, "YamlFormat.asset");
177+
178+
bool result = YamlSerializedFileDetector.IsYamlSerializedFile(testFile);
179+
180+
Assert.IsTrue(result, "YamlFormat.asset should be detected as a YAML SerializedFile");
181+
}
182+
183+
[Test]
184+
public void IsYamlSerializedFile_BinarySerializedFile_ReturnsFalse()
185+
{
186+
var testFile = Path.Combine(m_TestDataPath, "PlayerData", "2022.1.20f1", "level0");
187+
188+
bool result = YamlSerializedFileDetector.IsYamlSerializedFile(testFile);
189+
190+
Assert.IsFalse(result, "Binary SerializedFile should not be detected as YAML");
191+
}
192+
193+
[Test]
194+
public void IsYamlSerializedFile_Archive_ReturnsFalse()
195+
{
196+
var testFile = Path.Combine(m_TestDataPath, "AssetBundles", "2022.1.20f1", "assetbundle");
197+
198+
bool result = YamlSerializedFileDetector.IsYamlSerializedFile(testFile);
199+
200+
Assert.IsFalse(result, "AssetBundle should not be detected as YAML");
201+
}
202+
203+
[Test]
204+
public void IsYamlSerializedFile_JsonFile_ReturnsFalse()
205+
{
206+
var testFiles = Directory.GetFiles(Path.Combine(m_TestDataPath, "AddressableBuildLayouts"), "*.json");
207+
Assert.Greater(testFiles.Length, 0, "Should have at least one JSON test file");
208+
209+
foreach (var testFile in testFiles)
210+
{
211+
bool result = YamlSerializedFileDetector.IsYamlSerializedFile(testFile);
212+
213+
Assert.IsFalse(result, $"JSON file should not be detected as YAML SerializedFile: {Path.GetFileName(testFile)}");
214+
}
215+
}
216+
217+
[Test]
218+
public void IsYamlSerializedFile_NonExistentFile_ReturnsFalse()
219+
{
220+
var nonExistentFile = Path.Combine(m_TestDataPath, "ThisFileDoesNotExist.asset");
221+
222+
bool result = YamlSerializedFileDetector.IsYamlSerializedFile(nonExistentFile);
223+
224+
Assert.IsFalse(result, "Non-existent file should not be detected as YAML");
225+
}
226+
227+
#endregion
228+
171229
#region Archive Detection Tests
172230

173231
[Test]
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.IO;
3+
using System.Text;
4+
5+
namespace UnityDataTools.Analyzer.Util;
6+
7+
/// <summary>
8+
/// Utility for detecting YAML-format Unity SerializedFiles.
9+
///
10+
/// Unity SerializedFiles can be stored in two formats:
11+
/// 1. Binary format (used in builds and by Unity Runtime) - detected by SerializedFileDetector
12+
/// 2. YAML format (text format used in Editor for .asset, .prefab, .unity files) - detected by this class
13+
///
14+
/// The YAML format is not supported by Unity Runtime and cannot be read by UnityDataTool,
15+
/// which only supports the binary format.
16+
///
17+
/// YAML SerializedFiles begin with the magic string "%YAML 1.1", optionally preceded by
18+
/// a UTF-8 BOM (byte order mark: 0xEF 0xBB 0xBF).
19+
///
20+
/// This implementation is based on Unity's SerializedFile.cpp::FinalizeInitRead() and
21+
/// IsSerializedFileTextFile() from Runtime/Serialize/SerializedFile.cpp (lines 101-117, 668-687).
22+
/// </summary>
23+
public static class YamlSerializedFileDetector
24+
{
25+
// Magic string that identifies YAML-format SerializedFiles
26+
private const string UnityTextMagicString = "%YAML 1.1";
27+
28+
// UTF-8 BOM (Byte Order Mark): EF BB BF
29+
private static readonly byte[] Utf8Bom = new byte[] { 0xEF, 0xBB, 0xBF };
30+
31+
/// <summary>
32+
/// Checks if a file is a YAML-format Unity SerializedFile by reading the magic string
33+
/// at the beginning of the file.
34+
/// </summary>
35+
/// <param name="filePath">Path to the file to check</param>
36+
/// <returns>True if the file starts with "%YAML 1.1" (optionally with UTF-8 BOM), false otherwise</returns>
37+
public static bool IsYamlSerializedFile(string filePath)
38+
{
39+
if (!File.Exists(filePath))
40+
return false;
41+
42+
try
43+
{
44+
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
45+
46+
// Unity checks for UTF-8 BOM (3 bytes) + magic string (9 bytes) = 12 bytes total
47+
const int bomLength = 3;
48+
int magicLength = UnityTextMagicString.Length;
49+
int bufferSize = bomLength + magicLength;
50+
51+
if (stream.Length < magicLength)
52+
return false;
53+
54+
byte[] buffer = new byte[bufferSize];
55+
int bytesRead = stream.Read(buffer, 0, Math.Min(bufferSize, (int)stream.Length));
56+
57+
if (bytesRead < magicLength)
58+
return false;
59+
60+
// Check if file starts with UTF-8 BOM
61+
int offset = 0;
62+
if (bytesRead >= bomLength && HasUtf8Bom(buffer))
63+
{
64+
offset = bomLength;
65+
}
66+
67+
// Check for magic string after BOM (if present)
68+
if (bytesRead - offset < magicLength)
69+
return false;
70+
71+
string fileStart = Encoding.ASCII.GetString(buffer, offset, magicLength);
72+
return fileStart == UnityTextMagicString;
73+
}
74+
catch
75+
{
76+
// Any exception during file reading means this isn't a valid YAML file
77+
return false;
78+
}
79+
}
80+
81+
/// <summary>
82+
/// Checks if a byte array starts with UTF-8 BOM (0xEF 0xBB 0xBF).
83+
/// </summary>
84+
private static bool HasUtf8Bom(byte[] buffer)
85+
{
86+
if (buffer.Length < Utf8Bom.Length)
87+
return false;
88+
89+
for (int i = 0; i < Utf8Bom.Length; i++)
90+
{
91+
if (buffer[i] != Utf8Bom[i])
92+
return false;
93+
}
94+
95+
return true;
96+
}
97+
}

UnityDataTool.Tests/SerializedFileCommandTests.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,80 @@ public async Task ErrorHandling_InvalidFile_ShowsHelpfulMessage()
679679
}
680680
}
681681

682+
[Test]
683+
public async Task ErrorHandling_YamlFile_ReturnsHelpfulError()
684+
{
685+
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "YamlFormat.asset");
686+
687+
using var sw = new StringWriter();
688+
var currentErr = Console.Error;
689+
try
690+
{
691+
Console.SetError(sw);
692+
693+
var result = await Program.Main(new string[] { "serialized-file", "header", path });
694+
695+
Assert.AreNotEqual(0, result, "Should return error code for YAML file");
696+
697+
var errorOutput = sw.ToString();
698+
StringAssert.Contains("YAML-format SerializedFile", errorOutput, "Error message should mention YAML format");
699+
StringAssert.Contains("not supported", errorOutput, "Error message should explain YAML is not supported");
700+
StringAssert.Contains("binary-format", errorOutput, "Error message should mention binary format is supported");
701+
}
702+
finally
703+
{
704+
Console.SetError(currentErr);
705+
}
706+
}
707+
708+
[Test]
709+
public async Task ErrorHandling_YamlFile_ExternalRefs_ReturnsHelpfulError()
710+
{
711+
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "YamlFormat.asset");
712+
713+
using var sw = new StringWriter();
714+
var currentErr = Console.Error;
715+
try
716+
{
717+
Console.SetError(sw);
718+
719+
var result = await Program.Main(new string[] { "serialized-file", "externalrefs", path });
720+
721+
Assert.AreNotEqual(0, result, "Should return error code for YAML file");
722+
723+
var errorOutput = sw.ToString();
724+
StringAssert.Contains("YAML-format SerializedFile", errorOutput, "Error message should mention YAML format");
725+
}
726+
finally
727+
{
728+
Console.SetError(currentErr);
729+
}
730+
}
731+
732+
[Test]
733+
public async Task ErrorHandling_YamlFile_ObjectList_ReturnsHelpfulError()
734+
{
735+
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "YamlFormat.asset");
736+
737+
using var sw = new StringWriter();
738+
var currentErr = Console.Error;
739+
try
740+
{
741+
Console.SetError(sw);
742+
743+
var result = await Program.Main(new string[] { "sf", "objectlist", path });
744+
745+
Assert.AreNotEqual(0, result, "Should return error code for YAML file");
746+
747+
var errorOutput = sw.ToString();
748+
StringAssert.Contains("YAML-format SerializedFile", errorOutput, "Error message should mention YAML format");
749+
}
750+
finally
751+
{
752+
Console.SetError(currentErr);
753+
}
754+
}
755+
682756
#endregion
683757
}
684758

UnityDataTool/SerializedFileCommands.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ private static bool ValidateSerializedFile(string filePath, out SerializedFileIn
9494
return false;
9595
}
9696

97+
if (YamlSerializedFileDetector.IsYamlSerializedFile(filePath))
98+
{
99+
Console.Error.WriteLine($"Error: The file is a YAML-format SerializedFile, which is not supported.");
100+
Console.Error.WriteLine($"File: {filePath}");
101+
Console.Error.WriteLine();
102+
Console.Error.WriteLine("UnityDataTool only supports binary-format SerializedFiles (used in builds).");
103+
Console.Error.WriteLine("YAML-format SerializedFiles are used by Unity Editor for .asset, .prefab, and .unity files.");
104+
Console.Error.WriteLine();
105+
Console.Error.WriteLine("To analyze YAML files, you must first build your project. Then run UnityDataTool");
106+
Console.Error.WriteLine("on the build output, which contains binary-format SerializedFiles.");
107+
return false;
108+
}
109+
97110
if (!SerializedFileDetector.TryDetectSerializedFile(filePath, out fileInfo))
98111
{
99112
Console.Error.WriteLine($"Error: The file does not appear to be a valid Unity SerializedFile.");

0 commit comments

Comments
 (0)