ndn_packet/encode/
data.rs

1use std::time::Duration;
2
3use bytes::{BufMut, Bytes, BytesMut};
4use ndn_tlv::TlvWriter;
5
6use super::{write_name, write_nni};
7use crate::{Name, SignatureType, tlv_type};
8
9// ─── Fast-path shared helpers ─────────────────────────────────────────────────
10
11/// Encoded SignatureInfo TLV for DigestSha256 with no KeyLocator (5 bytes, fixed).
12///
13/// `0x16` type SignatureInfo | `0x03` length | `0x1B` type SignatureType | `0x01` length | `0x00` DigestSha256
14///
15/// # Limitations
16/// This encoding is correct only when no KeyLocator is present.  DigestSha256
17/// packets that carry a KeyLocator (rare, used for self-signed certificates) must
18/// be built via `sign_sync` / `sign` instead.
19const SIGINFO_DIGEST_SHA256: [u8; 5] = [0x16, 0x03, 0x1B, 0x01, 0x00];
20
21/// Encoded SignatureInfo TLV for DigestBlake3 with no KeyLocator (5 bytes, fixed).
22///
23/// Identical layout to `SIGINFO_DIGEST_SHA256` but with type code 6 (experimental
24/// BLAKE3 digest, not yet in the NDN Packet Format specification).
25const SIGINFO_DIGEST_BLAKE3: [u8; 5] = [0x16, 0x03, 0x1B, 0x01, 0x06];
26
27/// Write a VarNumber (u64) into `buf` using NDN's minimal-byte encoding.
28#[inline(always)]
29fn put_vu(buf: &mut BytesMut, v: u64) {
30    let mut tmp = [0u8; 9];
31    let n = ndn_tlv::write_varu64(&mut tmp, v);
32    buf.put_slice(&tmp[..n]);
33}
34
35/// Pre-computed TLV sizes for Name, MetaInfo, and Content.
36///
37/// Created once and shared between the size-calculation and write phases of
38/// the single-buffer fast paths (`sign_digest_sha256`, `sign_none`).
39struct FastPathSizes {
40    /// Total byte count of all encoded name components (value part of Name TLV).
41    comps_inner: usize,
42    /// Full encoded size of the Name TLV (type + length + comps_inner).
43    name_tlv: usize,
44    /// Total byte count of MetaInfo *value* bytes (0 when no freshness/FinalBlockId).
45    mi_inner: usize,
46    /// Full encoded size of the MetaInfo TLV (0 when `mi_inner == 0`).
47    metainfo_tlv: usize,
48    /// Full encoded size of the Content TLV.
49    content_tlv: usize,
50}
51
52impl FastPathSizes {
53    fn compute(
54        name: &Name,
55        freshness: Option<Duration>,
56        final_block_id: Option<&Bytes>,
57        content: &[u8],
58    ) -> Self {
59        use ndn_tlv::varu64_size;
60
61        let comps_inner: usize = name
62            .components()
63            .iter()
64            .map(|c| varu64_size(c.typ) + varu64_size(c.value.len() as u64) + c.value.len())
65            .sum();
66        let name_tlv = varu64_size(tlv_type::NAME) + varu64_size(comps_inner as u64) + comps_inner;
67
68        let mi_inner = {
69            let mut s = 0usize;
70            if let Some(f) = freshness {
71                let (_, nni_len) = super::nni(f.as_millis() as u64);
72                s +=
73                    varu64_size(tlv_type::FRESHNESS_PERIOD) + varu64_size(nni_len as u64) + nni_len;
74            }
75            if let Some(fb) = final_block_id {
76                s +=
77                    varu64_size(tlv_type::FINAL_BLOCK_ID) + varu64_size(fb.len() as u64) + fb.len();
78            }
79            s
80        };
81        let metainfo_tlv = if mi_inner > 0 {
82            varu64_size(tlv_type::META_INFO) + varu64_size(mi_inner as u64) + mi_inner
83        } else {
84            0
85        };
86
87        let content_tlv =
88            varu64_size(tlv_type::CONTENT) + varu64_size(content.len() as u64) + content.len();
89
90        Self {
91            comps_inner,
92            name_tlv,
93            mi_inner,
94            metainfo_tlv,
95            content_tlv,
96        }
97    }
98}
99
100/// Write Name, MetaInfo (if any), and Content TLVs directly into `buf`.
101///
102/// Relies on pre-computed sizes from [`FastPathSizes`] to avoid a second pass.
103/// Must be called with the same `freshness`/`final_block_id`/`content` used
104/// to compute `sz`, otherwise the written bytes will not match the size headers.
105fn write_fields(
106    buf: &mut BytesMut,
107    name: &Name,
108    freshness: Option<Duration>,
109    final_block_id: Option<&Bytes>,
110    content: &[u8],
111    sz: &FastPathSizes,
112) {
113    put_vu(buf, tlv_type::NAME);
114    put_vu(buf, sz.comps_inner as u64);
115    for comp in name.components() {
116        put_vu(buf, comp.typ);
117        put_vu(buf, comp.value.len() as u64);
118        buf.put_slice(&comp.value);
119    }
120    if sz.mi_inner > 0 {
121        put_vu(buf, tlv_type::META_INFO);
122        put_vu(buf, sz.mi_inner as u64);
123        if let Some(f) = freshness {
124            let (nni_buf, nni_len) = super::nni(f.as_millis() as u64);
125            put_vu(buf, tlv_type::FRESHNESS_PERIOD);
126            put_vu(buf, nni_len as u64);
127            buf.put_slice(&nni_buf[..nni_len]);
128        }
129        if let Some(fb) = final_block_id {
130            put_vu(buf, tlv_type::FINAL_BLOCK_ID);
131            put_vu(buf, fb.len() as u64);
132            buf.put_slice(fb);
133        }
134    }
135    put_vu(buf, tlv_type::CONTENT);
136    put_vu(buf, content.len() as u64);
137    buf.put_slice(content);
138}
139
140// ─── DataBuilder ─────────────────────────────────────────────────────────────
141
142/// Configurable Data encoder with optional signing.
143///
144/// ```
145/// # use ndn_packet::encode::DataBuilder;
146/// # use std::time::Duration;
147/// let wire = DataBuilder::new("/test", b"hello")
148///     .freshness(Duration::from_secs(10))
149///     .build();
150/// ```
151pub struct DataBuilder {
152    name: Name,
153    content: Vec<u8>,
154    freshness: Option<Duration>,
155    /// Raw bytes of a NameComponent TLV to write as the FinalBlockId value.
156    ///
157    /// Use [`DataBuilder::final_block_id_seg`] to set this from a segment index.
158    final_block_id: Option<Bytes>,
159}
160
161impl DataBuilder {
162    pub fn new(name: impl Into<Name>, content: &[u8]) -> Self {
163        Self {
164            name: name.into(),
165            content: content.to_vec(),
166            freshness: None,
167            final_block_id: None,
168        }
169    }
170
171    pub fn freshness(mut self, d: Duration) -> Self {
172        self.freshness = Some(d);
173        self
174    }
175
176    /// Set the FinalBlockId from a raw NameComponent TLV value.
177    pub fn final_block_id(mut self, component_bytes: Bytes) -> Self {
178        self.final_block_id = Some(component_bytes);
179        self
180    }
181
182    /// Encode the last segment index as a GenericNameComponent and set as FinalBlockId.
183    ///
184    /// This matches the ASCII-string segment encoding used by `ndn-put` and `ndn-peek`.
185    ///
186    /// ```
187    /// # use ndn_packet::encode::DataBuilder;
188    /// let wire = DataBuilder::new("/test/0", b"hello")
189    ///     .final_block_id_seg(5)   // segments 0..=5
190    ///     .build();
191    /// ```
192    pub fn final_block_id_seg(self, last_seg: usize) -> Self {
193        let s = last_seg.to_string();
194        let bytes = s.as_bytes();
195        // GenericNameComponent: type=0x08, length, value
196        let mut buf = Vec::with_capacity(2 + bytes.len());
197        buf.push(0x08u8); // GenericNameComponent type
198        // Length as minimal variable-length (segments fit in < 128 bytes of digits)
199        buf.push(bytes.len() as u8);
200        buf.extend_from_slice(bytes);
201        self.final_block_id(Bytes::from(buf))
202    }
203
204    /// Encode the last segment index as a SegmentNameComponent (TLV type 0x32, big-endian
205    /// non-negative integer encoding) and set as FinalBlockId.
206    ///
207    /// This matches the segment encoding used by `ndn-cxx`'s `ndnputchunks`.
208    /// Use [`DataBuilder::final_block_id_seg`] for ASCII-decimal encoding instead.
209    pub fn final_block_id_typed_seg(self, last_seg: u64) -> Self {
210        let encoded = encode_nni_be(last_seg);
211        let mut buf = Vec::with_capacity(2 + encoded.len());
212        buf.push(0x32u8); // SegmentNameComponent TLV type
213        buf.push(encoded.len() as u8);
214        buf.extend_from_slice(&encoded);
215        self.final_block_id(Bytes::from(buf))
216    }
217
218    /// Build and sign the Data with `DigestSha256`.
219    ///
220    /// Single-buffer fast path: pre-computes all TLV sizes, allocates one
221    /// `BytesMut`, writes Name + MetaInfo + Content + SignatureInfo directly,
222    /// then hashes in-place — **1 allocation, 0 copies of the signed region**.
223    ///
224    /// **~6× fewer allocations** than `sign_sync` with a lambda; use this for
225    /// all high-throughput DigestSha256 production.
226    ///
227    /// # Limitations
228    /// - Requires the `std` feature (transitively requires `ring`).
229    ///   `no_std` callers must use `build()` + out-of-band signing.
230    /// - The hardcoded `SignatureInfo` contains no KeyLocator.  DigestSha256
231    ///   packets that carry a KeyLocator must be built via `sign_sync`/`sign`.
232    /// - `debug_assert` guards validate the size pre-computation but are elided
233    ///   in release builds.  The math is deterministic once the name/content
234    ///   are fixed; no runtime variability can trigger them in correct code.
235    #[cfg(feature = "std")]
236    pub fn sign_digest_sha256(self) -> Bytes {
237        use ndn_tlv::varu64_size;
238
239        // SignatureValue: type(1) + len(1) + SHA-256(32) = 34 bytes
240        const SIGVALUE: usize = 34;
241
242        let sz = FastPathSizes::compute(
243            &self.name,
244            self.freshness,
245            self.final_block_id.as_ref(),
246            &self.content,
247        );
248        let signed_size =
249            sz.name_tlv + sz.metainfo_tlv + sz.content_tlv + SIGINFO_DIGEST_SHA256.len();
250        let inner_size = signed_size + SIGVALUE;
251        let header_size = varu64_size(tlv_type::DATA) + varu64_size(inner_size as u64);
252
253        let mut buf = BytesMut::with_capacity(header_size + inner_size);
254        put_vu(&mut buf, tlv_type::DATA);
255        put_vu(&mut buf, inner_size as u64);
256
257        let signed_start = buf.len();
258        write_fields(
259            &mut buf,
260            &self.name,
261            self.freshness,
262            self.final_block_id.as_ref(),
263            &self.content,
264            &sz,
265        );
266        buf.put_slice(&SIGINFO_DIGEST_SHA256);
267        debug_assert_eq!(
268            buf.len() - signed_start,
269            signed_size,
270            "signed region size mismatch"
271        );
272
273        let hash = ring::digest::digest(&ring::digest::SHA256, &buf[signed_start..]);
274        buf.put_slice(&[0x17u8, 0x20]);
275        buf.put_slice(hash.as_ref());
276        debug_assert_eq!(buf.len(), header_size + inner_size, "total size mismatch");
277
278        buf.freeze()
279    }
280
281    /// Build a Data packet signed with a **BLAKE3 digest** (experimental).
282    ///
283    /// Single-buffer fast path with **1 allocation, 0 copies of the signed region**,
284    /// identical structure to `sign_digest_sha256` but using BLAKE3.
285    ///
286    /// Produces the same 32-byte output as SHA-256, so encoded packet size is identical.
287    ///
288    /// # Performance characteristics
289    /// BLAKE3 uses SIMD (NEON on ARM, AVX2/AVX-512 on x86) and tree parallelism for
290    /// large inputs. However, for the small per-packet payloads typical of NDN iperf
291    /// (< a few KB), tree parallelism never activates. On hardware with dedicated SHA
292    /// accelerators — ARM crypto extensions (Apple Silicon, Cortex-A) or Intel SHA-NI —
293    /// `ring`'s SHA-256 implementation uses single-cycle hardware instructions and will
294    /// match or beat BLAKE3 at these payload sizes. BLAKE3's advantage over SHA-256
295    /// is most visible on hardware without such extensions (older x86, RISC-V, embedded).
296    ///
297    /// # Note
298    /// Uses signature type code 6 (`DigestBlake3`), which is an experimental
299    /// NDA extension not yet in the NDN Packet Format specification.
300    #[cfg(feature = "std")]
301    pub fn sign_digest_blake3(self) -> Bytes {
302        use ndn_tlv::varu64_size;
303
304        // SignatureValue: type(1) + len(1) + BLAKE3(32) = 34 bytes — same as SHA-256.
305        const SIGVALUE: usize = 34;
306
307        let sz = FastPathSizes::compute(
308            &self.name,
309            self.freshness,
310            self.final_block_id.as_ref(),
311            &self.content,
312        );
313        let signed_size =
314            sz.name_tlv + sz.metainfo_tlv + sz.content_tlv + SIGINFO_DIGEST_BLAKE3.len();
315        let inner_size = signed_size + SIGVALUE;
316        let header_size = varu64_size(tlv_type::DATA) + varu64_size(inner_size as u64);
317
318        let mut buf = BytesMut::with_capacity(header_size + inner_size);
319        put_vu(&mut buf, tlv_type::DATA);
320        put_vu(&mut buf, inner_size as u64);
321
322        let signed_start = buf.len();
323        write_fields(
324            &mut buf,
325            &self.name,
326            self.freshness,
327            self.final_block_id.as_ref(),
328            &self.content,
329            &sz,
330        );
331        buf.put_slice(&SIGINFO_DIGEST_BLAKE3);
332        debug_assert_eq!(
333            buf.len() - signed_start,
334            signed_size,
335            "signed region size mismatch"
336        );
337
338        let hash = blake3::hash(&buf[signed_start..]);
339        buf.put_slice(&[0x17u8, 0x20]);
340        buf.put_slice(hash.as_bytes());
341        debug_assert_eq!(buf.len(), header_size + inner_size, "total size mismatch");
342
343        buf.freeze()
344    }
345
346    /// Build a Data packet with **no** signature fields (no SignatureInfo, no SignatureValue).
347    ///
348    /// Single-buffer fast path with **1 allocation, 0 crypto overhead**.
349    ///
350    /// # ⚠ Non-conformant NDN
351    /// All conformant NDN Data packets must carry a signature.  Packets produced
352    /// by this method will be **rejected by validators** unless validation is
353    /// explicitly bypassed (e.g., `FlowSignMode::None` in iperf for benchmarking).
354    /// Do not use in production data planes.
355    pub fn sign_none(self) -> Bytes {
356        use ndn_tlv::varu64_size;
357
358        let sz = FastPathSizes::compute(
359            &self.name,
360            self.freshness,
361            self.final_block_id.as_ref(),
362            &self.content,
363        );
364        let inner_size = sz.name_tlv + sz.metainfo_tlv + sz.content_tlv;
365        let header_size = varu64_size(tlv_type::DATA) + varu64_size(inner_size as u64);
366
367        let mut buf = BytesMut::with_capacity(header_size + inner_size);
368        put_vu(&mut buf, tlv_type::DATA);
369        put_vu(&mut buf, inner_size as u64);
370        write_fields(
371            &mut buf,
372            &self.name,
373            self.freshness,
374            self.final_block_id.as_ref(),
375            &self.content,
376            &sz,
377        );
378        buf.freeze()
379    }
380
381    /// Build unsigned Data with a DigestSha256 placeholder signature.
382    pub fn build(self) -> Bytes {
383        let mut w = TlvWriter::new();
384        w.write_nested(tlv_type::DATA, |w| {
385            write_name(w, &self.name);
386            if self.freshness.is_some() || self.final_block_id.is_some() {
387                let freshness = self.freshness;
388                let fbi = self.final_block_id.as_deref();
389                w.write_nested(tlv_type::META_INFO, |w| {
390                    if let Some(f) = freshness {
391                        write_nni(w, tlv_type::FRESHNESS_PERIOD, f.as_millis() as u64);
392                    }
393                    if let Some(fb) = fbi {
394                        w.write_tlv(tlv_type::FINAL_BLOCK_ID, fb);
395                    }
396                });
397            }
398            w.write_tlv(tlv_type::CONTENT, &self.content);
399            w.write_nested(tlv_type::SIGNATURE_INFO, |w| {
400                w.write_tlv(tlv_type::SIGNATURE_TYPE, &[0u8]);
401            });
402            w.write_tlv(tlv_type::SIGNATURE_VALUE, &[0u8; 32]);
403        });
404        w.finish()
405    }
406
407    /// Encode and sign the Data packet.
408    ///
409    /// `sig_type` and `key_locator` describe the signature algorithm and
410    /// optional KeyLocator name (for SignatureInfo). `sign_fn` receives the
411    /// signed region (Name + MetaInfo + Content + SignatureInfo) and returns
412    /// the raw signature value bytes.
413    pub async fn sign<F, Fut>(
414        self,
415        sig_type: SignatureType,
416        key_locator: Option<&Name>,
417        sign_fn: F,
418    ) -> Bytes
419    where
420        F: FnOnce(&[u8]) -> Fut,
421        Fut: std::future::Future<Output = Bytes>,
422    {
423        // Build Name + MetaInfo (if needed) + Content.
424        let mut inner = TlvWriter::new();
425        write_name(&mut inner, &self.name);
426        if self.freshness.is_some() || self.final_block_id.is_some() {
427            let freshness = self.freshness;
428            let fbi = self.final_block_id.as_deref();
429            inner.write_nested(tlv_type::META_INFO, |w| {
430                if let Some(f) = freshness {
431                    write_nni(w, tlv_type::FRESHNESS_PERIOD, f.as_millis() as u64);
432                }
433                if let Some(fb) = fbi {
434                    w.write_tlv(tlv_type::FINAL_BLOCK_ID, fb);
435                }
436            });
437        }
438        inner.write_tlv(tlv_type::CONTENT, &self.content);
439        let inner_bytes = inner.finish();
440
441        // Build SignatureInfo.
442        let mut sig_info_writer = TlvWriter::new();
443        sig_info_writer.write_nested(tlv_type::SIGNATURE_INFO, |w| {
444            write_nni(w, tlv_type::SIGNATURE_TYPE, sig_type.code());
445            if let Some(kl_name) = key_locator {
446                w.write_nested(tlv_type::KEY_LOCATOR, |w| {
447                    write_name(w, kl_name);
448                });
449            }
450        });
451        let sig_info_bytes = sig_info_writer.finish();
452
453        // Signed region = Name + MetaInfo + Content + SignatureInfo.
454        let mut signed_region = Vec::with_capacity(inner_bytes.len() + sig_info_bytes.len());
455        signed_region.extend_from_slice(&inner_bytes);
456        signed_region.extend_from_slice(&sig_info_bytes);
457
458        // Sign the region.
459        let sig_value = sign_fn(&signed_region).await;
460
461        // Assemble the full Data packet.
462        let mut w = TlvWriter::new();
463        w.write_nested(tlv_type::DATA, |w| {
464            w.write_raw(&signed_region);
465            w.write_tlv(tlv_type::SIGNATURE_VALUE, &sig_value);
466        });
467        w.finish()
468    }
469
470    /// Synchronous encode-and-sign using a single pre-sized buffer.
471    ///
472    /// Avoids the three intermediate allocations of the async `sign()` path.
473    /// `sign_fn` receives the signed region (Name + MetaInfo + Content +
474    /// SignatureInfo) and must return the raw signature bytes.
475    pub fn sign_sync<F>(
476        self,
477        sig_type: SignatureType,
478        key_locator: Option<&Name>,
479        sign_fn: F,
480    ) -> Bytes
481    where
482        F: FnOnce(&[u8]) -> Bytes,
483    {
484        // Estimate total size: name + metainfo + content + siginfo + sigvalue + outer TLV.
485        // Over-estimate is fine — BytesMut won't reallocate.
486        let est = self.content.len() + 256;
487        let mut w = TlvWriter::with_capacity(est);
488
489        // Build the signed region (Name + MetaInfo + Content + SignatureInfo)
490        // into the writer, then snapshot it for signing.
491        let signed_start = w.len();
492        write_name(&mut w, &self.name);
493        if self.freshness.is_some() || self.final_block_id.is_some() {
494            let freshness = self.freshness;
495            let fbi = self.final_block_id.as_deref();
496            w.write_nested(tlv_type::META_INFO, |w| {
497                if let Some(f) = freshness {
498                    write_nni(w, tlv_type::FRESHNESS_PERIOD, f.as_millis() as u64);
499                }
500                if let Some(fb) = fbi {
501                    w.write_tlv(tlv_type::FINAL_BLOCK_ID, fb);
502                }
503            });
504        }
505        w.write_tlv(tlv_type::CONTENT, &self.content);
506        w.write_nested(tlv_type::SIGNATURE_INFO, |w| {
507            write_nni(w, tlv_type::SIGNATURE_TYPE, sig_type.code());
508            if let Some(kl_name) = key_locator {
509                w.write_nested(tlv_type::KEY_LOCATOR, |w| {
510                    write_name(w, kl_name);
511                });
512            }
513        });
514        // Sign the region — zero-copy slice, no Vec allocation.
515        let sig_value = sign_fn(w.slice_from(signed_start));
516
517        // Wrap everything in the outer Data TLV.
518        let signed_region = w.slice_from(signed_start);
519        let inner_len = signed_region.len()
520            + ndn_tlv::varu64_size(tlv_type::SIGNATURE_VALUE)
521            + ndn_tlv::varu64_size(sig_value.len() as u64)
522            + sig_value.len();
523        let mut outer = TlvWriter::with_capacity(inner_len + 10);
524        outer.write_varu64(tlv_type::DATA);
525        outer.write_varu64(inner_len as u64);
526        outer.write_raw(signed_region);
527        outer.write_tlv(tlv_type::SIGNATURE_VALUE, &sig_value);
528        outer.finish()
529    }
530}
531
532// ─── Helpers ─────────────────────────────────────────────────────────────────
533
534/// Encode a non-negative integer as a minimal big-endian byte string (no leading zeros,
535/// except that 0 encodes as a single 0x00 byte). Used for typed name components.
536fn encode_nni_be(v: u64) -> Vec<u8> {
537    if v == 0 {
538        return vec![0x00];
539    }
540    let bytes = v.to_be_bytes();
541    let first_nonzero = bytes.iter().position(|&b| b != 0).unwrap_or(7);
542    bytes[first_nonzero..].to_vec()
543}
544
545// ─── Tests ────────────────────────────────────────────────────────────────────
546
547#[cfg(test)]
548mod tests {
549    use super::super::tests::{assert_bytes_eq, hex};
550    use super::*;
551    use crate::Data;
552    use bytes::Bytes;
553    use std::time::Duration;
554
555    #[test]
556    fn data_builder_basic() {
557        let wire = DataBuilder::new("/test", b"hello").build();
558        let data = Data::decode(wire).unwrap();
559        assert_eq!(data.name.to_string(), "/test");
560        assert_eq!(data.content().map(|b| b.as_ref()), Some(b"hello".as_ref()));
561    }
562
563    #[test]
564    fn data_builder_freshness() {
565        let wire = DataBuilder::new("/test", b"x")
566            .freshness(Duration::from_secs(60))
567            .build();
568        let data = Data::decode(wire).unwrap();
569        let mi = data.meta_info().expect("meta_info present");
570        assert_eq!(mi.freshness_period, Some(Duration::from_secs(60)));
571    }
572
573    #[test]
574    fn data_builder_sign() {
575        use std::pin::pin;
576        use std::task::{Context, Wake, Waker};
577
578        struct NoopWaker;
579        impl Wake for NoopWaker {
580            fn wake(self: std::sync::Arc<Self>) {}
581        }
582        let waker = Waker::from(std::sync::Arc::new(NoopWaker));
583        let mut cx = Context::from_waker(&waker);
584
585        let key_name: Name = "/key/test".parse().unwrap();
586        let fut = DataBuilder::new("/signed/data", b"payload")
587            .freshness(Duration::from_secs(10))
588            .sign(
589                SignatureType::SignatureEd25519,
590                Some(&key_name),
591                |region: &[u8]| {
592                    let digest = ring::digest::digest(&ring::digest::SHA256, region);
593                    std::future::ready(Bytes::copy_from_slice(digest.as_ref()))
594                },
595            );
596        let mut fut = pin!(fut);
597        let wire = match fut.as_mut().poll(&mut cx) {
598            std::task::Poll::Ready(b) => b,
599            std::task::Poll::Pending => panic!("sign future should complete immediately"),
600        };
601
602        let data = Data::decode(wire).unwrap();
603        assert_eq!(data.name.to_string(), "/signed/data");
604        assert_eq!(
605            data.content().map(|b| b.as_ref()),
606            Some(b"payload".as_ref())
607        );
608
609        let si = data.sig_info().expect("sig info");
610        assert_eq!(si.sig_type, SignatureType::SignatureEd25519);
611        let kl = si.key_locator.clone().expect("key locator");
612        assert_eq!(kl.to_string(), "/key/test");
613    }
614
615    #[test]
616    fn data_builder_sign_sync_matches_async() {
617        use std::pin::pin;
618        use std::task::{Context, Wake, Waker};
619
620        let key_name: Name = "/key/test".parse().unwrap();
621        let sign_fn = |region: &[u8]| -> Bytes {
622            let digest = ring::digest::digest(&ring::digest::SHA256, region);
623            Bytes::copy_from_slice(digest.as_ref())
624        };
625
626        // Async path
627        struct NoopWaker;
628        impl Wake for NoopWaker {
629            fn wake(self: std::sync::Arc<Self>) {}
630        }
631        let waker = Waker::from(std::sync::Arc::new(NoopWaker));
632        let mut cx = Context::from_waker(&waker);
633
634        let fut = DataBuilder::new("/signed/data", b"payload")
635            .freshness(Duration::from_secs(10))
636            .sign(
637                SignatureType::SignatureEd25519,
638                Some(&key_name),
639                |region: &[u8]| {
640                    let digest = ring::digest::digest(&ring::digest::SHA256, region);
641                    std::future::ready(Bytes::copy_from_slice(digest.as_ref()))
642                },
643            );
644        let mut fut = pin!(fut);
645        let async_wire = match fut.as_mut().poll(&mut cx) {
646            std::task::Poll::Ready(b) => b,
647            std::task::Poll::Pending => panic!("should complete immediately"),
648        };
649
650        // Sync path
651        let sync_wire = DataBuilder::new("/signed/data", b"payload")
652            .freshness(Duration::from_secs(10))
653            .sign_sync(SignatureType::SignatureEd25519, Some(&key_name), sign_fn);
654
655        assert_eq!(
656            async_wire, sync_wire,
657            "sign_sync must produce identical wire format"
658        );
659    }
660
661    #[test]
662    fn data_builder_sign_sync_no_freshness() {
663        let wire = DataBuilder::new("/test", b"content").sign_sync(
664            SignatureType::SignatureEd25519,
665            None,
666            |region| {
667                let digest = ring::digest::digest(&ring::digest::SHA256, region);
668                Bytes::copy_from_slice(digest.as_ref())
669            },
670        );
671        let data = Data::decode(wire).unwrap();
672        assert_eq!(data.name.to_string(), "/test");
673        assert_eq!(
674            data.content().map(|b| b.as_ref()),
675            Some(b"content".as_ref())
676        );
677        assert!(data.meta_info().is_none());
678        let si = data.sig_info().expect("sig info");
679        assert_eq!(si.sig_type, SignatureType::SignatureEd25519);
680    }
681
682    // ── Wire-format tests ────────────────────────────────────────────────────
683
684    #[test]
685    fn wire_data_builder_no_freshness_omits_metainfo() {
686        let wire = DataBuilder::new("/A", b"X").build();
687
688        assert_eq!(wire[0], 0x06);
689
690        // After Name (07 03 08 01 41), next should be Content (15), not MetaInfo (14).
691        assert_eq!(
692            wire[7], 0x15,
693            "Content should follow Name directly (no MetaInfo)"
694        );
695    }
696
697    #[test]
698    fn wire_data_builder_freshness_nni() {
699        // 10 seconds = 10000ms = 0x2710 → 2-byte NNI
700        let wire = DataBuilder::new("/A", b"X")
701            .freshness(Duration::from_secs(10))
702            .build();
703
704        // MetaInfo: 14 04 19 02 27 10
705        let meta_pos = 7; // after Name
706        assert_bytes_eq(
707            &wire[meta_pos..meta_pos + 6],
708            &[0x14, 0x04, 0x19, 0x02, 0x27, 0x10],
709            "MetaInfo with FreshnessPeriod=10000ms",
710        );
711    }
712
713    #[test]
714    fn wire_ed25519_sig_type() {
715        use std::pin::pin;
716        use std::task::{Context, Wake, Waker};
717
718        struct NoopWaker;
719        impl Wake for NoopWaker {
720            fn wake(self: std::sync::Arc<Self>) {}
721        }
722        let waker = Waker::from(std::sync::Arc::new(NoopWaker));
723        let mut cx = Context::from_waker(&waker);
724
725        let fut = DataBuilder::new("/A", b"X").sign(
726            SignatureType::SignatureEd25519,
727            None,
728            |_: &[u8]| std::future::ready(Bytes::from_static(&[0xFF; 64])),
729        );
730        let mut fut = pin!(fut);
731        let wire = match fut.as_mut().poll(&mut cx) {
732            std::task::Poll::Ready(b) => b,
733            std::task::Poll::Pending => panic!("should complete immediately"),
734        };
735
736        // SignatureInfo should contain: 1B 01 05 (SignatureType=5, 1-byte NNI)
737        let sig_info_content = [0x1B, 0x01, 0x05];
738        assert!(
739            wire.windows(3).any(|w| w == sig_info_content),
740            "SignatureType=5 should be 1-byte NNI: 1B 01 05, got: {}",
741            hex(&wire),
742        );
743    }
744}