Skip to content

Commit eb9f064

Browse files
authored
Restructure single-file lib.rs into per-domain modules (#114)
1 parent 7361420 commit eb9f064

24 files changed

Lines changed: 1136 additions & 980 deletions

src/car.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//! CAR (Content Addressable aRchive) v1 container decoding. Encoding is not
2+
//! implemented yet; when it lands this becomes `car/{de,ser}.rs`.
3+
4+
use cbor4ii::core::dec::Read;
5+
use pyo3::prelude::*;
6+
use pyo3::types::*;
7+
8+
use crate::dag_cbor::de::to_pyobject;
9+
use crate::error::value_error;
10+
use crate::ffi::recursion::current_recursion_limit;
11+
use crate::io::leb128::read_u64;
12+
use crate::io::SliceReader;
13+
14+
#[pyfunction]
15+
pub fn decode_car<'py>(py: Python<'py>, data: &[u8]) -> PyResult<(Py<PyAny>, Bound<'py, PyDict>)> {
16+
let buf = &mut SliceReader::new(data);
17+
let max_depth = current_recursion_limit();
18+
19+
if read_u64(buf).is_err() {
20+
return Err(value_error(
21+
"Failed to read CAR header",
22+
"Invalid uvarint".to_string(),
23+
));
24+
}
25+
let Ok(header_obj) = to_pyobject(py, buf, 0, max_depth) else {
26+
return Err(value_error(
27+
"Failed to read CAR header",
28+
"Invalid DAG-CBOR".to_string(),
29+
));
30+
};
31+
32+
let header = header_obj.cast_bound::<PyDict>(py)?;
33+
34+
let Some(version) = header.get_item("version")? else {
35+
return Err(value_error(
36+
"Failed to read CAR header",
37+
"Version is None".to_string(),
38+
));
39+
};
40+
if version.cast::<PyInt>()?.extract::<u64>()? != 1 {
41+
return Err(value_error(
42+
"Failed to read CAR header",
43+
"Unsupported version. Version must be 1".to_string(),
44+
));
45+
}
46+
47+
let Some(roots) = header.get_item("roots")? else {
48+
return Err(value_error(
49+
"Failed to read CAR header",
50+
"Roots is None".to_string(),
51+
));
52+
};
53+
if roots.cast::<PyList>()?.len() == 0 {
54+
return Err(value_error(
55+
"Failed to read CAR header",
56+
"Roots is empty. Must be at least one".to_string(),
57+
));
58+
}
59+
60+
// FIXME (MarshalX): we are not verifying if the roots are valid CIDs
61+
62+
let parsed_blocks = PyDict::new(py);
63+
64+
loop {
65+
if read_u64(buf).is_err() {
66+
// FIXME (MarshalX): we are not raising an error here because of possible EOF
67+
break;
68+
}
69+
70+
let cid_bytes_before = buf.buf;
71+
// `&[u8]` is itself an `io::Read`, so we hand it to `Cid::read_bytes`
72+
// directly and recover the consumed length from the slice shrink.
73+
let mut slice: &[u8] = cid_bytes_before;
74+
let cid_result = ::cid::Cid::read_bytes(&mut slice);
75+
let Ok(cid) = cid_result else {
76+
return Err(value_error(
77+
"Failed to read CID of block",
78+
cid_result.unwrap_err().to_string(),
79+
));
80+
};
81+
82+
if cid.codec() != 0x71 {
83+
return Err(value_error(
84+
"Failed to read CAR block",
85+
"Unsupported codec. For now we support only DAG-CBOR (0x71)".to_string(),
86+
));
87+
}
88+
89+
let consumed = cid_bytes_before.len() - slice.len();
90+
buf.advance(consumed);
91+
let cid_raw = &cid_bytes_before[..consumed];
92+
93+
let block_result = to_pyobject(py, buf, 0, max_depth);
94+
let Ok(block) = block_result else {
95+
return Err(value_error(
96+
"Failed to read CAR block",
97+
block_result.unwrap_err().to_string(),
98+
));
99+
};
100+
101+
let key = PyBytes::new(py, cid_raw).into_pyobject(py)?;
102+
parsed_blocks.set_item(key, block)?;
103+
}
104+
105+
Ok((header_obj, parsed_blocks))
106+
}

