Skip to content

Commit 4381a38

Browse files
[#130] Fix a field-less compound's size, and correct where the frame sits
RandomAccessReader.GetSize asked for the last child of a node with no children, which threw for a [Serializable] class with no serialized fields. Such a node occupies whatever its own size says, normally nothing. Exposed by the new managedreferences fixture, whose ShapeNoData field sits between the references and a plain int that has to read correctly after it. The comments describing the registry frame said it precedes the first field that can hold a reference. It does not: it leads the declaring class's own data, so the flag rides that class's first field whether or not that field is a reference. In the fixture it is a plain int. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0ed864e commit 4381a38

7 files changed

Lines changed: 58 additions & 17 deletions

File tree

‎Analyzer/PPtrAndCrcProcessor.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ public uint Process(long objectId, long offset, long size, TypeTreeNode node)
9999

100100
foreach (var child in node.Children)
101101
{
102-
// From SerializedFile version 25 the registry is a frame in the data ahead of the
103-
// marked field, which no node describes. Only a root object's fields carry one. The
104-
// frame is not part of that field, so its references get their own path root, named
105-
// after the registry as the node-described versions are.
102+
// From SerializedFile version 25 the registry is a frame leading the C# class's data,
103+
// which no node describes; the flag marks the field it precedes. Only a root object's
104+
// fields carry one. The frame is not part of that field, so its references get their own
105+
// path root, named after the registry as the node-described versions are.
106106
if (child.HasSerializedRefs)
107107
{
108108
m_StringBuilder.Clear();

‎Documentation/unity-content-format.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ metadata` reports the table as `Shared Subtrees`.
9696

9797
**The `[SerializeReference]` registry.** Up to Unity 6.6 the registry was described by TypeTree
9898
nodes like any other field, and appeared at the end of the object. From 6.7 it is a self-contained
99-
frame in the object's data, ahead of the first field that can hold a reference, and no TypeTree node
100-
describes it. `dump` prints its contents in the same shape either way, so the visible difference is
99+
frame that leads the C# class's own data - after the built-in fields such as `m_GameObject` and
100+
`m_Name`, and before the first field declared by the script - and no TypeTree node describes it.
101+
Putting the registry first is what lets a reader assign each reference as it reads the field,
102+
instead of patching it afterwards. `dump` prints its contents in the same shape either way, so the visible difference is
101103
the reported registry `version` (2 before, 3 from 6.7) and where it appears in the output. The new
102104
format can also express a null reference, which the older ones could not.
103105

‎TextDumper/TextDumperTool.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,9 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex
340340
{
341341
foreach (var child in node.Children)
342342
{
343-
// From SerializedFile version 25 the registry is a frame in the data ahead of the
344-
// marked field, described by no node. Only a root object's data carries one; the
345-
// same type tree read as a registry blob is frameless.
343+
// From SerializedFile version 25 the registry is a frame leading the C# class's
344+
// data, described by no node; the flag marks the field it precedes. Only a root
345+
// object's data carries one; the same tree read as a registry blob is frameless.
346346
if (isRootObject && child.HasSerializedRefs)
347347
DumpManagedReferenceFrame(ref offset, level + 1);
348348

‎UnityFileSystem.Tests/SerializedFileV26Tests.cs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,38 @@ public void Registry_Version22_ReadsNodeEntriesAsTheSameShape()
164164
AssertRegistryContents(m_V22Folder, 2);
165165
}
166166

167+
// ManagedReferenceTestBehaviour carries a plain int either side of its [SerializeReference]
168+
// fields, and a [Serializable] class with no fields at all. Reading the fields after those is
169+
// what catches a reader that mis-sizes either: it reads them shifted rather than failing.
170+
[Test]
171+
public void Registry_Version26_ReadsFieldsAroundTheFrameAndAFieldLessCompound()
172+
{
173+
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data",
174+
"AssetBundleTypeTreeVariations", "v26", "managedreferences.bundle");
175+
176+
using var archive = UnityFileSystem.MountArchive(path, "archive:/");
177+
var cab = "archive:/" + archive.Nodes.First(n => n.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)).Path;
178+
179+
using var sf = UnityFileSystem.OpenSerializedFile(cab);
180+
using var fileReader = new UnityFileReader(cab, 1024 * 1024);
181+
182+
var obj = sf.Objects.First(o => o.TypeId == MonoBehaviourClassId && o.Size > 500);
183+
var reader = new RandomAccessReader(sf, sf.GetTypeTreeRoot(obj.Id), fileReader, obj.Offset, objectSize: obj.Size);
184+
185+
// "before" is the field the frame precedes, so it is read past the whole frame.
186+
Assert.That(reader["before"].GetValue<int>(), Is.EqualTo(11));
187+
188+
// "after" follows both the reference fields and the field-less compound.
189+
Assert.That(reader["noData"].Size, Is.EqualTo(0));
190+
Assert.That(reader["after"].GetValue<int>(), Is.EqualTo(22));
191+
192+
var registry = reader.Registry;
193+
Assert.That(registry.Version, Is.EqualTo(ManagedReferenceRegistry.FrameVersion));
194+
Assert.That(registry.Entries.Any(e => e.IsNull), Is.True, "Expected a null reference entry");
195+
Assert.That(registry.Entries.Count(e => e.ClassName.EndsWith("Shape")), Is.GreaterThan(1),
196+
"Expected several instances of one type, sharing a Types table entry");
197+
}
198+
167199
[Test]
168200
public void Registry_ObjectWithoutReferences_IsNull()
169201
{

‎UnityFileSystem/ManagedReferenceRegistry.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ public sealed class ManagedReferenceEntry
2525

2626
// The [SerializeReference] instances owned by one serialized object.
2727
//
28-
// From SerializedFile version 25 the registry is a self-delimiting frame in the object's data,
29-
// sitting immediately before the data of the field flagged HasSerializedRefs. It is the first piece
30-
// of object layout no TypeTree node describes, so the tables inside it cannot be walked the way
31-
// every other field is.
28+
// From SerializedFile version 25 the registry is a self-delimiting frame leading the C# class's own
29+
// data - after the built-in fields, before the first field the script declares, which is the one
30+
// flagged HasSerializedRefs. It is the first piece of object layout no TypeTree node describes, so
31+
// the tables inside it cannot be walked the way every other field is.
3232
//
3333
// They are not parsed here either: UFS_GetRegistryFrame* wraps the engine's own frame parser, so
3434
// this class calls into the one implementation that exists rather than becoming a second one. Only

‎UnityFileSystem/TypeTreeNode.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ public class TypeTreeNode
4949
// True if the field is a ManagedReferenceRegistry
5050
public bool IsManagedReferenceRegistry => ((int)Flags & (int)TypeTreeFlags.IsManagedReferenceRegistry) != 0;
5151

52-
// True if the [SerializeReference] registry frame precedes this field's data. Only set on a
53-
// root object's fields; the same type tree read as a registry blob carries no frame.
52+
// True if the [SerializeReference] registry frame precedes this field's data. The flag rides
53+
// the first field of the declaring class, whether or not that field is itself a reference. Only
54+
// set on a root object's fields; the same tree read as a registry blob carries no frame.
5455
public bool HasSerializedRefs => ((int)Flags & (int)TypeTreeFlags.HasSerializedRefs) != 0;
5556

5657
// True if the node stands in for a compound the file stores once and shares between types

‎UnityFileSystem/TypeTreeReaders/RandomAccessReader.cs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,12 @@ int GetSize()
297297
{
298298
size = m_Reader.ReadInt32(Offset) + 4;
299299
}
300+
else if (m_TypeTreeNode.Children.Count == 0)
301+
{
302+
// A compound with no fields, which a [Serializable] class with nothing serialized
303+
// reaches. It occupies whatever its own node says, normally nothing.
304+
size = Math.Max(m_TypeTreeNode.Size, 0);
305+
}
300306
else
301307
{
302308
var lastChild = GetChild(m_TypeTreeNode.Children.Last().Name);
@@ -345,8 +351,8 @@ RandomAccessReader GetChild(string name)
345351
{
346352
var child = m_TypeTreeNode.Children[i];
347353

348-
// From SerializedFile version 25 the registry is a frame in the data ahead of the marked
349-
// field, described by no node, so the field's data starts past it. Only its size is
354+
// From SerializedFile version 25 the registry is a frame leading the C# class's data,
355+
// described by no node, so the marked field's data starts past it. Only its size is
350356
// needed to get there; Registry reads the tables if anyone asks for them.
351357
if (m_IsRoot && child.HasSerializedRefs)
352358
{

0 commit comments

Comments
 (0)