Skip to content

Commit 2b4e809

Browse files
committed
Fix manifest implementation
- Corrected the handling of section lengths and element sizes in the manifest. - Fixed decoding of FrameContents, Metadata, and Frames to use actual data sizes. - Updated compatibility tests to reflect the fixed implementation.
1 parent 60aa8de commit 2b4e809

File tree

3 files changed

+72
-74
lines changed

3 files changed

+72
-74
lines changed

pkg/manifest/compatibility_test.go

Lines changed: 33 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -99,22 +99,17 @@ func TestCarnationFrameFieldOrder(t *testing.T) {
9999
}
100100

101101
// TestMetadataSizeDiscrepancy tests the difference in FileMetadata/SomeStructure size.
102-
// CRITICAL BUG FOUND:
103-
// evrFileTools: FileMetadata is 40 bytes (5 * int64)
104-
// NRadEngine: ManifestSomeStructure is 32 bytes (4 * int64)
105-
// Actual files: ElementSize = 32 bytes
106-
// carnation: some_structure1 is 44 bytes (8+8+8+8+4+4) - also wrong
102+
// FIXED: Now uses correct 32-byte size matching NRadEngine's ManifestSomeStructure
107103
func TestMetadataSizeDiscrepancy(t *testing.T) {
108-
// evrFileTools FileMetadata (INCORRECT):
104+
// evrFileTools FileMetadata (CORRECTED):
109105
// TypeSymbol int64 (8)
110106
// FileSymbol int64 (8)
111107
// Unk1 int64 (8)
112108
// Unk2 int64 (8)
113-
// AssetType int64 (8) <- THIS FIELD DOESN'T EXIST
114-
// Total: 40 bytes
109+
// Total: 32 bytes
115110

116-
if FileMetadataSize != 40 {
117-
t.Errorf("FileMetadataSize: got %d, want 40", FileMetadataSize)
111+
if FileMetadataSize != 32 {
112+
t.Errorf("FileMetadataSize: got %d, want 32", FileMetadataSize)
118113
}
119114

120115
// NRadEngine/Actual format (32 bytes):
@@ -124,10 +119,7 @@ func TestMetadataSizeDiscrepancy(t *testing.T) {
124119
// unk2 int64 (8)
125120
// Total: 32 bytes
126121

127-
// This is a KNOWN BUG that causes incorrect Frame section parsing!
128-
t.Log("BUG: evrFileTools uses 40-byte FileMetadata, actual format is 32 bytes")
129-
t.Log("This causes Frames section offset to be calculated incorrectly")
130-
t.Log("See IMPLEMENTATION_ANALYSIS.md for fix recommendations")
122+
t.Log("FileMetadata size now correctly matches NRadEngine (32 bytes)")
131123
}
132124

133125
// TestSectionPadding verifies Section structure and padding.
@@ -150,7 +142,7 @@ func TestSectionPadding(t *testing.T) {
150142
}
151143

152144
// TestRealManifestParsing tests parsing of actual manifest files.
153-
// NOTE: This test may fail due to known bugs in section offset calculation.
145+
// Now that the implementation is fixed, these tests should pass without issues.
154146
func TestRealManifestParsing(t *testing.T) {
155147
// Look for test data files
156148
testDataPaths := []string{
@@ -185,12 +177,12 @@ func TestRealManifestParsing(t *testing.T) {
185177
t.Error("FileCount should not be 0")
186178
}
187179

188-
// Count frames with issues (known bug in large manifests)
180+
// Count frames with issues (should be zero now that implementation is fixed)
189181
var zeroLengthFrames int
190182
var badRatioFrames int
191183
var badPackageIndex int
192184

193-
for i, frame := range m.Frames {
185+
for _, frame := range m.Frames {
194186
if frame.Length == 0 && frame.CompressedSize > 0 {
195187
zeroLengthFrames++
196188
}
@@ -202,20 +194,22 @@ func TestRealManifestParsing(t *testing.T) {
202194
if frame.CompressedSize > frame.Length*2 && frame.Length > 0 {
203195
badRatioFrames++
204196
}
205-
_ = i
206197
}
207198

208-
// Report issues but don't fail - these are due to known bugs
209-
if zeroLengthFrames > 0 {
210-
t.Logf("KNOWN BUG: %d frames have compressed data but zero length", zeroLengthFrames)
211-
t.Log("This is caused by incorrect Frames section offset due to FileMetadata size bug")
199+
// These should now be very small with the fixed implementation
200+
// Some manifests may have a small number of frames with zero length
201+
// (possibly sentinel values or truncated packages)
202+
if zeroLengthFrames > len(m.Frames)/100 { // More than 1% is suspicious
203+
t.Errorf("%d frames have compressed data but zero length (>1%% of total)", zeroLengthFrames)
204+
} else if zeroLengthFrames > 0 {
205+
t.Logf("Note: %d frames have compressed data but zero length (may be expected)", zeroLengthFrames)
212206
}
213207

214208
if badPackageIndex > 0 {
215-
t.Logf("KNOWN BUG: %d frames have invalid PackageIndex", badPackageIndex)
209+
t.Errorf("%d frames have invalid PackageIndex", badPackageIndex)
216210
}
217211

218-
// Verify FrameContents reference frames (may fail for large manifests)
212+
// Verify FrameContents reference frames correctly
219213
maxFrameIndex := uint32(len(m.Frames))
220214
var badFrameRefs int
221215
for _, fc := range m.FrameContents {
@@ -225,13 +219,11 @@ func TestRealManifestParsing(t *testing.T) {
225219
}
226220

227221
if badFrameRefs > 0 {
228-
t.Logf("KNOWN BUG: %d FrameContents reference invalid frames", badFrameRefs)
222+
t.Errorf("%d FrameContents reference invalid frames", badFrameRefs)
229223
}
230224

231-
t.Logf("Manifest parsed: %d files in %d packages, %d frames",
225+
t.Logf("Manifest parsed successfully: %d files in %d packages, %d frames",
232226
m.FileCount(), m.PackageCount(), len(m.Frames))
233-
t.Logf("Issues found: zeroLength=%d, badRatio=%d, badPkgIdx=%d, badFrameRefs=%d",
234-
zeroLengthFrames, badRatioFrames, badPackageIndex, badFrameRefs)
235227
}
236228

237229
// TestArchiveHeaderFormat tests the ZSTD archive header format.
@@ -347,6 +339,7 @@ func TestEndToEndExtraction(t *testing.T) {
347339

348340
// TestCorrectSectionOffsetCalculation demonstrates the correct way to calculate
349341
// section offsets using the Length field from section descriptors.
342+
// Now that the implementation is fixed, this test validates correct behavior.
350343
func TestCorrectSectionOffsetCalculation(t *testing.T) {
351344
manifestPath := "../../_data/manifests/2b47aab238f60515"
352345

@@ -402,32 +395,21 @@ func TestCorrectSectionOffsetCalculation(t *testing.T) {
402395
}
403396
}
404397

405-
// Compare with what evrFileTools currently calculates
406-
wrongFcEnd := fcStart + len(m.FrameContents)*FrameContentSize
407-
wrongMdEnd := wrongFcEnd + len(m.Metadata)*FileMetadataSize
408-
wrongFrStart := wrongMdEnd
409-
410-
t.Logf("\nComparison (current evrFileTools vs Length-based):")
411-
t.Logf(" FrameContents end: %d vs %d (diff=%d)", wrongFcEnd, fcEnd, wrongFcEnd-fcEnd)
412-
t.Logf(" Metadata end: %d vs %d (diff=%d)", wrongMdEnd, mdEnd, wrongMdEnd-mdEnd)
413-
t.Logf(" Frames start: %d vs %d (diff=%d)", wrongFrStart, frStart, wrongFrStart-frStart)
414-
415-
// Check for discrepancy
416-
if wrongFrStart != frStart {
417-
t.Logf("BUG CONFIRMED: Frames section offset differs by %d bytes", wrongFrStart-frStart)
418-
} else {
419-
t.Log("For this manifest, offsets happen to match (Length == ElementSize * Count)")
420-
t.Log("The bug will manifest in manifests where Metadata.Length != count * 40")
398+
// Verify the implementation now uses Length-based offsets
399+
// The parsed frames should match what we read manually
400+
if len(m.Frames) > 0 {
401+
manualPkgIdx := binary.LittleEndian.Uint32(data[frStart:])
402+
if m.Frames[0].PackageIndex != manualPkgIdx {
403+
t.Errorf("Parsed frame PackageIndex %d doesn't match manual read %d",
404+
m.Frames[0].PackageIndex, manualPkgIdx)
405+
} else {
406+
t.Log("Parsed frame matches manual read - implementation is correct!")
407+
}
421408
}
422409

423-
// Additional check: verify ElementSize reported by manifest vs hardcoded
424-
t.Logf("\nElementSize comparison (manifest vs hardcoded):")
410+
// Verify ElementSize matches the new constant
411+
t.Logf("\nElementSize comparison (manifest vs constant):")
425412
t.Logf(" FrameContents: %d vs %d", m.Header.FrameContents.ElementSize, FrameContentSize)
426413
t.Logf(" Metadata: %d vs %d", m.Header.Metadata.ElementSize, FileMetadataSize)
427414
t.Logf(" Frames: %d vs %d", m.Header.Frames.ElementSize, FrameSize)
428-
429-
if m.Header.Metadata.ElementSize != FileMetadataSize {
430-
t.Logf("WARNING: Metadata.ElementSize=%d but FileMetadataSize=%d",
431-
m.Header.Metadata.ElementSize, FileMetadataSize)
432-
}
433415
}

pkg/manifest/manifest.go

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import (
1111

1212
// Binary sizes for manifest structures
1313
const (
14-
HeaderSize = 192 // Fixed header size:
14+
HeaderSize = 192 // Fixed header size:
1515
// 4 (PackageCount) + 4 (Unk1) + 8 (Unk2)
1616
// + SectionSize (FrameContents) + 16 bytes padding
1717
// + SectionSize (Metadata) + 16 bytes padding
1818
// + SectionSize (Frames)
19-
SectionSize = 48 // 6 * 8 bytes (Section has 6 uint64 fields)
20-
FrameContentSize = 32 // 8 + 8 + 4 + 4 + 4 + 4 bytes
21-
FileMetadataSize = 40 // 5 * 8 bytes
22-
FrameSize = 16 // 4 * 4 bytes
19+
SectionSize = 48 // 6 * 8 bytes (Section has 6 uint64 fields)
20+
FrameContentSize = 32 // 8 + 8 + 4 + 4 + 4 + 4 bytes
21+
FileMetadataSize = 32 // 4 * 8 bytes (matches NRadEngine ManifestSomeStructure)
22+
FrameSize = 16 // 4 * 4 bytes
2323
)
2424

2525
// Manifest represents a parsed EVR manifest file.
@@ -63,12 +63,12 @@ type FrameContent struct {
6363
}
6464

6565
// FileMetadata contains additional file metadata.
66+
// Matches NRadEngine::ManifestSomeStructure (32 bytes)
6667
type FileMetadata struct {
6768
TypeSymbol int64 // File type identifier
6869
FileSymbol int64 // File identifier
6970
Unk1 int64 // Unknown - game launches with 0
7071
Unk2 int64 // Unknown - game launches with 0
71-
AssetType int64 // Asset type identifier
7272
}
7373

7474
// Frame describes a compressed data frame within a package.
@@ -117,40 +117,57 @@ func (m *Manifest) UnmarshalBinary(data []byte) error {
117117
decodeSection(&m.Header.Frames, data[offset:])
118118
offset += SectionSize
119119

120+
// Use section Length fields for correct positioning
121+
// This handles cases where ElementSize * Count != Length (padding/alignment)
122+
// Note: For Frames, ElementSize=32 includes padding but actual data is 16 bytes
123+
// We use actual data sizes for reading, but Length for section positioning
124+
125+
// Calculate actual strides - use ElementSize but bound by actual section length
126+
// to handle manifests where Length < ElementCount * ElementSize
127+
fcCount := int(m.Header.FrameContents.ElementCount)
128+
mdCount := int(m.Header.Metadata.ElementCount)
129+
frCount := int(m.Header.Frames.ElementCount)
130+
131+
fcStride := FrameContentSize // 32 bytes - actual data size
132+
mdStride := FileMetadataSize // 32 bytes - actual data size
133+
frStride := FrameSize // 16 bytes - actual data size
134+
120135
// Decode FrameContents
121-
count := int(m.Header.FrameContents.ElementCount)
122-
m.FrameContents = make([]FrameContent, count)
123-
for i := 0; i < count; i++ {
136+
m.FrameContents = make([]FrameContent, fcCount)
137+
for i := 0; i < fcCount; i++ {
124138
m.FrameContents[i].TypeSymbol = int64(binary.LittleEndian.Uint64(data[offset:]))
125139
m.FrameContents[i].FileSymbol = int64(binary.LittleEndian.Uint64(data[offset+8:]))
126140
m.FrameContents[i].FrameIndex = binary.LittleEndian.Uint32(data[offset+16:])
127141
m.FrameContents[i].DataOffset = binary.LittleEndian.Uint32(data[offset+20:])
128142
m.FrameContents[i].Size = binary.LittleEndian.Uint32(data[offset+24:])
129143
m.FrameContents[i].Alignment = binary.LittleEndian.Uint32(data[offset+28:])
130-
offset += FrameContentSize
144+
offset += fcStride
131145
}
132146

147+
// Advance to Metadata section using Length field
148+
offset = HeaderSize + int(m.Header.FrameContents.Length)
149+
133150
// Decode Metadata
134-
count = int(m.Header.Metadata.ElementCount)
135-
m.Metadata = make([]FileMetadata, count)
136-
for i := 0; i < count; i++ {
151+
m.Metadata = make([]FileMetadata, mdCount)
152+
for i := 0; i < mdCount; i++ {
137153
m.Metadata[i].TypeSymbol = int64(binary.LittleEndian.Uint64(data[offset:]))
138154
m.Metadata[i].FileSymbol = int64(binary.LittleEndian.Uint64(data[offset+8:]))
139155
m.Metadata[i].Unk1 = int64(binary.LittleEndian.Uint64(data[offset+16:]))
140156
m.Metadata[i].Unk2 = int64(binary.LittleEndian.Uint64(data[offset+24:]))
141-
m.Metadata[i].AssetType = int64(binary.LittleEndian.Uint64(data[offset+32:]))
142-
offset += FileMetadataSize
157+
offset += mdStride
143158
}
144159

145-
// Decode Frames
146-
count = int(m.Header.Frames.ElementCount)
147-
m.Frames = make([]Frame, count)
148-
for i := 0; i < count; i++ {
160+
// Advance to Frames section using Length field
161+
offset = HeaderSize + int(m.Header.FrameContents.Length) + int(m.Header.Metadata.Length)
162+
163+
// Decode Frames - actual frame data is 16 bytes, even though ElementSize may report 32
164+
m.Frames = make([]Frame, frCount)
165+
for i := 0; i < frCount; i++ {
149166
m.Frames[i].PackageIndex = binary.LittleEndian.Uint32(data[offset:])
150167
m.Frames[i].Offset = binary.LittleEndian.Uint32(data[offset+4:])
151168
m.Frames[i].CompressedSize = binary.LittleEndian.Uint32(data[offset+8:])
152169
m.Frames[i].Length = binary.LittleEndian.Uint32(data[offset+12:])
153-
offset += FrameSize
170+
offset += frStride
154171
}
155172

156173
return nil
@@ -223,7 +240,6 @@ func (m *Manifest) EncodeTo(buf []byte) {
223240
binary.LittleEndian.PutUint64(buf[offset+8:], uint64(m.Metadata[i].FileSymbol))
224241
binary.LittleEndian.PutUint64(buf[offset+16:], uint64(m.Metadata[i].Unk1))
225242
binary.LittleEndian.PutUint64(buf[offset+24:], uint64(m.Metadata[i].Unk2))
226-
binary.LittleEndian.PutUint64(buf[offset+32:], uint64(m.Metadata[i].AssetType))
227243
offset += FileMetadataSize
228244
}
229245

pkg/manifest/manifest_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ func TestManifest(t *testing.T) {
1616
ElementCount: 2,
1717
},
1818
Metadata: Section{
19-
Length: 80,
20-
ElementSize: 40,
19+
Length: 64, // 2 * 32 bytes (fixed size)
20+
ElementSize: 32, // Correct 32-byte size
2121
Count: 2,
2222
ElementCount: 2,
2323
},

0 commit comments

Comments
 (0)