src/cid.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! CID (Content IDentifier) codec plus the shared CID helpers used across
2+
//! codecs: extraction from arbitrary Python objects and the O(1) shape check.
3+
4+
pub(crate) mod de;
5+
pub(crate) mod ser;
6+
7+
pub(crate) use de::decode_cid;
8+
pub(crate) use ser::encode_cid;
9+
10+
use pyo3::prelude::*;
11+
use pyo3::types::*;
12+
13+
use crate::convert::extract_bytes;
14+
use crate::error::value_error;
15+
16+
// `Cid::try_from` parses two varints + a multihash on every call; this O(1)
17+
// shape check rejects payloads that can't be a CID without paying for it.
18+
// CIDv1 starts with `0x01`; CIDv0 is exactly 34 bytes starting `0x12 0x20`.
19+
#[inline]
20+
pub(crate) fn looks_like_cid(bytes: &[u8]) -> bool {
21+
if bytes.len() < 4 {
22+
return false;
23+
}
24+
if bytes[0] == 0x01 {
25+
return true;
26+
}
27+
bytes.len() == 34 && bytes[0] == 0x12 && bytes[1] == 0x20
28+
}
29+
30+
pub(crate) fn extract_cid(data: &Bound<PyAny>) -> PyResult<::cid::Cid> {
31+
let cid = if let Ok(s) = data.cast::<PyString>() {
32+
::cid::Cid::try_from(s.to_str()?)
33+
} else {
34+
::cid::Cid::try_from(extract_bytes(data)?)
35+
};
36+
37+
if let Ok(cid) = cid {
38+
Ok(cid)
39+
} else {
40+
Err(value_error(
41+
"Failed to decode CID",
42+
cid.unwrap_err().to_string(),
43+
))
44+
}
45+
}

src/cid/de.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use pyo3::prelude::*;
2+
use pyo3::types::*;
3+
4+
use crate::cid::extract_cid;
5+
6+
fn hash_to_pydict<'py>(py: Python<'py>, cid: &::cid::Cid) -> PyResult<Bound<'py, PyDict>> {
7+
let hash = cid.hash();
8+
let dict_obj = PyDict::new(py);
9+
10+
dict_obj.set_item("code", hash.code())?;
11+
dict_obj.set_item("size", hash.size())?;
12+
dict_obj.set_item("digest", PyBytes::new(py, hash.digest()))?;
13+
14+
Ok(dict_obj)
15+
}
16+
17+
fn to_pydict<'py>(py: Python<'py>, cid: &::cid::Cid) -> PyResult<Bound<'py, PyDict>> {
18+
let dict_obj = PyDict::new(py);
19+
20+
dict_obj.set_item("version", cid.version() as u64)?;
21+
dict_obj.set_item("codec", cid.codec())?;
22+
dict_obj.set_item("hash", hash_to_pydict(py, cid)?)?;
23+
Ok(dict_obj)
24+
}
25+
26+
#[pyfunction]
27+
pub fn decode_cid<'py>(py: Python<'py>, data: &Bound<PyAny>) -> PyResult<Bound<'py, PyDict>> {
28+
to_pydict(py, &extract_cid(data)?)
29+
}

src/cid/ser.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use pyo3::prelude::*;
2+
use pyo3::types::*;
3+
4+
use crate::cid::extract_cid;
5+
6+
#[pyfunction]
7+
pub fn encode_cid<'py>(py: Python<'py>, data: &Bound<PyAny>) -> PyResult<Bound<'py, PyString>> {
8+
Ok(PyString::new(py, extract_cid(data)?.to_string().as_str()))
9+
}

src/convert.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use pyo3::prelude::*;
2+
use pyo3::types::*;
3+
4+
use crate::error::value_error;
5+
6+
/// Borrow a byte view from a `bytes`, `bytearray`, or `str` (UTF-8) object.
7+
pub(crate) fn extract_bytes<'py>(obj: &'py Bound<'py, PyAny>) -> PyResult<&'py [u8]> {
8+
if let Ok(b) = obj.cast::<PyBytes>() {
9+
Ok(b.as_bytes())
10+
} else if let Ok(ba) = obj.cast::<PyByteArray>() {
11+
Ok(unsafe { ba.as_bytes() })
12+
} else if let Ok(s) = obj.cast::<PyString>() {
13+
Ok(s.to_str()?.as_bytes())
14+
} else {
15+
Err(value_error(
16+
"Failed to encode multibase",
17+
"Unsupported data type".to_string(),
18+
))
19+
}
20+
}

src/dag_cbor.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//! DAG-CBOR codec: decode (`de`) and encode (`ser`) of the IPLD data model
2+
//! to and from native Python objects.
3+
4+
pub(crate) mod de;
5+
pub(crate) mod ser;
6+
7+
pub(crate) use de::{decode_dag_cbor, decode_dag_cbor_multi};
8+
pub(crate) use ser::encode_dag_cbor;

0 commit comments

Comments
 (0)