Skip to content

Commit 03a513c

Browse files
Adding "header" subcommand for serialized file
1 parent ea3ff65 commit 03a513c

4 files changed

Lines changed: 262 additions & 0 deletions

File tree

Documentation/command-serialized-file.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The `serialized-file` command (alias: `sf`) provides utilities for quickly inspe
88
|-------------|-------------|
99
| [`externalrefs`](#externalrefs) | List external file references |
1010
| [`objectlist`](#objectlist) | List all objects in the file |
11+
| [`header`](#header) | Show SerializedFile header information |
1112

1213
---
1314

@@ -128,13 +129,79 @@ UnityDataTool serialized-file objectlist level0 --format json
128129

129130
---
130131

132+
## header
133+
134+
Shows the SerializedFile header information. This is useful for testing whether a file is a valid SerializedFile and for inspecting the version and structure.
135+
136+
### Quick Reference
137+
138+
```
139+
UnityDataTool serialized-file header <filename> [options]
140+
UnityDataTool sf header <filename> [options]
141+
```
142+
143+
| Option | Description | Default |
144+
|--------|-------------|---------|
145+
| `<filename>` | Path to the SerializedFile | *(required)* |
146+
| `-f, --format <format>` | Output format: `Text` or `Json` | `Text` |
147+
148+
### Example - Text Output
149+
150+
```bash
151+
UnityDataTool sf header sharedassets0.assets
152+
```
153+
154+
**Output:**
155+
```
156+
Version 22
157+
Format Modern (64-bit)
158+
File Size 1,234,567 bytes
159+
Metadata Size 45,678 bytes
160+
Data Offset 45,728
161+
Endianness Little Endian
162+
```
163+
164+
### Example - JSON Output
165+
166+
```bash
167+
UnityDataTool serialized-file header level0 --format json
168+
```
169+
170+
**Output:**
171+
```json
172+
{
173+
"version": 22,
174+
"format": "Modern (64-bit)",
175+
"fileSize": 31988,
176+
"metadataSize": 24580,
177+
"dataOffset": 24640,
178+
"endianness": "Big Endian"
179+
}
180+
```
181+
182+
### Header Fields
183+
184+
| Field | Description |
185+
|-------|-------------|
186+
| **Version** | SerializedFile format version. Modern Unity (2019+) uses version 22+. |
187+
| **Format** | Header format type: "Legacy (32-bit)" for versions < 22, or "Modern (64-bit)" for versions ≥ 22. Modern format supports files larger than 4GB. |
188+
| **File Size** | Total size of the SerializedFile in bytes. Padding might make the actual file size slightly larger. |
189+
| **Metadata Size** | Size of the metadata section containing type information and object indices. |
190+
| **Data Offset** | Byte offset where the object data section begins in the file. |
191+
| **Endianness** | Byte order of the file: "Little Endian" (x86, most platforms) or "Big Endian" (older console platforms). |
192+
193+
---
194+
131195
## Use Cases
132196

133197
### Quick File Inspection
134198

135199
Use `serialized-file` when you need quick information about a SerializedFile without generating a full SQLite database:
136200

137201
```bash
202+
# Check file format and version
203+
UnityDataTool sf header level0
204+
138205
# Check what objects are in a file
139206
UnityDataTool sf objectlist sharedassets0.assets
140207

UnityDataTool.Tests/SerializedFileCommandTests.cs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,123 @@ public async Task ObjectList_SharedAssets_ContainsExpectedTypes()
295295

296296
#endregion
297297

298+
#region Header Tests
299+
300+
[Test]
301+
public async Task Header_TextFormat_OutputsCorrectly()
302+
{
303+
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "PlayerNoTypeTree", "sharedassets0.assets");
304+
using var sw = new StringWriter();
305+
var currentOut = Console.Out;
306+
try
307+
{
308+
Console.SetOut(sw);
309+
310+
Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "header", path }));
311+
312+
var output = sw.ToString();
313+
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
314+
315+
// Should have header information lines
316+
Assert.Greater(lines.Length, 0, "Expected header information");
317+
318+
// Check for expected fields
319+
StringAssert.Contains("Version", output);
320+
StringAssert.Contains("Format", output);
321+
StringAssert.Contains("File Size", output);
322+
StringAssert.Contains("Metadata Size", output);
323+
StringAssert.Contains("Data Offset", output);
324+
StringAssert.Contains("Endianness", output);
325+
}
326+
finally
327+
{
328+
Console.SetOut(currentOut);
329+
}
330+
}
331+
332+
[Test]
333+
public async Task Header_JsonFormat_OutputsValidJson()
334+
{
335+
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "BuildReports", "Player.buildreport");
336+
using var sw = new StringWriter();
337+
var currentOut = Console.Out;
338+
try
339+
{
340+
Console.SetOut(sw);
341+
342+
Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "header", path, "-f", "json" }));
343+
344+
var output = sw.ToString();
345+
346+
// Parse JSON to verify it's valid
347+
var jsonDoc = JsonDocument.Parse(output);
348+
var root = jsonDoc.RootElement;
349+
350+
// Verify all expected properties are present
351+
Assert.IsTrue(root.TryGetProperty("version", out _));
352+
Assert.IsTrue(root.TryGetProperty("format", out _));
353+
Assert.IsTrue(root.TryGetProperty("fileSize", out _));
354+
Assert.IsTrue(root.TryGetProperty("metadataSize", out _));
355+
Assert.IsTrue(root.TryGetProperty("dataOffset", out _));
356+
Assert.IsTrue(root.TryGetProperty("endianness", out _));
357+
358+
// Verify version is a number
359+
var version = root.GetProperty("version").GetUInt32();
360+
Assert.Greater(version, 0u, "Version should be greater than 0");
361+
362+
// Verify format is a valid string
363+
var format = root.GetProperty("format").GetString();
364+
Assert.IsTrue(format == "Legacy (32-bit)" || format == "Modern (64-bit)",
365+
$"Format should be either Legacy or Modern, got: {format}");
366+
}
367+
finally
368+
{
369+
Console.SetOut(currentOut);
370+
}
371+
}
372+
373+
[Test]
374+
public async Task Header_InvalidFile_ReturnsError()
375+
{
376+
var path = Path.Combine(m_TestDataFolder, "README.md");
377+
378+
var result = await Program.Main(new string[] { "serialized-file", "header", path });
379+
Assert.AreNotEqual(0, result, "Should return error code for invalid file");
380+
}
381+
382+
[Test]
383+
public async Task Header_ArchiveFile_ReturnsError()
384+
{
385+
var legacyDir = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "LegacyFormats");
386+
var archivePath = Path.Combine(legacyDir, "alienprefab");
387+
388+
if (!File.Exists(archivePath))
389+
{
390+
Assert.Ignore("alienprefab test file not found");
391+
return;
392+
}
393+
394+
using var sw = new StringWriter();
395+
var currentErr = Console.Error;
396+
try
397+
{
398+
Console.SetError(sw);
399+
400+
var result = await Program.Main(new string[] { "serialized-file", "header", archivePath });
401+
402+
Assert.AreNotEqual(0, result, "Should return error code for archive file");
403+
404+
var errorOutput = sw.ToString();
405+
StringAssert.Contains("Unity Archive", errorOutput, "Error message should mention Unity Archive");
406+
}
407+
finally
408+
{
409+
Console.SetError(currentErr);
410+
}
411+
}
412+
413+
#endregion
414+
298415
#region Cross-Validation with Analyze Command
299416

300417
[Test]

UnityDataTool/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,21 @@ public static async Task<int> Main(string[] args)
154154
(FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleObjectList(fi, f)),
155155
pathArg, fOpt);
156156

157+
var headerCommand = new Command("header", "Show SerializedFile header information.")
158+
{
159+
pathArg,
160+
fOpt,
161+
};
162+
163+
headerCommand.SetHandler(
164+
(FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleHeader(fi, f)),
165+
pathArg, fOpt);
166+
157167
var serializedFileCommand = new Command("serialized-file", "Inspect a SerializedFile (scene, assets, etc.).")
158168
{
159169
externalRefsCommand,
160170
objectListCommand,
171+
headerCommand,
161172
};
162173

163174
serializedFileCommand.AddAlias("sf");

UnityDataTool/SerializedFileCommands.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,47 @@ public static int HandleObjectList(FileInfo filename, OutputFormat format)
3030
});
3131
}
3232

33+
public static int HandleHeader(FileInfo filename, OutputFormat format)
34+
{
35+
string filePath = filename.FullName;
36+
37+
// Check if file exists
38+
if (!File.Exists(filePath))
39+
{
40+
Console.Error.WriteLine($"Error: File not found: {filePath}");
41+
return 1;
42+
}
43+
44+
// Detect if it's an archive (not supported for header command)
45+
if (ArchiveDetector.IsUnityArchive(filePath))
46+
{
47+
Console.Error.WriteLine($"Error: The file is an AssetBundle or other Unity Archive, not a SerializedFile.");
48+
Console.Error.WriteLine($"File: {filePath}");
49+
Console.Error.WriteLine();
50+
Console.Error.WriteLine("Unity Archives do not have a SerializedFile header.");
51+
Console.Error.WriteLine("Extract the archive first to access SerializedFiles inside:");
52+
Console.Error.WriteLine($" UnityDataTool archive extract \"{filePath}\" -o <output-directory>");
53+
return 1;
54+
}
55+
56+
// Try to detect the SerializedFile header
57+
bool isSerializedFile = SerializedFileDetector.TryDetectSerializedFile(filePath, out var fileInfo);
58+
if (!isSerializedFile || fileInfo == null)
59+
{
60+
Console.Error.WriteLine($"Error: The file does not appear to be a valid Unity SerializedFile.");
61+
Console.Error.WriteLine($"File: {filePath}");
62+
return 1;
63+
}
64+
65+
// Output the header information
66+
if (format == OutputFormat.Json)
67+
OutputHeaderJson(fileInfo);
68+
else
69+
OutputHeaderText(fileInfo);
70+
71+
return 0;
72+
}
73+
3374
/// <summary>
3475
/// Opens a SerializedFile and processes it with the given action.
3576
/// Provides helpful error messages if the file is not a valid SerializedFile.
@@ -169,5 +210,31 @@ private static string GetTypeName(SerializedFile sf, ObjectInfo obj)
169210
return TypeIdRegistry.GetTypeName(obj.TypeId);
170211
}
171212
}
213+
214+
private static void OutputHeaderText(SerializedFileInfo info)
215+
{
216+
Console.WriteLine($"{"Version",-20} {info.Version}");
217+
Console.WriteLine($"{"Format",-20} {(info.IsLegacyFormat ? "Legacy (32-bit)" : "Modern (64-bit)")}");
218+
Console.WriteLine($"{"File Size",-20} {info.FileSize:N0} bytes");
219+
Console.WriteLine($"{"Metadata Size",-20} {info.MetadataSize:N0} bytes");
220+
Console.WriteLine($"{"Data Offset",-20} {info.DataOffset:N0}");
221+
Console.WriteLine($"{"Endianness",-20} {(info.Endianness == 0 ? "Little Endian" : "Big Endian")}");
222+
}
223+
224+
private static void OutputHeaderJson(SerializedFileInfo info)
225+
{
226+
var jsonObject = new
227+
{
228+
version = info.Version,
229+
format = info.IsLegacyFormat ? "Legacy (32-bit)" : "Modern (64-bit)",
230+
fileSize = info.FileSize,
231+
metadataSize = info.MetadataSize,
232+
dataOffset = info.DataOffset,
233+
endianness = info.Endianness == 0 ? "Little Endian" : "Big Endian"
234+
};
235+
236+
var json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true });
237+
Console.WriteLine(json);
238+
}
172239
}
173240

0 commit comments

Comments
 (0)