Skip to content

Commit 8c6a130

Browse files
[#99] Resolve ContentDirectory refs when content files lack the .cf extension
ContentLayout reference resolution keyed content files as <hash>.cf, but the analyzed file was keyed by its actual on-disk name. Builds whose archive entries omit the extension (just <hash>) never matched, so references fell through to the .cfid placeholders in the external reference table, producing phantom .cfid rows in serialized_files (NULL archive) and a flood of dangling_refs. Normalize content-file names to the bare content hash (strip a trailing .cf) at every site where the name becomes a serialized-file id key, so a content file resolves to the same id whether or not the extension is present on disk.
1 parent ae9bc52 commit 8c6a130

4 files changed

Lines changed: 47 additions & 27 deletions

File tree

‎Analyzer/SQLite/Writers/ContentLayoutSQLWriter.cs‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,11 @@ public void WriteContentLayout(string filename, ContentLayout layout)
189189
}
190190

191191
// Fills the shared dependency map that resolves the external references of the analyzed
192-
// .cf files (see ContentFileDependencyMap): for each content file, the dependency indexes
193-
// become the target filenames, in order. Built-in dependencies (no content hash) map to
194-
// null so the reference falls back to the external table path.
192+
// content files (see ContentFileDependencyMap): for each content file, the dependency
193+
// indexes become the target names, in order. Built-in dependencies (no content hash) map to
194+
// null so the reference falls back to the external table path. Keys and resolved names use
195+
// the normalized content-hash name so they match the analyzed files regardless of whether
196+
// those carry the ".cf" extension on disk.
195197
private void PopulateContentFileDependencies(ContentLayout layout)
196198
{
197199
var hashByIndex = (layout.SerializedFiles ?? []).ToDictionary(f => f.Index, f => f.ContentHash);
@@ -203,21 +205,24 @@ private void PopulateContentFileDependencies(ContentLayout layout)
203205

204206
var resolved = (file.SerializedFileDependencies ?? [])
205207
.Select(i => hashByIndex.TryGetValue(i, out var hash) && !string.IsNullOrEmpty(hash)
206-
? hash + ".cf"
208+
? ContentFileDependencyMap.NormalizeFileName(hash)
207209
: null)
208210
.ToArray();
209211

210-
m_ContentFileDependencies.Add(file.ContentHash + ".cf", resolved);
212+
m_ContentFileDependencies.Add(ContentFileDependencyMap.NormalizeFileName(file.ContentHash), resolved);
211213
}
212214
}
213215

