1pub fn cid_from_bytes(data: &[u8]) -> String {
32 use sha2::Digest;
33
34 let digest: [u8; 32] = sha2::Sha256::digest(data).into();
36
37 let mut multihash = Vec::with_capacity(34);
39 multihash.push(0x12u8);
40 multihash.push(0x20u8);
41 multihash.extend_from_slice(&digest);
42
43 let mut cid_bytes = Vec::with_capacity(36);
45 cid_bytes.push(0x01u8);
46 cid_bytes.push(0x55u8);
47 cid_bytes.extend_from_slice(&multihash);
48
49 let encoded = base32_lower(&cid_bytes);
51 format!("b{encoded}")
52}
53
54fn base32_lower(input: &[u8]) -> String {
56 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
57 let mut output = Vec::with_capacity((input.len() * 8).div_ceil(5));
58 let mut buffer: u64 = 0;
59 let mut bits_left: u32 = 0;
60
61 for &byte in input {
62 buffer = (buffer << 8) | u64::from(byte);
63 bits_left += 8;
64 while bits_left >= 5 {
65 bits_left -= 5;
66 let idx = ((buffer >> bits_left) & 0x1f) as usize;
67 output.push(ALPHABET[idx]);
68 }
69 }
70 if bits_left > 0 {
71 let idx = ((buffer << (5 - bits_left)) & 0x1f) as usize;
72 output.push(ALPHABET[idx]);
73 }
74
75 String::from_utf8(output).unwrap_or_default()
77}
78
79struct InMemoryStore {
84 blocks: std::collections::HashMap<String, Vec<u8>>,
85 total_bytes: usize,
86}
87
88impl InMemoryStore {
89 fn new() -> Self {
90 Self {
91 blocks: std::collections::HashMap::new(),
92 total_bytes: 0,
93 }
94 }
95
96 fn put(&mut self, data: &[u8]) -> String {
99 let cid = cid_from_bytes(data);
100 if !self.blocks.contains_key(&cid) {
101 self.total_bytes += data.len();
102 self.blocks.insert(cid.clone(), data.to_vec());
103 }
104 cid
105 }
106
107 fn get(&self, cid: &str) -> Option<&Vec<u8>> {
108 self.blocks.get(cid)
109 }
110
111 fn has(&self, cid: &str) -> bool {
112 self.blocks.contains_key(cid)
113 }
114
115 fn delete(&mut self, cid: &str) -> bool {
117 if let Some(data) = self.blocks.remove(cid) {
118 self.total_bytes = self.total_bytes.saturating_sub(data.len());
119 true
120 } else {
121 false
122 }
123 }
124
125 fn list(&self) -> Vec<String> {
126 self.blocks.keys().cloned().collect()
127 }
128
129 fn block_count(&self) -> usize {
130 self.blocks.len()
131 }
132
133 fn total_bytes(&self) -> usize {
134 self.total_bytes
135 }
136}
137
138#[cfg(feature = "napi")]
156#[napi_derive::napi]
157pub struct IpfrsClient {
158 data_dir: String,
159 store: std::sync::Mutex<InMemoryStore>,
160}
161
162#[cfg(not(feature = "napi"))]
165pub struct IpfrsClient {
166 data_dir: String,
167 store: std::sync::Mutex<InMemoryStore>,
168}
169
170impl IpfrsClient {
171 pub fn create(data_dir: String) -> Result<Self, ClientError> {
176 Ok(Self {
177 data_dir,
178 store: std::sync::Mutex::new(InMemoryStore::new()),
179 })
180 }
181
182 pub fn add_bytes_inner(&self, data: &[u8]) -> Result<String, ClientError> {
184 let mut store = self
185 .store
186 .lock()
187 .map_err(|_| ClientError::lock("add_bytes"))?;
188 Ok(store.put(data))
189 }
190
191 pub fn get_bytes_inner(&self, cid: &str) -> Result<Option<Vec<u8>>, ClientError> {
193 let store = self
194 .store
195 .lock()
196 .map_err(|_| ClientError::lock("get_bytes"))?;
197 Ok(store.get(cid).cloned())
198 }
199
200 pub fn has_inner(&self, cid: &str) -> Result<bool, ClientError> {
202 let store = self.store.lock().map_err(|_| ClientError::lock("has"))?;
203 Ok(store.has(cid))
204 }
205
206 pub fn list_cids_inner(&self) -> Result<Vec<String>, ClientError> {
208 let store = self
209 .store
210 .lock()
211 .map_err(|_| ClientError::lock("list_cids"))?;
212 Ok(store.list())
213 }
214
215 pub fn delete_inner(&self, cid: &str) -> Result<bool, ClientError> {
217 let mut store = self.store.lock().map_err(|_| ClientError::lock("delete"))?;
218 Ok(store.delete(cid))
219 }
220
221 pub fn version_inner(&self) -> String {
223 env!("CARGO_PKG_VERSION").to_string()
224 }
225
226 pub fn stats_inner(&self) -> Result<String, ClientError> {
228 let store = self.store.lock().map_err(|_| ClientError::lock("stats"))?;
229 let json = serde_json::json!({
230 "data_dir": self.data_dir,
231 "block_count": store.block_count(),
232 "total_bytes": store.total_bytes(),
233 "version": env!("CARGO_PKG_VERSION"),
234 });
235 serde_json::to_string(&json).map_err(|e| ClientError::serialise(e.to_string()))
236 }
237}
238
239#[cfg(feature = "napi")]
244#[napi_derive::napi]
245impl IpfrsClient {
246 #[napi(constructor)]
251 pub fn new(data_dir: String) -> napi::Result<Self> {
252 IpfrsClient::create(data_dir).map_err(into_napi)
253 }
254
255 #[napi]
257 pub fn add_bytes(&self, data: napi::bindgen_prelude::Buffer) -> napi::Result<String> {
258 self.add_bytes_inner(data.as_ref()).map_err(into_napi)
259 }
260
261 #[napi]
263 pub fn get_bytes(&self, cid: String) -> napi::Result<Option<napi::bindgen_prelude::Buffer>> {
264 self.get_bytes_inner(&cid)
265 .map(|opt| opt.map(|v| v.into()))
266 .map_err(into_napi)
267 }
268
269 #[napi]
271 pub fn has(&self, cid: String) -> napi::Result<bool> {
272 self.has_inner(&cid).map_err(into_napi)
273 }
274
275 #[napi]
277 pub fn list_cids(&self) -> napi::Result<Vec<String>> {
278 self.list_cids_inner().map_err(into_napi)
279 }
280
281 #[napi]
283 pub fn delete(&self, cid: String) -> napi::Result<bool> {
284 self.delete_inner(&cid).map_err(into_napi)
285 }
286
287 #[napi]
289 pub fn version(&self) -> String {
290 self.version_inner()
291 }
292
293 #[napi]
295 pub fn stats(&self) -> napi::Result<String> {
296 self.stats_inner().map_err(into_napi)
297 }
298}
299
300#[cfg(feature = "napi")]
309#[napi_derive::napi]
310pub fn compute_cid(data: napi::bindgen_prelude::Buffer) -> String {
311 cid_from_bytes(data.as_ref())
312}
313
314#[cfg(not(feature = "napi"))]
316pub fn compute_cid(data: &[u8]) -> String {
317 cid_from_bytes(data)
318}
319
320#[cfg(feature = "napi")]
322#[napi_derive::napi]
323pub fn verify_cid(cid: String, data: napi::bindgen_prelude::Buffer) -> bool {
324 cid_from_bytes(data.as_ref()) == cid
325}
326
327#[cfg(not(feature = "napi"))]
329pub fn verify_cid(cid: &str, data: &[u8]) -> bool {
330 cid_from_bytes(data) == cid
331}
332
333#[cfg(feature = "napi")]
335#[napi_derive::napi]
336pub fn version() -> String {
337 env!("CARGO_PKG_VERSION").to_string()
338}
339
340#[cfg(not(feature = "napi"))]
342pub fn version() -> String {
343 env!("CARGO_PKG_VERSION").to_string()
344}
345
346#[cfg(feature = "napi")]
356#[napi_derive::napi]
357pub struct Node {
358 inner: std::sync::Arc<tokio::sync::Mutex<ipfrs::Node>>,
359 runtime: std::sync::Arc<tokio::runtime::Runtime>,
360}
361
362#[cfg(feature = "napi")]
363#[napi_derive::napi]
364impl Node {
365 #[napi(constructor)]
367 pub fn new(config: Option<NodeConfig>) -> napi::Result<Self> {
368 let rust_config = config.map(|c| c.into_rust_config()).unwrap_or_default();
369
370 let runtime = tokio::runtime::Runtime::new()
371 .map_err(|e| napi::Error::from_reason(format!("Failed to create runtime: {e}")))?;
372
373 let inner = ipfrs::Node::new(rust_config)
374 .map_err(|e| napi::Error::from_reason(format!("Failed to create node: {e}")))?;
375
376 Ok(Self {
377 inner: std::sync::Arc::new(tokio::sync::Mutex::new(inner)),
378 runtime: std::sync::Arc::new(runtime),
379 })
380 }
381
382 #[napi]
384 pub async fn start(&self) -> napi::Result<()> {
385 let inner = self.inner.clone();
386 self.runtime
387 .block_on(async move {
388 let mut node = inner.lock().await;
389 node.start().await
390 })
391 .map_err(|e| napi::Error::from_reason(format!("Failed to start: {e}")))
392 }
393
394 #[napi]
396 pub async fn stop(&self) -> napi::Result<()> {
397 let inner = self.inner.clone();
398 self.runtime
399 .block_on(async move {
400 let mut node = inner.lock().await;
401 node.stop().await
402 })
403 .map_err(|e| napi::Error::from_reason(format!("Failed to stop: {e}")))
404 }
405
406 #[napi]
408 pub async fn put_block(&self, data: napi::bindgen_prelude::Buffer) -> napi::Result<String> {
409 let data_bytes: bytes::Bytes = bytes::Bytes::from(data.to_vec());
410 let block = ipfrs::Block::new(data_bytes)
411 .map_err(|e| napi::Error::from_reason(format!("Failed to create block: {e}")))?;
412 let cid = *block.cid();
413
414 let inner = self.inner.clone();
415 self.runtime
416 .block_on(async move {
417 let node = inner.lock().await;
418 node.put_block(&block).await
419 })
420 .map_err(|e| napi::Error::from_reason(format!("Failed to put block: {e}")))?;
421
422 Ok(cid.to_string())
423 }
424
425 #[napi]
427 pub async fn get_block(
428 &self,
429 cid: String,
430 ) -> napi::Result<Option<napi::bindgen_prelude::Buffer>> {
431 let rust_cid: ipfrs::Cid = cid
432 .parse()
433 .map_err(|_| napi::Error::from_reason("Invalid CID"))?;
434
435 let inner = self.inner.clone();
436 let result = self.runtime.block_on(async move {
437 let node = inner.lock().await;
438 node.get_block(&rust_cid).await
439 });
440
441 match result {
442 Ok(Some(block)) => Ok(Some(block.data().to_vec().into())),
443 Ok(None) => Ok(None),
444 Err(e) => Err(napi::Error::from_reason(format!(
445 "Failed to get block: {e}"
446 ))),
447 }
448 }
449
450 #[napi]
452 pub async fn has_block(&self, cid: String) -> napi::Result<bool> {
453 let rust_cid: ipfrs::Cid = cid
454 .parse()
455 .map_err(|_| napi::Error::from_reason("Invalid CID"))?;
456
457 let inner = self.inner.clone();
458 self.runtime
459 .block_on(async move {
460 let node = inner.lock().await;
461 node.has_block(&rust_cid).await
462 })
463 .map_err(|e| napi::Error::from_reason(format!("Failed to check block: {e}")))
464 }
465
466 #[napi]
468 pub async fn delete_block(&self, cid: String) -> napi::Result<()> {
469 let rust_cid: ipfrs::Cid = cid
470 .parse()
471 .map_err(|_| napi::Error::from_reason("Invalid CID"))?;
472
473 let inner = self.inner.clone();
474 self.runtime
475 .block_on(async move {
476 let node = inner.lock().await;
477 node.delete_block(&rust_cid).await
478 })
479 .map_err(|e| napi::Error::from_reason(format!("Failed to delete block: {e}")))
480 }
481
482 #[napi]
484 pub async fn index_content(&self, cid: String, embedding: Vec<f64>) -> napi::Result<()> {
485 let rust_cid: ipfrs::Cid = cid
486 .parse()
487 .map_err(|_| napi::Error::from_reason("Invalid CID"))?;
488 let embedding_f32: Vec<f32> = embedding.iter().map(|&v| v as f32).collect();
489
490 let inner = self.inner.clone();
491 self.runtime
492 .block_on(async move {
493 let node = inner.lock().await;
494 node.index_content(&rust_cid, &embedding_f32).await
495 })
496 .map_err(|e| napi::Error::from_reason(format!("Failed to index: {e}")))
497 }
498
499 #[napi]
501 pub async fn search_similar(
502 &self,
503 query: Vec<f64>,
504 k: u32,
505 ) -> napi::Result<Vec<NodeSearchResult>> {
506 let query_f32: Vec<f32> = query.iter().map(|&v| v as f32).collect();
507 let k_usize = k as usize;
508
509 let inner = self.inner.clone();
510 let results = self
511 .runtime
512 .block_on(async move {
513 let node = inner.lock().await;
514 node.search_similar(&query_f32, k_usize).await
515 })
516 .map_err(|e| napi::Error::from_reason(format!("Search failed: {e}")))?;
517
518 Ok(results
519 .into_iter()
520 .map(|r| NodeSearchResult {
521 cid: r.cid.to_string(),
522 score: f64::from(r.score),
523 })
524 .collect())
525 }
526
527 #[napi]
529 pub fn add_fact(&self, fact: NodePredicate) -> napi::Result<()> {
530 let rust_fact = fact.into_rust_predicate()?;
531 let node = self.inner.blocking_lock();
532 node.add_fact(rust_fact)
533 .map_err(|e| napi::Error::from_reason(format!("Failed to add fact: {e}")))
534 }
535
536 #[napi]
538 pub fn add_rule(&self, rule: NodeRule) -> napi::Result<()> {
539 let rust_rule = rule.into_rust_rule()?;
540 let node = self.inner.blocking_lock();
541 node.add_rule(rust_rule)
542 .map_err(|e| napi::Error::from_reason(format!("Failed to add rule: {e}")))
543 }
544
545 #[napi]
549 pub fn infer(&self, goal: NodePredicate) -> napi::Result<Vec<String>> {
550 let rust_goal = goal.into_rust_predicate()?;
551 let node = self.inner.blocking_lock();
552 let results = node
553 .infer(&rust_goal)
554 .map_err(|e| napi::Error::from_reason(format!("Inference failed: {e}")))?;
555
556 Ok(results.into_iter().map(|s| format!("{s:?}")).collect())
557 }
558
559 #[napi]
561 pub fn prove(&self, goal: NodePredicate) -> napi::Result<Option<String>> {
562 let rust_goal = goal.into_rust_predicate()?;
563 let node = self.inner.blocking_lock();
564 let proof = node
565 .prove(&rust_goal)
566 .map_err(|e| napi::Error::from_reason(format!("Proof generation failed: {e}")))?;
567
568 Ok(proof.map(|p| format!("{p:?}")))
569 }
570
571 #[napi]
573 pub fn kb_stats(&self) -> napi::Result<KbStats> {
574 let node = self.inner.blocking_lock();
575 let stats = node
576 .tensorlogic_stats()
577 .map_err(|e| napi::Error::from_reason(format!("Failed to get stats: {e}")))?;
578
579 Ok(KbStats {
580 num_facts: stats.num_facts as u32,
581 num_rules: stats.num_rules as u32,
582 })
583 }
584
585 #[napi]
587 pub async fn save_semantic_index(&self, path: String) -> napi::Result<()> {
588 let inner = self.inner.clone();
589 self.runtime
590 .block_on(async move {
591 let node = inner.lock().await;
592 node.save_semantic_index(std::path::PathBuf::from(path))
593 .await
594 })
595 .map_err(|e| napi::Error::from_reason(format!("Failed to save index: {e}")))
596 }
597
598 #[napi]
600 pub async fn load_semantic_index(&self, path: String) -> napi::Result<()> {
601 let inner = self.inner.clone();
602 self.runtime
603 .block_on(async move {
604 let node = inner.lock().await;
605 node.load_semantic_index(std::path::PathBuf::from(path))
606 .await
607 })
608 .map_err(|e| napi::Error::from_reason(format!("Failed to load index: {e}")))
609 }
610
611 #[napi]
613 pub async fn save_kb(&self, path: String) -> napi::Result<()> {
614 let inner = self.inner.clone();
615 self.runtime
616 .block_on(async move {
617 let node = inner.lock().await;
618 node.save_knowledge_base(std::path::PathBuf::from(path))
619 .await
620 })
621 .map_err(|e| napi::Error::from_reason(format!("Failed to save KB: {e}")))
622 }
623
624 #[napi]
626 pub async fn load_kb(&self, path: String) -> napi::Result<()> {
627 let inner = self.inner.clone();
628 self.runtime
629 .block_on(async move {
630 let node = inner.lock().await;
631 node.load_knowledge_base(std::path::PathBuf::from(path))
632 .await
633 })
634 .map_err(|e| napi::Error::from_reason(format!("Failed to load KB: {e}")))
635 }
636}
637
638#[cfg(feature = "napi")]
644#[napi_derive::napi(object)]
645#[derive(Clone)]
646pub struct NodeConfig {
647 pub storage_path: Option<String>,
649 pub enable_semantic: Option<bool>,
651 pub enable_tensorlogic: Option<bool>,
653}
654
655#[cfg(feature = "napi")]
656impl NodeConfig {
657 fn into_rust_config(self) -> ipfrs::NodeConfig {
658 let mut config = ipfrs::NodeConfig::default();
659 if let Some(ref path) = self.storage_path {
660 config.storage.path = std::path::PathBuf::from(path);
661 }
662 if let Some(v) = self.enable_semantic {
663 config.enable_semantic = v;
664 }
665 if let Some(v) = self.enable_tensorlogic {
666 config.enable_tensorlogic = v;
667 }
668 config
669 }
670}
671
672#[cfg(feature = "napi")]
674#[napi_derive::napi(object)]
675pub struct NodeSearchResult {
676 pub cid: String,
677 pub score: f64,
678}
679
680#[cfg(feature = "napi")]
682#[napi_derive::napi(object)]
683pub struct KbStats {
684 pub num_facts: u32,
685 pub num_rules: u32,
686}
687
688#[cfg(feature = "napi")]
690#[napi_derive::napi(object)]
691#[derive(Clone)]
692pub struct NodeTerm {
693 pub kind: String,
695 pub value: String,
696}
697
698#[cfg(feature = "napi")]
699impl NodeTerm {
700 fn into_rust_term(self) -> napi::Result<ipfrs::Term> {
701 use ipfrs::{Constant, Term};
702 match self.kind.as_str() {
703 "int" => {
704 let val: i64 = self
705 .value
706 .parse()
707 .map_err(|_| napi::Error::from_reason("Invalid integer"))?;
708 Ok(Term::Const(Constant::Int(val)))
709 }
710 "float" => Ok(Term::Const(Constant::Float(self.value))),
711 "string" => Ok(Term::Const(Constant::String(self.value))),
712 "bool" => {
713 let val: bool = self
714 .value
715 .parse()
716 .map_err(|_| napi::Error::from_reason("Invalid boolean"))?;
717 Ok(Term::Const(Constant::Bool(val)))
718 }
719 "var" => Ok(Term::Var(self.value)),
720 other => Err(napi::Error::from_reason(format!(
721 "Unknown term kind: {other}"
722 ))),
723 }
724 }
725}
726
727#[cfg(feature = "napi")]
729#[napi_derive::napi(object)]
730#[derive(Clone)]
731pub struct NodePredicate {
732 pub name: String,
733 pub args: Vec<NodeTerm>,
734}
735
736#[cfg(feature = "napi")]
737impl NodePredicate {
738 fn into_rust_predicate(self) -> napi::Result<ipfrs::Predicate> {
739 let rust_args: napi::Result<Vec<ipfrs::Term>> =
740 self.args.into_iter().map(|t| t.into_rust_term()).collect();
741 Ok(ipfrs::Predicate::new(self.name, rust_args?))
742 }
743}
744
745#[cfg(feature = "napi")]
747#[napi_derive::napi(object)]
748#[derive(Clone)]
749pub struct NodeRule {
750 pub head: NodePredicate,
751 pub body: Vec<NodePredicate>,
752}
753
754#[cfg(feature = "napi")]
755impl NodeRule {
756 fn into_rust_rule(self) -> napi::Result<ipfrs::Rule> {
757 let rust_head = self.head.into_rust_predicate()?;
758 let rust_body: napi::Result<Vec<ipfrs::Predicate>> = self
759 .body
760 .into_iter()
761 .map(|p| p.into_rust_predicate())
762 .collect();
763 let body = rust_body?;
764 if body.is_empty() {
765 Ok(ipfrs::Rule::fact(rust_head))
766 } else {
767 Ok(ipfrs::Rule::new(rust_head, body))
768 }
769 }
770}
771
772#[derive(Debug)]
778pub struct ClientError {
779 message: String,
780}
781
782impl ClientError {
783 fn lock(op: &str) -> Self {
784 Self {
785 message: format!("Mutex poisoned during '{op}'"),
786 }
787 }
788
789 fn serialise(msg: String) -> Self {
790 Self { message: msg }
791 }
792}
793
794impl std::fmt::Display for ClientError {
795 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796 write!(f, "IpfrsClient error: {}", self.message)
797 }
798}
799
800impl std::error::Error for ClientError {}
801
802#[cfg(feature = "napi")]
803fn into_napi(e: ClientError) -> napi::Error {
804 napi::Error::from_reason(e.to_string())
805}
806
807#[cfg(test)]
812mod tests {
813 use super::*;
814
815 #[test]
820 fn test_client_add_get_roundtrip() {
821 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
822
823 let payload = b"hello, IPFRS!";
824 let cid = client.add_bytes_inner(payload).expect("add_bytes");
825 let retrieved = client
826 .get_bytes_inner(&cid)
827 .expect("get_bytes")
828 .expect("should be Some");
829
830 assert_eq!(payload.as_slice(), retrieved.as_slice());
831 }
832
833 #[test]
838 fn test_cid_deterministic() {
839 let data = b"determinism matters";
840 let cid1 = cid_from_bytes(data);
841 let cid2 = cid_from_bytes(data);
842 assert_eq!(cid1, cid2);
843 }
844
845 #[test]
850 fn test_cid_unique_per_content() {
851 let cid_a = cid_from_bytes(b"alpha");
852 let cid_b = cid_from_bytes(b"beta");
853 assert_ne!(cid_a, cid_b);
854 }
855
856 #[test]
861 fn test_has_returns_false_for_unknown() {
862 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
863
864 let present = client
865 .has_inner("bafyreifake000000000000000000000000000000000000000")
866 .expect("has");
867 assert!(!present);
868 }
869
870 #[test]
875 fn test_has_returns_true_after_add() {
876 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
877
878 let cid = client.add_bytes_inner(b"exists").expect("add_bytes");
879 assert!(client.has_inner(&cid).expect("has"));
880 }
881
882 #[test]
887 fn test_delete_block() {
888 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
889
890 let cid = client.add_bytes_inner(b"to delete").expect("add_bytes");
891 assert!(client.has_inner(&cid).expect("has before delete"));
892
893 let removed = client.delete_inner(&cid).expect("delete");
894 assert!(removed);
895 assert!(!client.has_inner(&cid).expect("has after delete"));
896 }
897
898 #[test]
903 fn test_delete_unknown_returns_false() {
904 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
905
906 let removed = client
907 .delete_inner("bafyreifake000000000000000000000000000000000000000")
908 .expect("delete");
909 assert!(!removed);
910 }
911
912 #[test]
917 fn test_list_cids() {
918 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
919
920 assert!(
921 client.list_cids_inner().expect("list empty").is_empty(),
922 "starts empty"
923 );
924
925 let cid1 = client.add_bytes_inner(b"first").expect("add 1");
926 let cid2 = client.add_bytes_inner(b"second").expect("add 2");
927
928 let mut cids = client.list_cids_inner().expect("list two");
929 cids.sort();
930 let mut expected = vec![cid1, cid2];
931 expected.sort();
932 assert_eq!(cids, expected);
933 }
934
935 #[test]
940 fn test_stats_json_valid() {
941 let client =
942 IpfrsClient::create("/tmp/ipfrs-stats-test".to_string()).expect("create client");
943
944 client.add_bytes_inner(b"some data").expect("add");
945 let json_str = client.stats_inner().expect("stats");
946 let parsed: serde_json::Value = serde_json::from_str(&json_str).expect("valid JSON");
947
948 assert_eq!(parsed["block_count"], 1);
949 assert_eq!(parsed["data_dir"], "/tmp/ipfrs-stats-test");
950 assert!(parsed["total_bytes"].as_u64().unwrap_or(0) > 0);
951 }
952
953 #[test]
958 fn test_verify_cid() {
959 let data = b"verifiable content";
960 let cid = cid_from_bytes(data);
961
962 assert!(cid_from_bytes(data) == cid, "correct CID should verify");
963 assert!(cid_from_bytes(data) != "bwrongcid", "wrong CID should fail");
964 }
965
966 #[test]
971 fn test_compute_cid_free_function() {
972 let cid = cid_from_bytes(b"ipfrs nodejs");
973 assert!(
974 cid.starts_with('b'),
975 "CIDv1 multibase base32lower must start with 'b', got: {cid}"
976 );
977 assert_eq!(cid.len(), 59, "unexpected CID length: {}", cid.len());
979 }
980
981 #[test]
986 fn test_version_string() {
987 let v = version();
988 assert!(!v.is_empty(), "version must not be empty");
989 let parts: Vec<&str> = v.split('.').collect();
991 assert_eq!(parts.len(), 3, "version should be semver: {v}");
992 }
993
994 #[test]
999 fn test_idempotent_add() {
1000 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
1001
1002 let cid1 = client.add_bytes_inner(b"idempotent").expect("add 1");
1003 let cid2 = client.add_bytes_inner(b"idempotent").expect("add 2");
1004 assert_eq!(cid1, cid2, "same content must yield same CID");
1005
1006 let cids = client.list_cids_inner().expect("list");
1007 assert_eq!(cids.len(), 1, "should only store one block");
1008 }
1009
1010 #[test]
1015 fn test_get_bytes_unknown_returns_none() {
1016 let client = IpfrsClient::create("/tmp/ipfrs-test".to_string()).expect("create client");
1017
1018 let result = client
1019 .get_bytes_inner("bafyreifake000000000000000000000000000000000000000")
1020 .expect("get_bytes");
1021 assert!(result.is_none());
1022 }
1023
1024 #[test]
1029 fn test_cid_format_base32lower() {
1030 let cid = cid_from_bytes(b"format check");
1031 assert!(cid.starts_with('b'));
1032 for ch in cid.chars().skip(1) {
1034 assert!(
1035 ch.is_ascii_lowercase() || ('2'..='7').contains(&ch),
1036 "unexpected char '{ch}' in CID '{cid}'"
1037 );
1038 }
1039 }
1040
1041 #[test]
1046 fn test_cid_known_value_stability() {
1047 let cid = cid_from_bytes(b"ipfrs stable");
1050 assert!(cid.starts_with('b'));
1052 assert_eq!(cid.len(), 59);
1053 let cid2 = cid_from_bytes(b"ipfrs stable");
1055 assert_eq!(cid, cid2);
1056 }
1057}