Skip to main content

nectar_mantaray/
codec.rs

1//! Binary encoding for mantaray nodes (v0.1 and v0.2).
2
3use std::collections::BTreeMap;
4
5use crate::error::{MantarayError, Result};
6use crate::mode::NodeEntry;
7use crate::node::{Fork, Node, NodeType, Prefix};
8use crate::obfuscation::ObfuscationKey;
9
10use alloy_primitives::{U256, hex};
11use nectar_primitives::chunk::ChunkAddress;
12
13/// Mantaray wire format version (truncated keccak256, 31 bytes).
14enum VersionHash {
15    V01,
16    V02,
17}
18
19impl VersionHash {
20    /// Wire size of a truncated version hash.
21    const SIZE: usize = 31;
22
23    const V01_BYTES: [u8; Self::SIZE] =
24        hex!("025184789d63635766d78c41900196b57d7400875ebe4d9b5d1e76bd9652a9");
25    const V02_BYTES: [u8; Self::SIZE] =
26        hex!("5768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f");
27
28    const fn as_bytes(&self) -> &[u8; Self::SIZE] {
29        match self {
30            Self::V01 => &Self::V01_BYTES,
31            Self::V02 => &Self::V02_BYTES,
32        }
33    }
34
35    fn from_bytes(bytes: &[u8]) -> Option<Self> {
36        if bytes == Self::V01_BYTES {
37            Some(Self::V01)
38        } else if bytes == Self::V02_BYTES {
39            Some(Self::V02)
40        } else {
41            None
42        }
43    }
44}
45
46/// Wire layout descriptor for a serialised node header.
47struct NodeHeader;
48
49impl NodeHeader {
50    const SIZE: usize = ObfuscationKey::SIZE + VersionHash::SIZE + size_of::<u8>();
51    const VERSION_HASH_OFFSET: usize = ObfuscationKey::SIZE;
52    const REF_SIZE_OFFSET: usize = ObfuscationKey::SIZE + VersionHash::SIZE;
53}
54
55/// Wire layout descriptor for a serialised fork header.
56struct ForkHeader;
57
58impl ForkHeader {
59    /// Protocol anchor: total pre-reference bytes in a fork.
60    const PRE_REFERENCE_SIZE: usize = 32;
61    /// Offset to the prefix data (past node_type u8 + prefix_len u8).
62    const PREFIX_OFFSET: usize = size_of::<u8>() + size_of::<u8>();
63    /// Maximum prefix length that fits in a fork header.
64    const MAX_PREFIX_LEN: usize = Self::PRE_REFERENCE_SIZE - Self::PREFIX_OFFSET;
65    /// Size of the metadata length field.
66    const METADATA_LEN_SIZE: usize = size_of::<u16>();
67}
68
69// Compile-time layout assertions.
70const _: () = assert!(NodeHeader::SIZE == 64);
71const _: () = assert!(ForkHeader::PRE_REFERENCE_SIZE == 32);
72const _: () = assert!(ForkHeader::MAX_PREFIX_LEN == Prefix::MAX_LEN);
73const _: () = assert!(ObfuscationKey::SIZE == 32);
74
75#[cfg(test)]
76const VERSION_HASH_01_BYTES: [u8; 32] =
77    hex!("025184789d63635766d78c41900196b57d7400875ebe4d9b5d1e76bd9652a9b7");
78#[cfg(test)]
79const VERSION_HASH_02_BYTES: [u8; 32] =
80    hex!("5768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f7b");
81
82#[cfg(test)]
83const VERSION_STRING_01: &str = "mantaray:0.1";
84#[cfg(test)]
85const VERSION_STRING_02: &str = "mantaray:0.2";
86
87/// XOR `data` in-place with a repeating `key`.
88fn xor_in_place(data: &mut [u8], key: &[u8]) {
89    let key_len = key.len();
90    for (i, byte) in data.iter_mut().enumerate() {
91        *byte ^= key[i % key_len];
92    }
93}
94
95impl<E: NodeEntry> TryFrom<&Node<E>> for Vec<u8> {
96    type Error = MantarayError;
97
98    #[inline]
99    fn try_from(node: &Node<E>) -> Result<Self> {
100        encode_node(node)
101    }
102}
103
104fn encode_node<E: NodeEntry>(node: &Node<E>) -> Result<Vec<u8>> {
105    let ref_size = E::SIZE;
106    // Pre-allocate: header + entry + bitfield(32) + estimated fork data
107    let estimated = NodeHeader::SIZE
108        + ref_size
109        + 32
110        + node.forks.len() * (ForkHeader::PRE_REFERENCE_SIZE + ref_size);
111    let mut data = Vec::with_capacity(estimated);
112    data.resize(NodeHeader::SIZE, 0);
113
114    // Use the obfuscation key as-is. The key is set at manifest construction:
115    // - PlainManifest: ObfuscationKey::ZERO (no obfuscation)
116    // - EncryptedManifest: ObfuscationKey::generate() (random key)
117    let obfuscation_key = node.obfuscation_key.as_bytes();
118
119    data[..ObfuscationKey::SIZE].copy_from_slice(obfuscation_key);
120
121    data[NodeHeader::VERSION_HASH_OFFSET..NodeHeader::VERSION_HASH_OFFSET + VersionHash::SIZE]
122        .copy_from_slice(VersionHash::V02.as_bytes());
123
124    data[NodeHeader::REF_SIZE_OFFSET] = ref_size as u8;
125
126    // append entry (or E::SIZE zero bytes if empty)
127    match &node.entry {
128        Some(e) => e.write_to(&mut data),
129        None => data.resize(data.len() + ref_size, 0),
130    }
131
132    // build the 256-bit index of which fork bytes are present
133    let mut index = U256::ZERO;
134    for &fork_byte in node.forks.keys() {
135        index.set_bit(fork_byte as usize, true);
136    }
137    data.extend_from_slice(&index.to_le_bytes::<32>());
138
139    // append forks in sorted order
140    for fork in node.forks.values() {
141        fork.encode_into(&mut data)?;
142    }
143
144    // XOR-encrypt everything after the obfuscation key in-place
145    xor_in_place(&mut data[ObfuscationKey::SIZE..], obfuscation_key);
146
147    Ok(data)
148}
149
150impl<E: NodeEntry> TryFrom<&[u8]> for Node<E> {
151    type Error = MantarayError;
152
153    fn try_from(value: &[u8]) -> Result<Self> {
154        if value.len() < NodeHeader::SIZE {
155            return Err(MantarayError::DataTooShort);
156        }
157
158        let mut data = value.to_vec();
159
160        let key_bytes: [u8; ObfuscationKey::SIZE] = data[..ObfuscationKey::SIZE]
161            .try_into()
162            .map_err(|_| MantarayError::DataTooShort)?;
163        let obfuscation_key = ObfuscationKey::from(key_bytes);
164
165        // decrypt in-place
166        xor_in_place(
167            &mut data[ObfuscationKey::SIZE..],
168            obfuscation_key.as_bytes(),
169        );
170
171        let version_hash = &data
172            [NodeHeader::VERSION_HASH_OFFSET..NodeHeader::VERSION_HASH_OFFSET + VersionHash::SIZE];
173
174        let mut node = match VersionHash::from_bytes(version_hash) {
175            Some(VersionHash::V01) => decode_v01::<E>(&data)?,
176            Some(VersionHash::V02) => decode_v02::<E>(&data)?,
177            None => return Err(MantarayError::InvalidVersionHash),
178        };
179
180        node.obfuscation_key = obfuscation_key;
181        node.loaded = true;
182        Ok(node)
183    }
184}
185
186// ┌─────────────────────────── HAZMAT ───────────────────────────┐
187// │ BEE-WORKAROUND(bee#5483): bee's mantaray writer occasionally  │
188// │ emits a node with `ref_size = 0` (the byte at header offset  │
189// │ 63) for entry-less terminal nodes. This is not spec-legal:  │
190// │ the spec doc (bee/pkg/manifest/mantaray/docs/format/node.md) │
191// │ and every reference impl (bee, mantaray-js, nectar) treat    │
192// │ `ref_size` as a single uniform width in {32, 64} governing   │
193// │ both the entry slot and every fork ref slot. mantaray-js     │
194// │ documents the bee artifact with an explicit FIXME: "in Bee,  │
195// │ if one uploads a file on the bzz endpoint, the node under    │
196// │ `/` gets 0 refsize."                                         │
197// │                                                              │
198// │ Remove `decode_empty_terminal_node` and the two call-sites   │
199// │ guarded by `BEE-WORKAROUND(bee#5483)` once the upstream bee   │
200// │ fix lands and downstream consumers have upgraded past the    │
201// │ buggy releases.                                              │
202// └──────────────────────────────────────────────────────────────┘
203
204/// Decode a `ref_size = 0` node as the empty terminal node that bee intends
205/// it to mean.
206///
207/// Accepts this wire shape only when the forks bitfield is also empty. A
208/// `ref_size = 0` node with non-empty forks is unrecoverable by any
209/// implementation (fork refs would have zero width), so we reject it as
210/// malformed rather than silently dropping forks the way bee's v0.2 decoder
211/// does (`bee/pkg/manifest/mantaray/marshal.go:285-287`).
212///
213/// See the HAZMAT block above for the full context.
214fn decode_empty_terminal_node<E: NodeEntry>(data: &[u8]) -> Result<Node<E>> {
215    let bitfield_start = NodeHeader::SIZE;
216    let bitfield_end = bitfield_start + 32;
217    if data.len() < bitfield_end {
218        return Err(MantarayError::DataTooShort);
219    }
220    if data[bitfield_start..bitfield_end].iter().any(|&b| b != 0) {
221        return Err(MantarayError::EntrySizeMismatch {
222            expected: E::SIZE,
223            actual: 0,
224        });
225    }
226    Ok(Node {
227        entry: None,
228        forks: BTreeMap::new(),
229        ..Default::default()
230    })
231}
232
233fn decode_v01<E: NodeEntry>(data: &[u8]) -> Result<Node<E>> {
234    let ref_bytes_size = data[NodeHeader::REF_SIZE_OFFSET] as usize;
235    // BEE-WORKAROUND(bee#5483): see HAZMAT block above `decode_empty_terminal_node`.
236    if ref_bytes_size == 0 {
237        return decode_empty_terminal_node::<E>(data);
238    }
239    if ref_bytes_size != E::SIZE {
240        return Err(MantarayError::EntrySizeMismatch {
241            expected: E::SIZE,
242            actual: ref_bytes_size,
243        });
244    }
245
246    let entry_bytes = &data[NodeHeader::SIZE..NodeHeader::SIZE + ref_bytes_size];
247    let entry = if entry_bytes.iter().all(|&b| b == 0) {
248        None
249    } else {
250        Some(E::try_from_bytes(entry_bytes)?)
251    };
252
253    let mut offset = NodeHeader::SIZE + ref_bytes_size;
254    let index = U256::from_le_slice(&data[offset..offset + 32]);
255    offset += 32;
256
257    let mut forks = BTreeMap::new();
258    for b in 0..=u8::MAX {
259        if index.bit(b as usize) {
260            let end = offset + ForkHeader::PRE_REFERENCE_SIZE + ref_bytes_size;
261            if data.len() < end {
262                return Err(MantarayError::InsufficientForkBytes {
263                    expected: end,
264                    actual: data.len(),
265                    byte_index: b as usize,
266                });
267            }
268
269            let mut fork = Fork::default();
270            fork.decode_v01(&data[offset..end])?;
271            forks.insert(b, fork);
272            offset = end;
273        }
274    }
275
276    Ok(Node {
277        entry,
278        forks,
279        ..Default::default()
280    })
281}
282
283fn decode_v02<E: NodeEntry>(data: &[u8]) -> Result<Node<E>> {
284    let ref_bytes_size = data[NodeHeader::REF_SIZE_OFFSET] as usize;
285    // BEE-WORKAROUND(bee#5483): see HAZMAT block above `decode_empty_terminal_node`.
286    if ref_bytes_size == 0 {
287        return decode_empty_terminal_node::<E>(data);
288    }
289    if ref_bytes_size != E::SIZE {
290        return Err(MantarayError::EntrySizeMismatch {
291            expected: E::SIZE,
292            actual: ref_bytes_size,
293        });
294    }
295
296    let entry_bytes = &data[NodeHeader::SIZE..NodeHeader::SIZE + ref_bytes_size];
297    let entry = if entry_bytes.iter().all(|&b| b == 0) {
298        None
299    } else {
300        Some(E::try_from_bytes(entry_bytes)?)
301    };
302
303    let mut offset = NodeHeader::SIZE + ref_bytes_size;
304    let mut node_type = NodeType::empty();
305
306    // deduce edge type from index
307    if data[offset..offset + 32].iter().any(|&b| b != 0) {
308        node_type |= NodeType::EDGE;
309    }
310
311    let index = U256::from_le_slice(&data[offset..offset + 32]);
312    offset += 32;
313
314    let mut forks = BTreeMap::new();
315    for b in 0..=u8::MAX {
316        if index.bit(b as usize) {
317            let mut fork = Fork::default();
318
319            if data.len() < offset + 1 {
320                return Err(MantarayError::InsufficientForkBytes {
321                    expected: offset + 1,
322                    actual: data.len(),
323                    byte_index: b as usize,
324                });
325            }
326
327            let fork_node_type = NodeType::from_bits_truncate(data[offset]);
328            let mut node_fork_size = ForkHeader::PRE_REFERENCE_SIZE + ref_bytes_size;
329
330            if fork_node_type.contains(NodeType::METADATA) {
331                if data.len()
332                    < offset
333                        + ForkHeader::PRE_REFERENCE_SIZE
334                        + ref_bytes_size
335                        + ForkHeader::METADATA_LEN_SIZE
336                {
337                    return Err(MantarayError::InsufficientForkBytes {
338                        expected: offset
339                            + ForkHeader::PRE_REFERENCE_SIZE
340                            + ref_bytes_size
341                            + ForkHeader::METADATA_LEN_SIZE,
342                        actual: data.len(),
343                        byte_index: b as usize,
344                    });
345                }
346
347                let metadata_bytes_size = u16::from_be_bytes(
348                    data[offset + node_fork_size
349                        ..offset + node_fork_size + ForkHeader::METADATA_LEN_SIZE]
350                        .try_into()
351                        .map_err(|_| MantarayError::DataTooShort)?,
352                ) as usize;
353
354                node_fork_size += ForkHeader::METADATA_LEN_SIZE;
355                node_fork_size += metadata_bytes_size;
356
357                if offset + node_fork_size > data.len() {
358                    return Err(MantarayError::InsufficientForkBytes {
359                        expected: offset + node_fork_size,
360                        actual: data.len(),
361                        byte_index: b as usize,
362                    });
363                }
364
365                fork.decode_v02(
366                    &data[offset..offset + node_fork_size],
367                    ref_bytes_size,
368                    metadata_bytes_size,
369                )?;
370            } else {
371                if data.len() < offset + ForkHeader::PRE_REFERENCE_SIZE + ref_bytes_size {
372                    return Err(MantarayError::InsufficientForkBytes {
373                        expected: offset + ForkHeader::PRE_REFERENCE_SIZE + ref_bytes_size,
374                        actual: data.len(),
375                        byte_index: b as usize,
376                    });
377                }
378
379                fork.decode_v01(&data[offset..offset + node_fork_size])?;
380            }
381
382            forks.insert(b, fork);
383            offset += node_fork_size;
384        }
385    }
386
387    Ok(Node {
388        node_type,
389        entry,
390        forks,
391        ..Default::default()
392    })
393}
394
395/// Parse and validate fork header. Returns (node_type, prefix).
396fn parse_fork_header(data: &[u8]) -> Result<(NodeType, Prefix)> {
397    let node_type = NodeType::from_bits_truncate(data[0]);
398    let prefix_length = data[1] as usize;
399    if prefix_length == 0 || prefix_length > Prefix::MAX_LEN {
400        return Err(MantarayError::InvalidPrefixLength {
401            max: Prefix::MAX_LEN,
402            actual: prefix_length,
403        });
404    }
405    let prefix = Prefix::from_slice(
406        &data[ForkHeader::PREFIX_OFFSET..ForkHeader::PREFIX_OFFSET + prefix_length],
407    );
408    Ok((node_type, prefix))
409}
410
411impl<E: NodeEntry> Fork<E> {
412    /// Create a node from reference bytes (first 32 bytes used as chunk address).
413    fn node_from_ref_bytes(ref_data: &[u8]) -> Result<Node<E>> {
414        if ref_data.len() < 32 {
415            return Err(MantarayError::DataTooShort);
416        }
417        let addr_bytes: [u8; 32] = ref_data[..32]
418            .try_into()
419            .map_err(|_| MantarayError::DataTooShort)?;
420        Ok(Node::from_reference(ChunkAddress::from(addr_bytes)))
421    }
422
423    /// Encode this fork, appending to `buf`.
424    fn encode_into(&self, data: &mut Vec<u8>) -> Result<()> {
425        data.push(self.node.node_type.bits());
426        data.push(self.prefix.len() as u8);
427
428        // write prefix padded to Prefix::MAX_LEN — Prefix is already zero-padded
429        data.extend_from_slice(self.prefix.padded_bytes());
430
431        // Write E::SIZE bytes for the reference (chunk address + zero padding)
432        if let Some(addr) = &self.node.reference {
433            data.extend_from_slice(addr.as_bytes());
434            // Pad to E::SIZE if needed (encrypted mode has 64-byte refs)
435            let padding = E::SIZE.saturating_sub(32);
436            if padding > 0 {
437                data.resize(data.len() + padding, 0);
438            }
439        }
440
441        if self.node.is_with_metadata() {
442            let mut metadata_json = serde_json::to_string(&self.node.metadata)
443                .map_err(|e| MantarayError::InvalidMetadata {
444                    message: e.to_string(),
445                })?
446                .into_bytes();
447
448            let metadata_bytes_size_with_header =
449                metadata_json.len() + ForkHeader::METADATA_LEN_SIZE;
450
451            let padding = if metadata_bytes_size_with_header < ObfuscationKey::SIZE {
452                ObfuscationKey::SIZE - metadata_bytes_size_with_header
453            } else if metadata_bytes_size_with_header > ObfuscationKey::SIZE {
454                let rem = metadata_bytes_size_with_header % ObfuscationKey::SIZE;
455                if rem == 0 {
456                    0
457                } else {
458                    ObfuscationKey::SIZE - rem
459                }
460            } else {
461                0
462            };
463
464            metadata_json.resize(metadata_json.len() + padding, 0x0a);
465
466            let metadata_size = metadata_json.len();
467            if metadata_size > u16::MAX as usize {
468                return Err(MantarayError::MetadataTooLarge {
469                    max: u16::MAX as usize,
470                    actual: metadata_size,
471                });
472            }
473
474            data.extend_from_slice(&(metadata_size as u16).to_be_bytes());
475            data.extend_from_slice(&metadata_json);
476        }
477
478        Ok(())
479    }
480
481    /// Decode a fork from v0.1 binary data.
482    pub(crate) fn decode_v01(&mut self, data: &[u8]) -> Result<()> {
483        let (node_type, prefix) = parse_fork_header(data)?;
484
485        self.prefix = prefix;
486        let ref_data = &data[ForkHeader::PRE_REFERENCE_SIZE..];
487        self.node = Self::node_from_ref_bytes(ref_data)?;
488        self.node.node_type = node_type;
489
490        Ok(())
491    }
492
493    /// Decode a fork from v0.2 binary data (with metadata).
494    pub(crate) fn decode_v02(
495        &mut self,
496        data: &[u8],
497        ref_bytes_size: usize,
498        metadata_bytes_size: usize,
499    ) -> Result<()> {
500        let (node_type, prefix) = parse_fork_header(data)?;
501
502        self.prefix = prefix;
503        let ref_data =
504            &data[ForkHeader::PRE_REFERENCE_SIZE..ForkHeader::PRE_REFERENCE_SIZE + ref_bytes_size];
505        self.node = Self::node_from_ref_bytes(ref_data)?;
506        self.node.node_type = node_type;
507
508        if metadata_bytes_size > 0 {
509            let metadata_start =
510                ForkHeader::PRE_REFERENCE_SIZE + ref_bytes_size + ForkHeader::METADATA_LEN_SIZE;
511            let metadata_bytes = &data[metadata_start..];
512            self.node.metadata = serde_json::from_slice(metadata_bytes).map_err(|e| {
513                MantarayError::InvalidMetadata {
514                    message: e.to_string(),
515                }
516            })?;
517        }
518
519        Ok(())
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use alloy_primitives::hex;
527    use alloy_primitives::utils::keccak256;
528
529    const ENCODED_V01: &str = "52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64950ac787fbce1061870e8d34e0a638bc7e812c7ca4ebd31d626a572ba47b06f6952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072102654f163f5f0fa0621d729566c74d10037c4d7bbb0407d1e2c64950fcd3072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64950f89d6640e3044f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64850ff9f642182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64b50fc98072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64a50ff99622182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64d";
530    const ENCODED_V02: &str = "52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64905954fb18659339d0b25e0fb9723d3cd5d528fb3c8d495fd157bd7b7a210496952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072102654f163f5f0fa0621d729566c74d10037c4d7bbb0407d1e2c64940fcd3072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952e3872548ec012a6e123b60f9177017fb12e57732621d2c1ada267adbe8cc4350f89d6640e3044f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64850ff9f642182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64b50fc98072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64a50ff99622182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64952fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64d";
531
532    #[derive(Clone, Default)]
533    struct TestEntry {
534        path: String,
535        metadata: BTreeMap<String, String>,
536    }
537
538    fn test_entries() -> [TestEntry; 5] {
539        [
540            TestEntry {
541                path: "/".to_string(),
542                metadata: serde_json::from_str(r#"{"index-document": "aaaaa"}"#).unwrap(),
543            },
544            TestEntry {
545                path: "aaaaa".to_string(),
546                ..Default::default()
547            },
548            TestEntry {
549                path: "cc".to_string(),
550                ..Default::default()
551            },
552            TestEntry {
553                path: "d".to_string(),
554                ..Default::default()
555            },
556            TestEntry {
557                path: "ee".to_string(),
558                ..Default::default()
559            },
560        ]
561    }
562
563    #[test]
564    fn version_hash_01() {
565        assert_eq!(
566            keccak256(VERSION_STRING_01.as_bytes()),
567            VERSION_HASH_01_BYTES,
568        );
569    }
570
571    #[test]
572    fn version_hash_02() {
573        assert_eq!(
574            keccak256(VERSION_STRING_02.as_bytes()),
575            VERSION_HASH_02_BYTES,
576        );
577    }
578
579    #[test]
580    fn decode_v01() {
581        let data = hex::decode(ENCODED_V01).unwrap();
582        let n = Node::<ChunkAddress>::try_from(data.as_slice()).unwrap();
583
584        let mut expect_bytes = hex::decode(&ENCODED_V01[128..192]).unwrap();
585        xor_in_place(&mut expect_bytes, n.obfuscation_key().as_bytes());
586
587        // Root entry bytes are all zeros after decryption → None (no entry).
588        if expect_bytes.iter().all(|&b| b == 0) {
589            assert!(n.entry().is_none());
590        } else {
591            assert_eq!(n.entry().unwrap().as_bytes(), &expect_bytes[..]);
592        }
593        assert_eq!(test_entries().len(), n.forks().len());
594
595        for entry in test_entries() {
596            let key = entry.path.as_bytes()[0];
597            assert!(n.forks().contains_key(&key));
598            assert_eq!(n.forks()[&key].prefix(), entry.path.as_bytes());
599        }
600    }
601
602    #[test]
603    fn decode_v02() {
604        let data = hex::decode(ENCODED_V02).unwrap();
605        let n = Node::<ChunkAddress>::try_from(data.as_slice()).unwrap();
606
607        let mut expect_bytes = hex::decode(&ENCODED_V02[128..192]).unwrap();
608        xor_in_place(&mut expect_bytes, n.obfuscation_key().as_bytes());
609
610        // Root entry bytes are all zeros after decryption → None (no entry).
611        if expect_bytes.iter().all(|&b| b == 0) {
612            assert!(n.entry().is_none());
613        } else {
614            assert_eq!(n.entry().unwrap().as_bytes(), &expect_bytes[..]);
615        }
616        assert_eq!(test_entries().len(), n.forks().len());
617
618        for entry in test_entries() {
619            let key = entry.path.as_bytes()[0];
620            assert!(n.forks().contains_key(&key));
621            assert_eq!(n.forks()[&key].prefix(), entry.path.as_bytes());
622
623            if !entry.metadata.is_empty() {
624                assert_eq!(n.forks()[&key].node().metadata(), &entry.metadata);
625            }
626        }
627    }
628
629    #[test]
630    fn decode_nil_input() {
631        let result = Node::<ChunkAddress>::try_from([].as_slice());
632        assert!(matches!(result, Err(MantarayError::DataTooShort)));
633    }
634
635    #[test]
636    fn decode_too_short_for_header() {
637        let data = vec![0u8; NodeHeader::SIZE - 1];
638        let result = Node::<ChunkAddress>::try_from(data.as_slice());
639        assert!(matches!(result, Err(MantarayError::DataTooShort)));
640    }
641
642    #[test]
643    fn decode_invalid_version_hash() {
644        let data = vec![0u8; NodeHeader::SIZE];
645        let result = Node::<ChunkAddress>::try_from(data.as_slice());
646        assert!(matches!(result, Err(MantarayError::InvalidVersionHash)));
647    }
648
649    /// Test vector: valid manifest with correct metadata size (93 bytes).
650    /// This is a v0.2 manifest with zero obfuscation key, a single fork at '/',
651    /// and website-index-document metadata.
652    #[test]
653    fn decode_valid_manifest_from_go() {
654        let data = hex::decode(
655            "00000000000000000000000000000000000000000000000000000000000000005768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f200000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000016012f0000000000000000000000000000000000000000000000000000000000e87f95c3d081c4fede769b6c69e27b435e525cbd25c6715c607e7c531e329639005d7b22776562736974652d696e6465782d646f63756d656e74223a2233356561656538316262363338303436393965633637316265323736326465626665346662643330636461646139303232393239646131613965366134366436227d0a"
656        ).unwrap();
657        assert!(Node::<ChunkAddress>::try_from(data.as_slice()).is_ok());
658    }
659
660    /// Test vector: metadata size field says 89 but actual content needs 93.
661    /// Should fail because there aren't enough bytes for the declared metadata.
662    #[test]
663    fn decode_invalid_manifest_size_89() {
664        let data = hex::decode(
665            "00000000000000000000000000000000000000000000000000000000000000005768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f200000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000016012f0000000000000000000000000000000000000000000000000000000000e87f95c3d081c4fede769b6c69e27b435e525cbd25c6715c607e7c531e32963900597b22776562736974652d696e6465782d646f63756d656e74223a2233356561656538316262363338303436393965633637316265323736326465626665346662643330636461646139303232393239646131613965366134366436227d0a"
666        ).unwrap();
667        assert!(Node::<ChunkAddress>::try_from(data.as_slice()).is_err());
668    }
669
670    /// Test vector: metadata size field says 95 but actual content is 93.
671    /// Should fail because the size exceeds available bytes.
672    #[test]
673    fn decode_invalid_manifest_size_95() {
674        let data = hex::decode(
675            "00000000000000000000000000000000000000000000000000000000000000005768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f200000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000016012f0000000000000000000000000000000000000000000000000000000000e87f95c3d081c4fede769b6c69e27b435e525cbd25c6715c607e7c531e329639005f7b22776562736974652d696e6465782d646f63756d656e74223a2233356561656538316262363338303436393965633637316265323736326465626665346662643330636461646139303232393239646131613965366134366436227d0a"
676        ).unwrap();
677        assert!(Node::<ChunkAddress>::try_from(data.as_slice()).is_err());
678    }
679
680    /// Test vector: metadata size field says 96 but actual content is 93.
681    /// Should fail because the size exceeds available bytes.
682    #[test]
683    fn decode_invalid_manifest_size_96() {
684        let data = hex::decode(
685            "00000000000000000000000000000000000000000000000000000000000000005768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f200000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000016012f0000000000000000000000000000000000000000000000000000000000e87f95c3d081c4fede769b6c69e27b435e525cbd25c6715c607e7c531e32963900607b22776562736974652d696e6465782d646f63756d656e74223a2233356561656538316262363338303436393965633637316265323736326465626665346662643330636461646139303232393239646131613965366134366436227d0a"
686        ).unwrap();
687        assert!(Node::<ChunkAddress>::try_from(data.as_slice()).is_err());
688    }
689
690    /// BEE-WORKAROUND(bee#5483): bee occasionally emits nodes with
691    /// `ref_size = 0` for entry-less terminal nodes (mantaray-js FIXME:
692    /// "in Bee, if one uploads a file on the bzz endpoint, the node under
693    /// `/` gets 0 refsize"). Tolerate this wire shape only when the forks
694    /// bitfield is also empty.
695    #[test]
696    fn decode_bee_legacy_ref_size_zero_empty_node() {
697        // v0.2 layout: 32 obfuscation key zeros || 31 version hash || ref_size=0 || 32 index zeros = 96 bytes
698        let mut data = vec![0u8; 96];
699        data[ObfuscationKey::SIZE..ObfuscationKey::SIZE + VersionHash::SIZE]
700            .copy_from_slice(VersionHash::V02.as_bytes());
701        // ref_size at offset 63 is left as 0; index (offset 64..96) is all zero.
702
703        let n = Node::<ChunkAddress>::try_from(data.as_slice())
704            .expect("ref_size=0 with empty forks should decode as terminal node");
705        assert!(n.entry().is_none());
706        assert!(n.forks().is_empty());
707    }
708
709    /// BEE-WORKAROUND(bee#5483): a `ref_size = 0` node with a non-empty forks
710    /// bitfield is unrecoverable by any reference implementation (fork refs
711    /// would have zero width). Reject as malformed rather than silently
712    /// dropping forks the way bee's v0.2 decoder does.
713    #[test]
714    fn decode_bee_legacy_ref_size_zero_with_forks_is_rejected() {
715        let mut data = vec![0u8; 96];
716        data[ObfuscationKey::SIZE..ObfuscationKey::SIZE + VersionHash::SIZE]
717            .copy_from_slice(VersionHash::V02.as_bytes());
718        // ref_size = 0 (offset 63 already zero), but flip one bit in the index.
719        data[NodeHeader::SIZE] = 0x01;
720
721        let result = Node::<ChunkAddress>::try_from(data.as_slice());
722        assert!(matches!(
723            result,
724            Err(MantarayError::EntrySizeMismatch {
725                expected: 32,
726                actual: 0
727            })
728        ));
729    }
730
731    /// BEE-WORKAROUND(bee#5483): same as above but for v0.1; both decoders
732    /// must apply the same rule.
733    #[test]
734    fn decode_bee_legacy_ref_size_zero_v01_empty_node() {
735        let mut data = vec![0u8; 96];
736        data[ObfuscationKey::SIZE..ObfuscationKey::SIZE + VersionHash::SIZE]
737            .copy_from_slice(VersionHash::V01.as_bytes());
738
739        let n = Node::<ChunkAddress>::try_from(data.as_slice())
740            .expect("v0.1 ref_size=0 with empty forks should decode as terminal node");
741        assert!(n.entry().is_none());
742        assert!(n.forks().is_empty());
743    }
744
745    /// Pin nectar's encoder behaviour: even for an entry-less node, it must
746    /// emit `ref_size = E::SIZE`, never `0`. Spec-correct, matches bee's
747    /// "valid manifest" test fixture, matches mantaray-js. Emitting 0 would
748    /// reproduce the bee bug rather than fix it.
749    #[test]
750    fn encoder_never_emits_ref_size_zero_for_entryless_node() {
751        let n = Node::<ChunkAddress>::new_unencrypted();
752        let encoded = Vec::<u8>::try_from(&n).unwrap();
753
754        // Decrypt (obfuscation key is all-zero for `new_unencrypted`, so XOR
755        // is a no-op, but go through the motions for clarity).
756        let mut decoded = encoded;
757        let key = decoded[..ObfuscationKey::SIZE].to_vec();
758        xor_in_place(&mut decoded[ObfuscationKey::SIZE..], &key);
759
760        assert_eq!(
761            decoded[NodeHeader::REF_SIZE_OFFSET] as usize,
762            <ChunkAddress as NodeEntry>::SIZE,
763            "encoder must emit ref_size = E::SIZE, not 0; spec requires uniform reference width"
764        );
765    }
766
767    /// Encode-decode round-trip preserves entries and metadata.
768    #[test]
769    fn encode_decode_round_trip() {
770        let mut n = Node::<ChunkAddress>::new_unencrypted();
771
772        for entry in test_entries() {
773            let path = entry.path.as_bytes();
774            let e = {
775                let mut buf = [0u8; 32];
776                let len = path.len().min(32);
777                buf[32 - len..].copy_from_slice(&path[..len]);
778                ChunkAddress::from(buf)
779            };
780            n.add::<nectar_primitives::store::NullLoader, { nectar_primitives::bmt::DEFAULT_BODY_SIZE }>(
781                path, Some(e), entry.metadata, &nectar_primitives::store::NullLoader,
782            )
783            .unwrap();
784        }
785
786        // assign deterministic references to forks so encoding works
787        for (counter, fork) in n.forks.values_mut().enumerate() {
788            let mut addr = [0u8; 32];
789            addr[31] = counter as u8;
790            fork.node.reference = Some(nectar_primitives::chunk::ChunkAddress::from(addr));
791        }
792
793        let encoded = Vec::<u8>::try_from(&n).unwrap();
794        let n2 = Node::<ChunkAddress>::try_from(encoded.as_slice()).unwrap();
795
796        // Root has no entry; encoding writes zero bytes, decoding reads them back as None
797        assert!(n2.entry().is_none());
798        assert_eq!(n.forks().len(), n2.forks().len());
799
800        for entry in test_entries() {
801            let key = entry.path.as_bytes()[0];
802            assert!(n2.forks().contains_key(&key));
803            assert_eq!(n2.forks()[&key].prefix(), entry.path.as_bytes());
804            if !entry.metadata.is_empty() {
805                assert_eq!(n2.forks()[&key].node().metadata(), &entry.metadata);
806            }
807        }
808    }
809}