|
| 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 | +} |
0 commit comments