Skip to content

Commit 85f767f

Browse files
slapec93Gergely Békési
andauthored
fix: mantaray marshal and unmarshal logic (#1202)
* fix: mantaray marshal and unmarshal logic * fix: upload spec fails * fix: lint --------- Co-authored-by: Gergely Békési <gergely.bekesi@ethswarm.org>
1 parent e47f996 commit 85f767f

2 files changed

Lines changed: 84 additions & 26 deletions

File tree

src/manifest/manifest.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ export class Fork {
7474
throw Error('Fork#marshal node.selfAddress is not set')
7575
}
7676
const data: Uint8Array[] = []
77-
data.push(new Uint8Array([this.node.determineType()]))
77+
// Re-emit the type byte read from the chunk when the node is untouched, so an
78+
// unmarshal → marshal round-trip is byte-identical. determineType() can only
79+
// recompute it correctly once the node's children are loaded/built in memory.
80+
data.push(new Uint8Array([this.node.type ?? this.node.determineType()]))
7881
data.push(Binary.numberToUint8(this.prefix.length))
7982
data.push(this.prefix)
8083

@@ -126,7 +129,7 @@ export class Fork {
126129
metadata = JSON.parse(DECODER.decode(reader.read(metadataLength)))
127130
}
128131

129-
return new Fork(prefix, new MantarayNode({ selfAddress, metadata, path: prefix }))
132+
return new Fork(prefix, new MantarayNode({ selfAddress, metadata, path: prefix, type }))
130133
}
131134
}
132135

@@ -137,6 +140,7 @@ interface MantarayNodeOptions {
137140
metadata?: Record<string, string> | null
138141
path?: Uint8Array | null
139142
parent?: MantarayNode | null
143+
type?: number | null
140144
}
141145

