1use anyhow::{anyhow, Result};
13use std::path::Path;
14
15use crate::commands::query::OutputFormat;
16
17pub fn parse_ipld_path(path: &str) -> Result<(String, Vec<String>)> {
27 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 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
52fn 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
90fn 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 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
123pub 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 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 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
176pub 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 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
231pub 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#[derive(Debug, Default, Clone)]
284pub struct ImportStats {
285 pub blocks_imported: usize,
286 pub bytes_imported: u64,
287}
288
289pub async fn dag_stat(cid_str: &str, format: &OutputFormat) -> Result<()> {
293 ipld_stat(cid_str, format).await
295}
296
297pub 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 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 eprintln!("Warning: block {} not available locally, skipping", cid);
336 continue;
337 }
338 };
339
340 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 let car_bytes = build_car_v1(&root, &blocks)?;
356
357 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
378pub 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 let (_header_len, _header_bytes) = read_varint_and_data(&car_bytes, &mut cursor)?;
396
397 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 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
436fn 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
452fn build_car_v1(root: &ipfrs_core::Cid, blocks: &[(ipfrs_core::Cid, Vec<u8>)]) -> Result<Vec<u8>> {
454 let mut out = Vec::new();
455
456 let root_cid_bytes = cid_to_bytes(root)?;
459 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 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
476fn cid_to_bytes(cid: &ipfrs_core::Cid) -> Result<Vec<u8>> {
478 Ok(cid.to_bytes())
480}
481
482fn build_car_header_cbor(root_cid_bytes: &[u8]) -> Result<Vec<u8>> {
491 let mut buf = Vec::new();
492
493 buf.push(0xa2);
495
496 buf.push(0x65);
498 buf.extend_from_slice(b"roots");
499
500 buf.push(0x81);
502
503 buf.push(0xd8);
506 buf.push(42u8);
507
508 let cid_with_prefix = {
510 let mut v = vec![0u8]; v.extend_from_slice(root_cid_bytes);
512 v
513 };
514
515 encode_cbor_bytes_header(&mut buf, cid_with_prefix.len());
517 buf.extend_from_slice(&cid_with_prefix);
518
519 buf.push(0x67);
521 buf.extend_from_slice(b"version");
522
523 buf.push(0x01);
525
526 Ok(buf)
527}
528
529fn 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
549fn 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
565fn 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
574fn 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; }
589 }
590 None
591}
592
593fn parse_cid_bytes(block_data: &[u8]) -> Result<(ipfrs_core::Cid, usize)> {
595 use ipfrs_core::Cid;
596 use std::io::Cursor;
597
598 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
606fn 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#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[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 #[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 #[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 #[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 #[test]
670 fn test_dag_stat_format_json_keys() {
671 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 #[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 #[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}