Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Documentation/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,11 @@ is only needed when scripts access the data on the CPU. `view_potential_duplicat
objects across the whole build (this is why the full analysis matters) and is only expected to have
results for AssetBundle builds: rows that span archives usually mean a shared dependency was not
assigned to a common AssetBundle, so it was duplicated into each AssetBundle that needs it. To see
a suspicious object in full, dump it from the file the query reported:
a suspicious object in full, dump it from the file the query reported (the `archive`,
`serialized_file` and `object_id` columns of `object_view`):

```
UnityDataTool dump /path/to/build/some.bundle -i <object_id> --stdout
UnityDataTool dump /path/to/build/some.bundle -e <serialized_file> -i <object_id> --stdout
```

## Worked example: why is this one AssetBundle so large?
Expand Down
10 changes: 4 additions & 6 deletions Documentation/assetbundle-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,12 @@ built-in shaders it needs and does not depend on the Player's Always Included Sh

The [`archive`](command-archive.md) command lists or extracts the files inside a bundle, and
[`dump`](command-dump.md) / [`serialized-file`](command-serialized-file.md) inspect the
SerializedFiles. A typical workflow is to extract the bundle into a folder and then dump specific
objects:
SerializedFiles directly inside the bundle, without extracting it:

```
UnityDataTool archive extract mybundle.bundle -o extracted
cd extracted
UnityDataTool sf objectlist CAB-<hash>
UnityDataTool dump --stdout CAB-<hash> --type AssetBundle
UnityDataTool archive list mybundle.bundle
UnityDataTool sf objectlist mybundle.bundle -e CAB-<hash>
UnityDataTool dump --stdout mybundle.bundle -e CAB-<hash> --type AssetBundle
```

## The AssetBundle object
Expand Down
13 changes: 11 additions & 2 deletions Documentation/command-dump.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ UnityDataTool dump <path> [options]

| Option | Description | Default |
|--------|-------------|---------|
| `<path>` | Path to file to dump | *(required)* |
| `<path>` | Path to the SerializedFile, or to an archive that contains SerializedFiles | *(required)* |
| `-e, --entry <name>` | Only dump this SerializedFile from inside the archive (see [Archive Support](#archive-support)) | All SerializedFiles |
| `-o, --output-path <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 <format>` | Output format | `text` |
Expand Down Expand Up @@ -84,7 +85,7 @@ 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.
- For Unity archives that contain more than one SerializedFile, choose the one to dump with `--entry`. Without it `--stdout` is refused, because there is no unambiguous way to deliver multiple files on a single stream.

---

Expand Down Expand Up @@ -114,6 +115,14 @@ BuildPlayer-Scene2.sharedAssets.txt
BuildPlayer-Scene2.txt
```

To dump only one of the SerializedFiles, pass its name with `-e` / `--entry`. Nothing is extracted to disk. The names are listed by [`archive list`](command-archive.md), and they are also the `serialized_file` column of `object_view` in a database built by `analyze`:

```bash
UnityDataTool dump scenes.bundle -e BuildPlayer-Scene2.sharedAssets --stdout
```

When the archive contains only one SerializedFile (the usual case for an AssetBundle that does not contain scenes), `--stdout` uses it without `--entry`. When there are several, the error lists their names.

---

## TypeTree Requirement
Expand Down
32 changes: 20 additions & 12 deletions Documentation/command-serialized-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ UnityDataTool sf externalrefs <filename> [options]

