Skip to main content

ipfrs_cli/commands/
ipld.rs

1//! IPLD path resolution and DAG inspection CLI commands.
2//!
3//! Implements three subcommands under `ipfrs ipld`:
4//!
5//! - `resolve <path>` — resolve an `/ipld/<cid>/...` path and print the value
6//! - `stat <cid>`     — print codec, size, and link count for a CID
7//! - `links <cid>`    — list every CID linked from a given block
8//!
9//! The additional `dag_cli` helpers (`dag_stat`, `dag_export`, `dag_import`)
10//! are exposed from this module for use by the `ipfrs dag` command group.
11
12use anyhow::{anyhow, Result};
13use std::path::Path;
14
15use crate::commands::query::OutputFormat;
16
17// ─── Path parsing ────────────────────────────────────────────────────────────
18
19/// Parse an `/ipld/<cid-string>[/seg1/seg2/…]` path into its components.
20///
21/// Returns `(cid_string, path_segments)` on success.
22///
23/// # Errors
24/// Returns an error when the path does not start with `/ipld/` or when the
25/// CID segment is missing.
26pub fn parse_ipld_path(path: &str) -> Result<(String, Vec<String>)> {
27    // Strip optional leading slash and split on '/'
28    let stripped = path.trim_start_matches('/');
29    let mut parts: Vec<&str> = stripped.split('/').filter(|s| !s.is_empty()).collect();
30
31    if parts.is_empty() {
32        return Err(anyhow!("Empty path"));
33    }
34
35    if parts[0] != "ipld" {
36        return Err(anyhow!("Path must start with /ipld/, got: /{}", parts[0]));
37    }
38
39    // Remove the "ipld" prefix segment
40    parts.remove(0);
41
42    if parts.is_empty() {
43        return Err(anyhow!("Path is missing the CID segment: {}", path));
44    }
45
46    let cid_str = parts.remove(0).to_string();
47    let segments: Vec<String> = parts.iter().map(|s| s.to_string()).collect();
48
49    Ok((cid_str, segments))
50}
51
52// ─── IPLD value display ───────────────────────────────────────────────────────
53
54/// Walk an `ipfrs_core::Ipld` tree following `segments` and return the leaf.
55fn traverse_ipld<'a>(
56    node: &'a ipfrs_core::Ipld,
57    segments: &[String],
58) -> Result<&'a ipfrs_core::Ipld> {
59    if segments.is_empty() {
60        return Ok(node);
61    }
62
63    let seg = &segments[0];
64    let rest = &segments[1..];
65
66    match node {
67        ipfrs_core::Ipld::Map(map) => {
68            let child = map
69                .get(seg.as_str())
70                .ok_or_else(|| anyhow!("Key '{}' not found in IPLD map", seg))?;
71            traverse_ipld(child, rest)
72        }
73        ipfrs_core::Ipld::List(list) => {
74            let idx: usize = seg
75                .parse()
76                .map_err(|_| anyhow!("Expected numeric index for list, got '{}'", seg))?;
77            let child = list
78                .get(idx)
79                .ok_or_else(|| anyhow!("Index {} out of bounds (len {})", idx, list.len()))?;
80            traverse_ipld(child, rest)
81        }
82        other => Err(anyhow!(
83            "Cannot descend into {:?} with segment '{}'",
84            std::mem::discriminant(other),
85            seg
86        )),
87    }
88}
89
90/// Render an `ipfrs_core::Ipld` value as a `serde_json::Value` for display.
91fn ipld_to_json(ipld: &ipfrs_core::Ipld) -> serde_json::Value {
92    match ipld {
93        ipfrs_core::Ipld::Null => serde_json::Value::Null,
94        ipfrs_core::Ipld::Bool(b) => serde_json::Value::Bool(*b),
95        ipfrs_core::Ipld::Integer(n) => serde_json::json!(*n),
96        ipfrs_core::Ipld::Float(f) => serde_json::json!(*f),
97        ipfrs_core::Ipld::String(s) => serde_json::Value::String(s.clone()),
98        ipfrs_core::Ipld::Bytes(b) => {
99            // Encode bytes as base64 in a DAG-JSON style object
100            use std::fmt::Write;
101            let mut hex = String::with_capacity(b.len() * 2);
102            for byte in b {
103                write!(hex, "{:02x}", byte).ok();
104            }
105            serde_json::json!({ "bytes": hex })
106        }
107        ipfrs_core::Ipld::List(items) => {
108            serde_json::Value::Array(items.iter().map(ipld_to_json).collect())
109        }
110        ipfrs_core::Ipld::Map(map) => {
111            let obj: serde_json::Map<String, serde_json::Value> = map
112                .iter()
113                .map(|(k, v)| (k.clone(), ipld_to_json(v)))
114                .collect();
115            serde_json::Value::Object(obj)
116        }
117        ipfrs_core::Ipld::Link(cid) => {
118            serde_json::json!({ "/": cid.0.to_string() })
119        }
120    }
121}
122
123// ─── ipld resolve ─────────────────────────────────────────────────────────────
124
125/// Resolve an IPLD path and print the value.
126///
127/// `path` must follow the format `/ipld/<cid-string>/field/subfield/0`.
128/// The block is fetched from local storage, decoded as DAG-CBOR, and the
129/// given sub-path is traversed.
130pub async fn ipld_resolve(path: &str, format: &OutputFormat) -> Result<()> {
131    use ipfrs::{Node, NodeConfig};
132    use ipfrs_core::Cid;
133
134    let (cid_str, segments) = parse_ipld_path(path)?;
135
136    let cid = cid_str
137        .parse::<Cid>()
138        .map_err(|e| anyhow!("Invalid CID '{}': {}", cid_str, e))?;
139
140    let mut node = Node::new(NodeConfig::default())?;
141    node.start().await?;
142
143    let raw = node
144        .get_block_raw(&cid)
145        .await?
146        .ok_or_else(|| anyhow!("Block not found: {}", cid))?;
147
148    // Decode as DAG-CBOR
149    let ipld = ipfrs_core::Ipld::from_dag_cbor(&raw)
150        .map_err(|e| anyhow!("Failed to decode block as DAG-CBOR: {}", e))?;
151
152    let leaf = traverse_ipld(&ipld, &segments)?;
153    let json_val = ipld_to_json(leaf);
154
155    node.stop().await?;
156
157    match format {
158        OutputFormat::Json => {
159            println!("{}", serde_json::to_string_pretty(&json_val)?);
160        }
161        OutputFormat::Text => {
162            // Pretty-print but without the JSON wrapper for scalar types
163            match &json_val {
164                serde_json::Value::String(s) => println!("{}", s),
165                serde_json::Value::Number(n) => println!("{}", n),
166                serde_json::Value::Bool(b) => println!("{}", b),
167                serde_json::Value::Null => println!("null"),
168                other => println!("{}", serde_json::to_string_pretty(other)?),
169            }
170        }
171    }
172
173    Ok(())
174}
175
176// ─── ipld stat ────────────────────────────────────────────────────────────────
177
178/// Print metadata about a CID: codec, size, links count.
179pub async fn ipld_stat(cid_str: &str, format: &OutputFormat) -> Result<()> {
180    use ipfrs::{Node, NodeConfig};
181    use ipfrs_core::Cid;
182
183    let cid = cid_str
184        .parse::<Cid>()
185        .map_err(|e| anyhow!("Invalid CID '{}': {}", cid_str, e))?;
186
187    let mut node = Node::new(NodeConfig::default())?;
188    node.start().await?;
189
190    let raw = node
191        .get_block_raw(&cid)
192        .await?
193        .ok_or_else(|| anyhow!("Block not found: {}", cid))?;
194
195    let size = raw.len();
196    let codec_code = cid.codec();
197    let codec_name = codec_name_for(codec_code);
198
199    // Count links by trying to decode as DAG-CBOR
200    let links_count = match ipfrs_core::Ipld::from_dag_cbor(&raw) {
201        Ok(ipld) => ipld.links().len(),
202        Err(_) => 0,
203    };
204
205    node.stop().await?;
206
207    match format {
208        OutputFormat::Json => {
209            let obj = serde_json::json!({
210                "cid": cid.to_string(),
211                "size": size,
212                "codec": codec_name,
213                "codec_code": codec_code,
214                "links": links_count,
215            });
216            println!("{}", serde_json::to_string_pretty(&obj)?);
217        }
218        OutputFormat::Text => {
219            use crate::output::{print_header, print_kv};
220            print_header("IPLD Block Stat");
221            print_kv("CID", &cid.to_string());
222            print_kv("Size", &format!("{} bytes", size));
223            print_kv("Codec", &format!("{} (0x{:x})", codec_name, codec_code));
224            print_kv("Links", &links_count.to_string());
225        }
226    }
227
228    Ok(())
229}
230
231// ─── ipld links ───────────────────────────────────────────────────────────────
232
233/// List all CIDs linked from a given CID.
234pub async fn ipld_links(cid_str: &str, format: &OutputFormat) -> Result<()> {
235    use ipfrs::{Node, NodeConfig};
236    use ipfrs_core::Cid;
237
238    let cid = cid_str
239        .parse::<Cid>()
240        .map_err(|e| anyhow!("Invalid CID '{}': {}", cid_str, e))?;
241
242    let mut node = Node::new(NodeConfig::default())?;
243    node.start().await?;
244
245    let raw = node
246        .get_block_raw(&cid)
247        .await?
248        .ok_or_else(|| anyhow!("Block not found: {}", cid))?;
249
250    let ipld = ipfrs_core::Ipld::from_dag_cbor(&raw)
251        .map_err(|e| anyhow!("Failed to decode block as DAG-CBOR: {}", e))?;
252
253    let links: Vec<String> = ipld.links().iter().map(|c| c.to_string()).collect();
254
255    node.stop().await?;
256
257    match format {
258        OutputFormat::Json => {
259            let arr: serde_json::Value = links
260                .iter()
261                .map(|s| serde_json::json!({ "/": s }))
262                .collect::<Vec<_>>()
263                .into();
264            println!("{}", serde_json::to_string_pretty(&arr)?);
265        }
266        OutputFormat::Text => {
267            if links.is_empty() {
268                println!("No links found in block {}", cid);
269            } else {
270                for link in &links {
271                    println!("{}", link);
272                }
273            }
274        }
275    }
276
277    Ok(())
278}
279
280// ─── dag_cli helpers ──────────────────────────────────────────────────────────
281
282/// Statistics returned after a DAG import operation.
283#[derive(Debug, Default, Clone)]
284pub struct ImportStats {
285    pub blocks_imported: usize,
286    pub bytes_imported: u64,
287}
288
289/// Print DAG node stats: CID, size, links, codec.
290///
291/// This mirrors `ipld_stat` but is exposed under the `dag` command group.
292pub async fn dag_stat(cid_str: &str, format: &OutputFormat) -> Result<()> {
293    // Delegate to ipld_stat — identical semantics
294    ipld_stat(cid_str, format).await
295}
296
297/// Export a DAG sub-graph as a CAR v1 stream.
298///
299/// When `output` is `None`, the CAR bytes are written to stdout.
300/// Traversal is breadth-first; only locally available blocks are included.
301///
302/// CAR v1 format (simplified, no compression):
303/// ```text
304/// <varint: header-len> <dag-cbor-header> <blocks…>
305/// ```
306/// Each block:
307/// ```text
308/// <varint: cid-len + data-len> <cid-bytes> <data-bytes>
309/// ```
310pub async fn dag_export(cid_str: &str, output: Option<&Path>) -> Result<()> {
311    use ipfrs::{Node, NodeConfig};
312    use ipfrs_core::Cid;
313    use std::io::Write;
314
315    let root = cid_str
316        .parse::<Cid>()
317        .map_err(|e| anyhow!("Invalid CID '{}': {}", cid_str, e))?;
318
319    let mut node = Node::new(NodeConfig::default())?;
320    node.start().await?;
321
322    // Breadth-first traversal collecting (cid, raw_bytes) pairs
323    let mut visited: std::collections::HashSet<Cid> = std::collections::HashSet::new();
324    let mut queue: std::collections::VecDeque<Cid> = std::collections::VecDeque::new();
325    let mut blocks: Vec<(Cid, Vec<u8>)> = Vec::new();
326
327    queue.push_back(root);
328    visited.insert(root);
329
330    while let Some(cid) = queue.pop_front() {
331        let raw = match node.get_block_raw(&cid).await? {
332            Some(r) => r,
333            None => {
334                // Skip unavailable blocks (warn but continue)
335                eprintln!("Warning: block {} not available locally, skipping", cid);
336                continue;
337            }
338        };
339
340        // Discover child links if decodable as DAG-CBOR
341        if let Ok(ipld) = ipfrs_core::Ipld::from_dag_cbor(&raw) {
342            for link in ipld.links() {
343                if visited.insert(link) {
344                    queue.push_back(link);
345                }
346            }
347        }
348
349        blocks.push((cid, raw));
350    }
351
352    node.stop().await?;
353
354    // Build CAR v1 payload in memory
355    let car_bytes = build_car_v1(&root, &blocks)?;
356
357    // Write output
358    match output {
359        Some(path) => {
360            tokio::fs::write(path, &car_bytes).await?;
361            eprintln!(
362                "Exported {} blocks ({} bytes) to {}",
363                blocks.len(),
364                car_bytes.len(),
365                path.display()
366            );
367        }
368        None => {
369            std::io::stdout()
370                .write_all(&car_bytes)
371                .map_err(|e| anyhow!("Failed to write CAR to stdout: {}", e))?;
372        }
373    }
374
375    Ok(())
376}
377
378/// Import blocks from a CAR file into local block storage.
379///
380/// Supports CAR v1 format (the simplest widely-used variant).
381pub async fn dag_import(input: &Path) -> Result<ImportStats> {
382    use ipfrs::{Node, NodeConfig};
383
384    let car_bytes = tokio::fs::read(input)
385        .await
386        .map_err(|e| anyhow!("Failed to read {}: {}", input.display(), e))?;
387
388    let mut node = Node::new(NodeConfig::default())?;
389    node.start().await?;
390
391    let mut stats = ImportStats::default();
392    let mut cursor: usize = 0;
393
394    // Skip CAR v1 header (DAG-CBOR encoded header — we only need to advance past it)
395    let (_header_len, _header_bytes) = read_varint_and_data(&car_bytes, &mut cursor)?;
396
397    // Read blocks until EOF
398    while cursor < car_bytes.len() {
399        let (block_len, _) = read_varint_prefix_len(&car_bytes, &mut cursor)?;
400        if block_len == 0 {
401            break;
402        }
403
404        let block_start = cursor;
405        let block_end = block_start + block_len;
406
407        if block_end > car_bytes.len() {
408            return Err(anyhow!(
409                "CAR file truncated: expected {} bytes at offset {}",
410                block_len,
411                cursor
412            ));
413        }
414
415        // CID bytes: encoded CID, length determined by parsing
416        let (cid, cid_len) = parse_cid_bytes(&car_bytes[block_start..block_end])?;
417        let data_start = block_start + cid_len;
418        let data = car_bytes[data_start..block_end].to_vec();
419        let data_len = data.len() as u64;
420
421        node.put_block_raw(data)
422            .await
423            .map_err(|e| anyhow!("Failed to store block {}: {}", cid, e))?;
424
425        stats.blocks_imported += 1;
426        stats.bytes_imported += data_len;
427
428        cursor = block_end;
429    }
430
431    node.stop().await?;
432
433    Ok(stats)
434}
435
436// ─── CAR v1 helpers ───────────────────────────────────────────────────────────
437
438/// Encode a single unsigned varint and append it to `buf`.
439fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
440    loop {
441        let byte = (value & 0x7f) as u8;
442        value >>= 7;
443        if value == 0 {
444            buf.push(byte);
445            break;
446        } else {
447            buf.push(byte | 0x80);
448        }
449    }
450}
451
452/// Build a minimal CAR v1 payload from a list of `(cid, raw_bytes)` blocks.
453fn build_car_v1(root: &ipfrs_core::Cid, blocks: &[(ipfrs_core::Cid, Vec<u8>)]) -> Result<Vec<u8>> {
454    let mut out = Vec::new();
455
456    // --- Header (DAG-CBOR encoded map: {"version": 1, "roots": [<cid-link>]}) ---
457    // Encode a minimal CAR header: {"version":1,"roots":[/<root-cid>]}
458    let root_cid_bytes = cid_to_bytes(root)?;
459    // DAG-CBOR encoding of the header map: hand-crafted for simplicity
460    let header_cbor = build_car_header_cbor(&root_cid_bytes)?;
461    write_varint(&mut out, header_cbor.len() as u64);
462    out.extend_from_slice(&header_cbor);
463
464    // --- Blocks ---
465    for (cid, data) in blocks {
466        let cid_bytes = cid_to_bytes(cid)?;
467        let block_len = cid_bytes.len() + data.len();
468        write_varint(&mut out, block_len as u64);
469        out.extend_from_slice(&cid_bytes);
470        out.extend_from_slice(data);
471    }
472
473    Ok(out)
474}
475
476/// Serialize a CID into its binary representation (multihash-encoded CIDv1).
477fn cid_to_bytes(cid: &ipfrs_core::Cid) -> Result<Vec<u8>> {
478    // cid crate provides `.to_bytes()` which gives the standard binary encoding
479    Ok(cid.to_bytes())
480}
481
482/// Build a minimal DAG-CBOR header for a CAR v1 file.
483///
484/// Structure: `{"roots": [<cid-link>], "version": 1}`
485///
486/// DAG-CBOR:
487/// - map(2) = 0xa2
488/// - "roots" key + array of 1 CID tag(42) + bytes
489/// - "version" key + uint(1)
490fn build_car_header_cbor(root_cid_bytes: &[u8]) -> Result<Vec<u8>> {
491    let mut buf = Vec::new();
492
493    // map of 2 entries: 0xa2
494    buf.push(0xa2);
495
496    // key: "roots" (5 chars) → text(5) = 0x65, then bytes
497    buf.push(0x65);
498    buf.extend_from_slice(b"roots");
499
500    // value: array(1) = 0x81
501    buf.push(0x81);
502
503    // CID link: tag(42) = 0xd8 0x2a, then bytes(len) + CID bytes
504    // CBOR tag 42
505    buf.push(0xd8);
506    buf.push(42u8);
507
508    // The CID bytes are prefixed with 0x00 (multibase identity prefix for binary CIDs in CAR)
509    let cid_with_prefix = {
510        let mut v = vec![0u8]; // identity multibase prefix
511        v.extend_from_slice(root_cid_bytes);
512        v
513    };
514
515    // bytes(len)
516    encode_cbor_bytes_header(&mut buf, cid_with_prefix.len());
517    buf.extend_from_slice(&cid_with_prefix);
518
519    // key: "version" (7 chars) → text(7) = 0x67
520    buf.push(0x67);
521    buf.extend_from_slice(b"version");
522
523    // value: 1 → 0x01
524    buf.push(0x01);
525
526    Ok(buf)
527}
528
529/// Encode a CBOR byte string length header (major type 2).
530fn encode_cbor_bytes_header(buf: &mut Vec<u8>, len: usize) {
531    if len <= 23 {
532        buf.push(0x40 | len as u8);
533    } else if len <= 0xff {
534        buf.push(0x58);
535        buf.push(len as u8);
536    } else if len <= 0xffff {
537        buf.push(0x59);
538        buf.push((len >> 8) as u8);
539        buf.push(len as u8);
540    } else {
541        buf.push(0x5a);
542        buf.push((len >> 24) as u8);
543        buf.push((len >> 16) as u8);
544        buf.push((len >> 8) as u8);
545        buf.push(len as u8);
546    }
547}
548
549/// Read a varint-prefixed blob from `data` starting at `*cursor`.
550///
551/// Returns `(payload_length, payload_slice)` and advances `*cursor` past the payload.
552fn read_varint_and_data<'a>(data: &'a [u8], cursor: &mut usize) -> Result<(usize, &'a [u8])> {
553    let (len, n) = decode_varint(&data[*cursor..])
554        .ok_or_else(|| anyhow!("Truncated varint at offset {}", cursor))?;
555    *cursor += n;
556    let end = *cursor + len as usize;
557    if end > data.len() {
558        return Err(anyhow!("CAR payload truncated"));
559    }
560    let slice = &data[*cursor..end];
561    *cursor = end;
562    Ok((len as usize, slice))
563}
564
565/// Read the varint length prefix and advance `*cursor` past the varint only
566/// (not past the payload).  Returns `(payload_len, varint_byte_count)`.
567fn read_varint_prefix_len(data: &[u8], cursor: &mut usize) -> Result<(usize, usize)> {
568    let (len, n) = decode_varint(&data[*cursor..])
569        .ok_or_else(|| anyhow!("Truncated varint at offset {}", cursor))?;
570    *cursor += n;
571    Ok((len as usize, n))
572}
573
574/// Decode a unsigned varint from the beginning of `buf`.
575///
576/// Returns `(value, bytes_consumed)` or `None` on truncation.
577fn decode_varint(buf: &[u8]) -> Option<(u64, usize)> {
578    let mut value: u64 = 0;
579    let mut shift = 0u32;
580    for (i, &byte) in buf.iter().enumerate() {
581        value |= ((byte & 0x7f) as u64) << shift;
582        shift += 7;
583        if byte & 0x80 == 0 {
584            return Some((value, i + 1));
585        }
586        if shift >= 64 {
587            return None; // overflow
588        }
589    }
590    None
591}
592
593/// Parse the CID at the beginning of `block_data` and return `(cid, bytes_consumed)`.
594fn parse_cid_bytes(block_data: &[u8]) -> Result<(ipfrs_core::Cid, usize)> {
595    use ipfrs_core::Cid;
596    use std::io::Cursor;
597
598    // Try CIDv1 first (reads a varint-codec + varint-multihash)
599    let mut cur = Cursor::new(block_data);
600    let cid = Cid::read_bytes(&mut cur)
601        .map_err(|e| anyhow!("Failed to parse CID from CAR block: {}", e))?;
602    let consumed = cur.position() as usize;
603    Ok((cid, consumed))
604}
605
606// ─── Utility ─────────────────────────────────────────────────────────────────
607
608/// Return a human-readable codec name for common IPLD codec codes.
609fn codec_name_for(code: u64) -> &'static str {
610    match code {
611        0x55 => "raw",
612        0x70 => "dag-pb",
613        0x71 => "dag-cbor",
614        0x0129 => "dag-json",
615        _ => "unknown",
616    }
617}
618
619// ─── Tests ────────────────────────────────────────────────────────────────────
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    /// Parsing a well-formed `/ipld/<cid>/a/b/0` path must succeed and yield
626    /// the expected CID string and segment list.
627    #[test]
628    fn test_ipld_path_parse_valid() {
629        let (cid_str, segs) = parse_ipld_path("/ipld/bafkreihdwdcefgh48/a/b/0")
630            .expect("should parse valid ipld path");
631        assert_eq!(cid_str, "bafkreihdwdcefgh48");
632        assert_eq!(segs, vec!["a", "b", "0"]);
633    }
634
635    /// A path that does not begin with `/ipld/` must return an error.
636    #[test]
637    fn test_ipld_path_parse_missing_prefix() {
638        let err = parse_ipld_path("/rule/bafkreihdwdcefgh48/head").unwrap_err();
639        let msg = err.to_string();
640        assert!(
641            msg.contains("ipld"),
642            "Error message should mention 'ipld': {}",
643            msg
644        );
645    }
646
647    /// A path with only the `/ipld/` prefix but no CID must return an error.
648    #[test]
649    fn test_ipld_path_parse_missing_cid() {
650        let err = parse_ipld_path("/ipld/").unwrap_err();
651        let msg = err.to_string();
652        assert!(
653            msg.contains("CID") || msg.contains("cid") || msg.contains("missing"),
654            "Error should mention missing CID: {}",
655            msg
656        );
657    }
658
659    /// `ImportStats` with zero values should be constructible.
660    #[test]
661    fn test_import_stats_default() {
662        let stats = ImportStats::default();
663        assert_eq!(stats.blocks_imported, 0);
664        assert_eq!(stats.bytes_imported, 0);
665    }
666
667    /// Verify that the JSON output of `ipld_stat` would include the expected keys.
668    /// (Unit-level: we inspect the JSON object directly, no I/O needed.)
669    #[test]
670    fn test_dag_stat_format_json_keys() {
671        // Build the expected JSON object structure
672        let obj = serde_json::json!({
673            "cid": "bafkreitest",
674            "size": 42usize,
675            "codec": "dag-cbor",
676            "codec_code": 0x71u64,
677            "links": 0usize,
678        });
679
680        assert!(obj.get("cid").is_some(), "must have 'cid' key");
681        assert!(obj.get("size").is_some(), "must have 'size' key");
682        assert!(obj.get("links").is_some(), "must have 'links' key");
683    }
684
685    /// Verify varint encoding round-trips for small and large values.
686    #[test]
687    fn test_varint_roundtrip() {
688        for &val in &[0u64, 1, 127, 128, 255, 300, 16383, 16384, u32::MAX as u64] {
689            let mut buf = Vec::new();
690            write_varint(&mut buf, val);
691            let (decoded, consumed) = decode_varint(&buf).expect("should decode");
692            assert_eq!(decoded, val, "roundtrip failed for {}", val);
693            assert_eq!(
694                consumed,
695                buf.len(),
696                "consumed wrong number of bytes for {}",
697                val
698            );
699        }
700    }
701
702    /// `codec_name_for` must return known names for common codec codes.
703    #[test]
704    fn test_codec_name_known_codes() {
705        assert_eq!(codec_name_for(0x55), "raw");
706        assert_eq!(codec_name_for(0x70), "dag-pb");
707        assert_eq!(codec_name_for(0x71), "dag-cbor");
708        assert_eq!(codec_name_for(0xdeadbeef), "unknown");
709    }
710}