142146
export class MantarayNode {
@@ -147,6 +151,7 @@ export class MantarayNode {
147151
public path: Uint8Array = new Uint8Array(0)
148152
public forks: Map<number, Fork> = new Map()
149153
public parent: MantarayNode | null = null
154+
public type: number | null = null
150155

151156
constructor(options?: MantarayNodeOptions) {
152157
if (options?.targetAddress) {
@@ -172,6 +177,8 @@ export class MantarayNode {
172177
if (options?.parent) {
173178
this.parent = options.parent
174179
}
180+
181+
this.type = options?.type ?? null
175182
}
176183

177184
get fullPath(): Uint8Array {
@@ -243,14 +250,23 @@ export class MantarayNode {
243250
fork.node.selfAddress = (await fork.node.calculateSelfAddress()).toUint8Array()
244251
}
245252
}
253+
const hasEntry = !Binary.equals(this.targetAddress, NULL_ADDRESS)
254+
let refBytesSize = 0
255+
256+
if (hasEntry) {
257+
refBytesSize = this.targetAddress.length
258+
} else {
259+
for (const fork of this.forks.values()) {
260+
if (fork.node.selfAddress) {
261+
refBytesSize = fork.node.selfAddress.length
262+
break
263+
}
264+
}
265+
}
246266
const header = new Uint8Array(32)
247267
header.set(VERSION_02_HASH, 0)
248-
header.set(
249-
Binary.equals(this.targetAddress, NULL_ADDRESS) && Binary.equals(this.path, new Uint8Array([47]))
250-
? Binary.numberToUint8(0)
251-
: Binary.numberToUint8(this.targetAddress.length),
252-
31,
253-
)
268+
header.set(Binary.numberToUint8(refBytesSize), 31)
269+
const entry = hasEntry ? this.targetAddress : new Uint8Array(refBytesSize)
254270
const forkBitmap = new Uint8Array(32)
255271
for (const fork of this.forks.keys()) {
256272
Binary.setBit(forkBitmap, fork, 1, 'LE')
@@ -261,17 +277,7 @@ export class MantarayNode {
261277
forks.push(this.forks.get(i)!.marshal())
262278
}
263279
}
264-
const data = Binary.xorCypher(
265-
Binary.concatBytes(
266-
header,
267-
Binary.equals(this.targetAddress, NULL_ADDRESS) && Binary.equals(this.path, new Uint8Array([47]))
268-
? new Uint8Array(0)
269-
: this.targetAddress,
270-
forkBitmap,
271-
...forks,
272-
),
273-
this.obfuscationKey,
274-
)
280+
const data = Binary.xorCypher(Binary.concatBytes(header, entry, forkBitmap, ...forks), this.obfuscationKey)
275281

276282
return Binary.concatBytes(this.obfuscationKey, data)
277283
}
@@ -311,11 +317,14 @@ export class MantarayNode {
311317
const targetAddress = targetAddressLength ? reader.read(targetAddressLength) : NULL_ADDRESS
312318
const node = new MantarayNode({ selfAddress, targetAddress, obfuscationKey })
313319
const forkBitmap = reader.read(32)
314-
for (let i = 0; i < 256; i++) {
315-
if (Binary.getBit(forkBitmap, i, 'LE')) {
316-
const newFork = Fork.unmarshal(reader, selfAddress.length)
317-
node.forks.set(i, newFork)
318-
newFork.node.parent = node
320+
321+
if (targetAddressLength > 0) {
322+
for (let i = 0; i < 256; i++) {
323+
if (Binary.getBit(forkBitmap, i, 'LE')) {
324+
const newFork = Fork.unmarshal(reader, selfAddress.length)
325+
node.forks.set(i, newFork)
326+
newFork.node.parent = node
327+
}
319328
}
320329
}
321330

@@ -331,6 +340,7 @@ export class MantarayNode {
331340
metadata?: Record<string, string> | null,
332341
) {
333342
this.selfAddress = null
343+
this.type = null
334344
path = path instanceof Uint8Array ? path : ENCODER.encode(path)
335345
debug('adding fork', { path: DECODER.decode(path), reference: new Reference(reference).represent() })
336346
// TODO: this should not be ignored
@@ -368,11 +378,13 @@ export class MantarayNode {
368378
tip.forks.set(remainingPath[0], fork)
369379
fork.node.parent = tip
370380
tip.selfAddress = null
381+
tip.type = null
371382
tip = newFork.node
372383
} else {
373384
tip.forks.set(remainingPath[0], newFork)
374385
newFork.node.parent = tip
375386
tip.selfAddress = null
387+
tip.type = null
376388
tip = newFork.node
377389
}
378390
}
@@ -383,6 +395,7 @@ export class MantarayNode {
383395
*/
384396
removeFork(path: string | Uint8Array) {
385397
this.selfAddress = null
398+
this.type = null
386399
path = path instanceof Uint8Array ? path : ENCODER.encode(path)
387400

388401
if (path.length === 0) {
@@ -534,15 +547,22 @@ export class MantarayNode {
534547
determineType() {
535548
let type = 0
536549

537-
if (!Binary.equals(this.targetAddress, NULL_ADDRESS) || Binary.equals(this.path, PATH_SEPARATOR)) {
550+
// Mirror Bee (pkg/manifest/mantaray/node.go): Add() marks every explicitly
551+
// added leaf as a value (makeValue), even one with a null entry such as a
552+
// metadata-only "/" node. In final-state terms a leaf (no forks) is always an
553+
// added entry, so it is a value; a node with forks is a value only when it also
554+
// carries an entry. The path-separator flag is set only when a separator occurs
555+
// past the first byte (IndexRune > 0), so a prefix that merely starts with '/'
556+
// does not qualify.
557+
if (!Binary.equals(this.targetAddress, NULL_ADDRESS) || this.forks.size === 0) {
538558
type |= TYPE_VALUE
539559
}
540560

541561
if (this.forks.size > 0) {
542562
type |= TYPE_EDGE
543563
}
544564

545-
if (Binary.indexOf(this.path, PATH_SEPARATOR) !== -1 && !Binary.equals(this.path, PATH_SEPARATOR)) {
565+
if (Binary.indexOf(this.path, PATH_SEPARATOR) > 0) {
546566
type |= TYPE_WITH_PATH_SEPARATOR
547567
}
548568

test/unit/manifest.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Binary } from 'cafe-utility'
12
import { MantarayNode } from '../../src'
23
import { arbitraryReference } from '../utils'
34

@@ -43,6 +44,43 @@ test('MantarayNode basic', () => {
4344
expect(node.collect()).toHaveLength(2)
4445
})
4546

47+
test('directory node keeps its forks across a marshal/unmarshal round-trip', async () => {
48+
const node = new MantarayNode()
49+
node.addFork('/index.html', arbitraryReference())
50+
node.addFork('/about.html', arbitraryReference())
51+
52+
const slash = node.forks.get('/'.charCodeAt(0))!.node
53+
expect(ENCODER.encode('/')).toEqual(slash.path)
54+
expect(slash.forks.size).toBe(2)
55+
56+
const bytes = await slash.marshal()
57+
const restored = MantarayNode.unmarshalFromData(bytes, new Uint8Array(32))
58+
59+
expect(restored.forks.size).toBe(2)
60+
})
61+
62+
test('reads a Bee-produced v0.2 directory node (real reference vector)', async () => {
63+
// Generated by Bee's own mantaray MarshalBinary (bee/pkg/manifest/mantaray) for
64+
// a directory node ("/") with two value forks and a zero obfuscation key.
65+
// Byte 63 (the reference-size byte) is 0x20 = 32: Bee never emits 0 for a
66+
// directory node, which is exactly what marshal() must reproduce.
67+
const vector =
68+
'00000000000000000000000000000000000000000000000000000000000000005768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202000000000000000000000000000000000000020a61626f75742e68746d6c0000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa020a696e6465782e68746d6c0000000000000000000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
69+
const data = Binary.hexToUint8Array(vector)
70+
71+
expect(data[63]).toBe(32) // Bee's reference-size byte for this directory node
72+
73+
const node = MantarayNode.unmarshalFromData(data, new Uint8Array(32))
74+
75+
expect(node.forks.size).toBe(2)
76+
expect(node.forks.get('a'.charCodeAt(0))!.prefix).toEqual(ENCODER.encode('about.html'))
77+
expect(node.forks.get('i'.charCodeAt(0))!.prefix).toEqual(ENCODER.encode('index.html'))
78+
79+
// An untouched node must re-marshal to the exact same bytes Bee produced,
80+
// including each fork's preserved type byte (0x02 = value).
81+
expect(await node.marshal()).toEqual(data)
82+
})
83+
4684
test('MantarayNode long', () => {
4785
const htmlPath = '/Code/Swarm/bee-js/test/coverage/lcov-report/index.html'
4886
const jsPath = '/Code/Swarm/bee-js/test/coverage/lcov-report/index.js'

0 commit comments

Comments
 (0)