| Option | Description | Default |
|--------|-------------|---------|
| `<filename>` | Path to the SerializedFile | *(required)* |
| `<filename>` | Path to the SerializedFile, or to an archive that contains it | *(required)* |
| `-e, --entry <name>` | Name of the SerializedFile inside the archive (see [SerializedFiles inside an archive](#serializedfiles-inside-an-archive)) | — |
| `-f, --format <format>` | Output format: `Text` or `Json` | `Text` |

### Example - Text Output
Expand Down Expand Up @@ -93,7 +94,8 @@ UnityDataTool sf objectlist <filename> [options]

| Option | Description | Default |
|--------|-------------|---------|
| `<filename>` | Path to the SerializedFile | *(required)* |
| `<filename>` | Path to the SerializedFile, or to an archive that contains it | *(required)* |
| `-e, --entry <name>` | Name of the SerializedFile inside the archive (see [SerializedFiles inside an archive](#serializedfiles-inside-an-archive)) | — |
| `-f, --format <format>` | Output format: `Text` or `Json` | `Text` |

### Example - Text Output
Expand Down Expand Up @@ -155,7 +157,8 @@ UnityDataTool sf header <filename> [options]

| Option | Description | Default |
|--------|-------------|---------|
| `<filename>` | Path to the SerializedFile | *(required)* |
| `<filename>` | Path to the SerializedFile, or to an archive that contains it | *(required)* |
| `-e, --entry <name>` | Name of the SerializedFile inside the archive (see [SerializedFiles inside an archive](#serializedfiles-inside-an-archive)) | — |
| `-f, --format <format>` | Output format: `Text` or `Json` | `Text` |

### Example - Text Output
Expand Down Expand Up @@ -223,7 +226,8 @@ UnityDataTool sf metadata <filename> [options]

| Option | Description | Default |
|--------|-------------|---------|
| `<filename>` | Path to the SerializedFile | *(required)* |
| `<filename>` | Path to the SerializedFile, or to an archive that contains it | *(required)* |
| `-e, --entry <name>` | Name of the SerializedFile inside the archive (see [SerializedFiles inside an archive](#serializedfiles-inside-an-archive)) | — |
| `-f, --format <format>` | Output format: `Text` or `Json` | `Text` |

### Example - Text Output
Expand Down Expand Up @@ -363,22 +367,26 @@ UnityDataTool sf objectlist sharedassets0.assets -f json | jq '.[] | select(.typ

---

## SerializedFile vs Archive
## SerializedFiles inside an archive

When working with AssetBundles (or a compressed Player build) you need to extract the contents first (with `archive extract`), then run the `serialized-file` command on individual files in the extracted output.
AssetBundles, compressed Player builds and Content Directory builds store their SerializedFiles inside a Unity Archive. Every subcommand can read a SerializedFile directly from the archive, without extracting it: pass the archive path and name the SerializedFile with `-e` / `--entry`.

**Example workflow:**
```bash
# 1. List contents of an archive
# 1. List the contents of the archive
UnityDataTool archive list scenes.bundle

# 2. Extract the archive
UnityDataTool archive extract scenes.bundle -o extracted/
# 2. Inspect one of its SerializedFiles
UnityDataTool sf objectlist scenes.bundle -e BuildPlayer-SampleScene.sharedAssets
```

When the archive contains only one SerializedFile (the usual case for an AssetBundle that does not contain scenes), `--entry` can be left out:

# 3. Inspect individual SerializedFiles
UnityDataTool sf objectlist extracted/CAB-5d40f7cad7c871cf2ad2af19ac542994
```bash
UnityDataTool sf externalrefs mybundle.bundle
```

When there are several, the error lists their names. The names are also the `serialized_file` column of `object_view` in a database built by `analyze`.

---

## Notes
Expand Down
116 changes: 69 additions & 47 deletions SerializedFile/SerializedFileTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public enum OutputFormat
Json
}

public static int ListExternalRefs(FileInfo filename, OutputFormat format)
public static int ListExternalRefs(FileInfo filename, string entry, OutputFormat format)
{
// External references are read directly from the parsed metadata rather than via UnityFileSystemApi.
//
Expand All @@ -26,19 +26,20 @@ public static int ListExternalRefs(FileInfo filename, OutputFormat format)
//
// These trade-offs are minor compared to the benefit of handling the common no-TypeTree case,
// so there is no need to keep the UnityFileSystemApi code path.
if (!ValidateSerializedFile(filename.FullName, out var fileInfo))
using var file = OpenSerializedFile(filename.FullName, entry);
if (file == null)
return 1;

if (!SerializedFileDetector.TryParseMetadata(filename.FullName, fileInfo, out var metadata, out var errorMessage))
if (!SerializedFileDetector.TryParseMetadata(file.Stream, file.Info, out var metadata, out var errorMessage))
{
Console.Error.WriteLine($"Error: Failed to parse external references for: {filename.FullName}");
Console.Error.WriteLine($"Error: Failed to parse external references for: {file.DisplayName}");
Console.Error.WriteLine(errorMessage);
return 1;
}

if (metadata.ExternalReferences == null)
{
Console.Error.WriteLine($"Error: External references could not be parsed for: {filename.FullName}");
Console.Error.WriteLine($"Error: External references could not be parsed for: {file.DisplayName}");
return 1;
}

Expand All @@ -50,23 +51,24 @@ public static int ListExternalRefs(FileInfo filename, OutputFormat format)
return 0;
}

public static int ListObjects(FileInfo filename, OutputFormat format)
public static int ListObjects(FileInfo filename, string entry, OutputFormat format)
{
// The object list is read directly from the parsed metadata rather than via UnityFileSystemApi.
// (See comment in ListExternalRefs() for the reasons for doing it that way)
if (!ValidateSerializedFile(filename.FullName, out var fileInfo))
using var file = OpenSerializedFile(filename.FullName, entry);
if (file == null)
return 1;

if (!SerializedFileDetector.TryParseMetadata(filename.FullName, fileInfo, out var metadata, out var errorMessage))
if (!SerializedFileDetector.TryParseMetadata(file.Stream, file.Info, out var metadata, out var errorMessage))
{
Console.Error.WriteLine($"Error: Failed to parse object list for: {filename.FullName}");
Console.Error.WriteLine($"Error: Failed to parse object list for: {file.DisplayName}");
Console.Error.WriteLine(errorMessage);
return 1;
}

if (metadata.ObjectList == null)
{
Console.Error.WriteLine($"Error: Object list could not be parsed for: {filename.FullName}");
Console.Error.WriteLine($"Error: Object list could not be parsed for: {file.DisplayName}");
return 1;
}

Expand All @@ -78,27 +80,29 @@ public static int ListObjects(FileInfo filename, OutputFormat format)
return 0;
}

public static int PrintHeader(FileInfo filename, OutputFormat format)
public static int PrintHeader(FileInfo filename, string entry, OutputFormat format)
{
if (!ValidateSerializedFile(filename.FullName, out var fileInfo))
using var file = OpenSerializedFile(filename.FullName, entry);
if (file == null)
return 1;

if (format == OutputFormat.Json)
OutputHeaderJson(fileInfo);
OutputHeaderJson(file.Info);
else
OutputHeaderText(fileInfo);
OutputHeaderText(file.Info);

return 0;
}

public static int PrintMetadata(FileInfo filename, OutputFormat format)
public static int PrintMetadata(FileInfo filename, string entry, OutputFormat format)
{
if (!ValidateSerializedFile(filename.FullName, out var fileInfo))
using var file = OpenSerializedFile(filename.FullName, entry);
if (file == null)
return 1;

if (!SerializedFileDetector.TryParseMetadata(filename.FullName, fileInfo, out var metadata, out var errorMessage))
if (!SerializedFileDetector.TryParseMetadata(file.Stream, file.Info, out var metadata, out var errorMessage))
{
Console.Error.WriteLine($"Error: Failed to parse metadata for: {filename.FullName}");
Console.Error.WriteLine($"Error: Failed to parse metadata for: {file.DisplayName}");
Console.Error.WriteLine(errorMessage);
return 1;
}
Expand All @@ -111,52 +115,70 @@ public static int PrintMetadata(FileInfo filename, OutputFormat format)
return 0;
}

/// <summary>
/// Validates that a file is a SerializedFile and provides helpful error messages if not.
/// </summary>
/// <param name="filePath">Path to the file to validate</param>
/// <param name="fileInfo">SerializedFile header information if valid, null otherwise</param>
/// <returns>True if valid SerializedFile, false otherwise</returns>
private static bool ValidateSerializedFile(string filePath, out SerializedFileInfo fileInfo)
// A validated SerializedFile open for reading: a file on disk, or an entry of a mounted archive.
private sealed class OpenedSerializedFile : IDisposable
{
fileInfo = null;
public Stream Stream { get; init; }
public SerializedFileInfo Info { get; init; }
public string DisplayName { get; init; }
public MountedSerializedFile MountedFile { get; init; }

public void Dispose()
{
// The stream reads through the mount, so close it before unmounting.
Stream.Dispose();
MountedFile?.Dispose();
}
}

// Opens the file as a SerializedFile, or the SerializedFile chosen by entry when the file is an
// archive. Prints a helpful error and returns null when that is not possible, except that a failed
// selection inside the archive throws SerializedFileSelectionException.
private static OpenedSerializedFile OpenSerializedFile(string filePath, string entry)
{
if (!File.Exists(filePath))
{
Console.Error.WriteLine($"Error: File not found: {filePath}");
return false;
return null;
}

Stream stream;
string displayName;
MountedSerializedFile mountedFile = null;

if (ArchiveDetector.IsUnityArchive(filePath))
{
Console.Error.WriteLine($"Error: The file is an AssetBundle or other Unity Archive, not a SerializedFile.");
Console.Error.WriteLine($"File: {filePath}");
Console.Error.WriteLine();
Console.Error.WriteLine("Unity Archives contain SerializedFiles inside them.");
Console.Error.WriteLine("To access the SerializedFiles, first extract the archive using:");
Console.Error.WriteLine($" UnityDataTool archive extract \"{filePath}\" -o <output-directory>");
Console.Error.WriteLine();
Console.Error.WriteLine("Then you can run serialized-file commands on the extracted files.");
return false;
}
mountedFile = MountedSerializedFile.Open(filePath, entry);

if (YamlSerializedFileDetector.IsYamlSerializedFile(filePath))
// The metadata parser reads one small value at a time, and every unbuffered read is a native call.
stream = new BufferedStream(new UnityFileStream(mountedFile.MountedPath), 64 * 1024);
displayName = $"{mountedFile.PathInArchive} in {filePath}";
}
else
{
Console.Error.WriteLine($"Error: The file is a YAML-format SerializedFile, which is not supported.");
Console.Error.WriteLine($"File: {filePath}");
Console.Error.WriteLine();
Console.Error.WriteLine("UnityDataTool only supports binary-format SerializedFiles.");
return false;
if (YamlSerializedFileDetector.IsYamlSerializedFile(filePath))
{
Console.Error.WriteLine($"Error: The file is a YAML-format SerializedFile, which is not supported.");
Console.Error.WriteLine($"File: {filePath}");
Console.Error.WriteLine();
Console.Error.WriteLine("UnityDataTool only supports binary-format SerializedFiles.");
return null;
}

stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
displayName = filePath;
}

if (!SerializedFileDetector.TryDetectSerializedFile(filePath, out fileInfo))
if (!SerializedFileDetector.TryDetectSerializedFile(stream, out var info))
{
Console.Error.WriteLine($"Error: The file does not appear to be a valid Unity SerializedFile.");
Console.Error.WriteLine($"File: {filePath}");
return false;
Console.Error.WriteLine($"File: {displayName}");
stream.Dispose();
mountedFile?.Dispose();
return null;
}

return true;
return new OpenedSerializedFile { Stream = stream, Info = info, DisplayName = displayName, MountedFile = mountedFile };
}

private static void OutputExternalRefsText(ExternalReference[] refs)
Expand Down
Loading
Loading