214216
// Fills in the serialized_file column, linking each layout entry to its serialized_files
215-
// row. Called after all files are processed. Ids come from the shared IdProvider, so
216-
// entries whose .cf file was analyzed link to the row the analyze pass wrote (loose or
217-
// inside an archive, with its archive column intact). For files never encountered on the
218-
// input (a layout-only analyze, or a subset of a build) a placeholder serialized_files
219-
// row is written - name only, archive NULL, no objects - so the link is always valid,
220-
// mirroring what the dangling-refs finalize does for referenced-but-unanalyzed files.
217+
// row. Called after all files are processed. Ids come from the shared IdProvider (keyed by
218+
// the normalized content hash), so entries whose content file was analyzed link to the row
219+
// the analyze pass wrote (loose or inside an archive, with its archive column intact),
220+
// regardless of whether that file carried the ".cf" extension on disk. For files never
221+
// encountered on the input (a layout-only analyze, or a subset of a build) a placeholder
222+
// serialized_files row is written - name only, archive NULL, no objects - so the link is
223+
// always valid, mirroring what the dangling-refs finalize does for referenced-but-unanalyzed
224+
// files. The placeholder's name uses the canonical "<hash>.cf" form since the file was not
225+
// seen on disk.
221226
public void LinkSerializedFiles()
222227
{
223228
var existingIds = new HashSet<int>();
@@ -246,8 +251,8 @@ public void LinkSerializedFiles()
246251
{
247252
foreach (var file in m_ImportedFiles)
248253
{
249-
var fileName = file.ContentHash + ".cf";
250-
var id = m_SerializedFileIdProvider.GetId(fileName);
254+
var id = m_SerializedFileIdProvider.GetId(
255+
ContentFileDependencyMap.NormalizeFileName(file.ContentHash));
251256

252257
update.Parameters["@id"].Value = id;
253258
update.Parameters["@file_index"].Value = file.Index;
@@ -257,7 +262,7 @@ public void LinkSerializedFiles()
257262
{
258263
addStubRow.SetValue("id", id);
259264
addStubRow.SetValue("archive", null);
260-
addStubRow.SetValue("name", fileName);
265+
addStubRow.SetValue("name", file.ContentHash + ".cf");
261266
addStubRow.ExecuteNonQuery();
262267
}
263268
}

‎Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ public class SerializedFileSQLiteWriter : IDisposable
3131
private bool m_SkipCrc;
3232

3333
// Global id assignment shared across every serialized file in the database.
34-
// m_SerializedFileIdProvider maps a serialized file (by lowercased file name) to its
35-
// serialized_files row id; it is owned by AnalyzerTool because the ContentLayout import
34+
// m_SerializedFileIdProvider maps a serialized file (by file name, matched case-sensitively as
35+
// it appears on disk / in the archive, with any ".cf" content-file extension normalized away)
36+
// to its serialized_files row id; it is owned by AnalyzerTool because the ContentLayout import
3637
// assigns ids through the same provider. m_ObjectIdProvider maps a (serialized file id,
3738
// pathId) pair to its objects row id. See ObjectIdProvider for how cross-file references
3839
// are resolved.
@@ -185,7 +186,8 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con
185186
using var sf = UnityFileSystem.OpenSerializedFile(fullPath);
186187
using var reader = new UnityFileReader(fullPath, 64 * 1024 * 1024);
187188
using var pptrReader = new PPtrAndCrcProcessor(sf, reader, containingFolder, m_SkipCrc, AddReference);
188-
int serializedFileId = m_SerializedFileIdProvider.GetId(Path.GetFileName(fullPath));
189+
int serializedFileId = m_SerializedFileIdProvider.GetId(
190+
ContentFileDependencyMap.NormalizeFileName(Path.GetFileName(fullPath)));
189191
int sceneId = -1;
190192

191193
// Two SerializedFiles with the same name map to the same id (the provider deduplicates by
@@ -278,7 +280,7 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con
278280
// dependency list, matched by position; built-in dependencies (null entries) keep the
279281
// external table path.
280282
var resolvedDependencies = m_ContentFileDependencies.GetDependencies(
281-
Path.GetFileName(fullPath));
283+
ContentFileDependencyMap.NormalizeFileName(Path.GetFileName(fullPath)));
282284

283285
if (resolvedDependencies != null && resolvedDependencies.Length != sf.ExternalReferences.Count)
284286
{
Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23

34
namespace UnityDataTools.Analyzer.Util;
@@ -6,10 +7,10 @@ namespace UnityDataTools.Analyzer.Util;
67
// In ContentDirectory builds the external reference table inside a SerializedFile holds symbolic
78
// .cfid placeholders instead of real filenames; the actual target of a reference is determined
89
// positionally through the layout's dependency list (see Documentation/contentdirectory-format.md).
9-
// This map holds, for each content file (keyed by its "<contenthash>.cf" filename), the resolved
10-
// dependency filenames in external-reference-table order. An entry is null where the
11-
// dependency is a built-in file (which has no content hash); such references fall back to the
12-
// path from the external reference table.
10+
// This map holds, for each content file (keyed by its normalized content-hash name, see
11+
// NormalizeFileName), the resolved dependency names in external-reference-table order. An entry is
12+
// null where the dependency is a built-in file (which has no content hash); such references fall
13+
// back to the path from the external reference table.
1314
public class ContentFileDependencyMap
1415
{
1516
private Dictionary<string, string[]> m_Dependencies = new();
@@ -19,10 +20,21 @@ public void Add(string fileName, string[] resolvedDependencies)
1920
m_Dependencies[fileName] = resolvedDependencies;
2021
}
2122

22-
// Returns the resolved dependency filenames for the given filename, or null if the file is
23-
// not covered by an imported ContentLayout.
23+
// Returns the resolved dependency names for the given (normalized) content-file name, or null
24+
// if the file is not covered by an imported ContentLayout.
2425
public string[] GetDependencies(string fileName)
2526
{
2627
return m_Dependencies.TryGetValue(fileName, out var dependencies) ? dependencies : null;
2728
}
29+
30+
// Content files are named by their content hash and may or may not carry the ".cf" extension:
31+
// loose builds and archived builds vary, and the extension is informational (see
32+
// Documentation/contentdirectory-format.md). Strip it so a content file resolves to the same
33+
// serialized-file id whether or not the extension is present on disk. Non-content files never
34+
// end in ".cf", so this is a no-op for them. The name's case is preserved, matching how
35+
// serialized-file names are keyed case-sensitively elsewhere.
36+
public static string NormalizeFileName(string fileName)
37+
{
38+
return fileName.EndsWith(".cf", StringComparison.OrdinalIgnoreCase) ? fileName[..^3] : fileName;
39+
}
2840
}

‎Documentation/contentdirectory-format.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ A Unity object references data inside a `.resS`/`.resource` file by a content-ad
6565
form `cah:/<hash>`. This, and the way it differs from AssetBundle and Player builds, is covered in
6666
[References to resource files](#references-to-resource-files) below.
6767

68-
The extensions are informational. The loading system identifies content by its hash (through the
69-
`cah:/` scheme), not by file extension, so the extension is not required to resolve content. Extensions
70-
are present both for loose files and for entries packed inside an archive.
68+
The extensions are informational. The build manifest, content layout and loading system as a whole identifies content by its hash, not by file extension, and the extension is not required to resolve content.
69+
70+
Currently extensions are present both for loose files and for entries packed inside an archive, but
71+
it is best to avoid assumption that the extensions are always present.
7172

7273
### The build manifest
7374

0 commit comments

Comments
 (0)