Skip to content

Commit 88fe80f

Browse files
authored
Optimize DAG-CBOR decode by reading each CBOR header once instead of re-decoding it per value (#118)
1 parent 2e2e62d commit 88fe80f

1 file changed

Lines changed: 79 additions & 21 deletions

File tree

src/dag_cbor/de.rs

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{anyhow, Result};
22
use cbor4ii::core::{
3-
dec::{self, Decode, Read},
4-
major, marker, types,
3+
dec::{self, Read},
4+
major, marker,
55
};
66
use pyo3::{ffi, prelude::*, types::*, BoundObject};
77

@@ -28,6 +28,55 @@ fn map_key_cmp(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
2828
}
2929
}
3030

31+
// Argument of the already-peeked header byte: low bits 0..=0x17 are the value
32+
// itself; 0x18..=0x1b mean 1/2/4/8 following big-endian bytes. Consumes the
33+
// header. Rejects indefinite-length and reserved arguments (0x1c..=0x1f),
34+
// which DAG-CBOR forbids.
35+
#[inline]
36+
fn decode_arg<'de, R: dec::Read<'de>>(r: &mut R, byte: u8) -> Result<u64>
37+
where
38+
R::Error: Send + Sync,
39+
{
40+
r.advance(1);
41+
let low = byte & 0x1f;
42+
if low <= 0x17 {
43+
return Ok(low as u64);
44+
}
45+
let n = match low {
46+
0x18 => 1,
47+
0x19 => 2,
48+
0x1a => 4,
49+
0x1b => 8,
50+
_ => return Err(anyhow!("Indefinite or reserved header argument")),
51+
};
52+
let buf = r.fill(n)?;
53+
let s = buf.as_ref();
54+
if s.len() < n {
55+
return Err(anyhow!("end of data"));
56+
}
57+
let mut be = [0u8; 8];
58+
be[8 - n..].copy_from_slice(&s[..n]);
59+
r.advance(n);
60+
Ok(u64::from_be_bytes(be))
61+
}
62+
63+
// Definite-length bytes/string payload, zero-copy from the input.
64+
#[inline]
65+
fn decode_seg<'de, R: dec::Read<'de>>(r: &mut R, byte: u8) -> Result<&'de [u8]>
66+
where
67+
R::Error: Send + Sync,
68+
{
69+
let len = usize::try_from(decode_arg(r, byte)?)?;
70+
match r.fill(len)? {
71+
dec::Reference::Long(s) if s.len() >= len => {
72+
let s = &s[..len];
73+
r.advance(len);
74+
Ok(s)
75+
}
76+
_ => Err(anyhow!("end of data")),
77+
}
78+
}
79+
3180
pub(crate) fn to_pyobject<'de, R: dec::Read<'de>>(
3281
py: Python,
3382
r: &mut R,
@@ -48,24 +97,27 @@ where
4897

4998
let byte = peek_one(r)?;
5099
Ok(match dec::if_major(byte) {
51-
major::UNSIGNED => u64::decode(r)?.into_pyobject(py)?.into(),
52-
major::NEGATIVE => i128::decode(r)?.into_pyobject(py)?.into(),
53-
major::BYTES => PyBytes::new(py, <types::Bytes<&[u8]>>::decode(r)?.0)
100+
major::UNSIGNED => decode_arg(r, byte)?.into_pyobject(py)?.into(),
101+
major::NEGATIVE => {
102+
let v = decode_arg(r, byte)?;
103+
// `-1 - v` fits an i64 for v < 2^63; the i128 fallback covers the
104+
// rest and costs a `_PyLong_FromByteArray`-style conversion.
105+
if v <= i64::MAX as u64 {
106+
(-1i64 - v as i64).into_pyobject(py)?.into()
107+
} else {
108+
(-1i128 - v as i128).into_pyobject(py)?.into()
109+
}
110+
}
111+
major::BYTES => PyBytes::new(py, decode_seg(r, byte)?)
54112
.into_pyobject(py)?
55113
.into(),
56114
major::STRING => {
57115
// ASCII fast path inside the helper; non-ASCII falls through to
58116
// `PyUnicode_DecodeUTF8`, which is where the spec validation lives.
59-
from_bytes(
60-
py,
61-
<types::UncheckedStr<&[u8]>>::decode(r)
62-
.map_err(|_| anyhow!("Cannot decode as bytes"))?
63-
.0,
64-
)?
65-
.into()
117+
from_bytes(py, decode_seg(r, byte)?)?.into()
66118
}
67119
major::ARRAY => {
68-
let len = types::Array::len(r)?.ok_or_else(|| anyhow!("Array must contain length"))?;
120+
let len = usize::try_from(decode_arg(r, byte)?)?;
69121
// Every element costs at least one byte; reject a claimed length
70122
// beyond the remaining input before allocating for it.
71123
if r.fill(len)?.as_ref().len() < len {
@@ -94,7 +146,7 @@ where
94146
}
95147
}
96148
major::MAP => {
97-
let len = types::Map::len(r)?.ok_or_else(|| anyhow!("Map must contain length"))?;
149+
let len = usize::try_from(decode_arg(r, byte)?)?;
98150
// Every entry costs at least two bytes (key + value); reject a
99151
// claimed length beyond the remaining input before presizing.
100152
let need = len.saturating_mul(2);
@@ -114,9 +166,11 @@ where
114166
for _ in 0..len {
115167
// DAG-CBOR keys are always strings. Python does the UTF-8 validation when creating
116168
// the string.
117-
let key = <types::UncheckedStr<&[u8]>>::decode(r)
118-
.map_err(|_| anyhow!("Map keys must be strings"))?
119-
.0;
169+
let key_byte = peek_one(r)?;
170+
if dec::if_major(key_byte) != major::STRING {
171+
return Err(anyhow!("Map keys must be strings"));
172+
}
173+
let key = decode_seg(r, key_byte)?;
120174

121175
if let Some(prev_key) = prev_key {
122176
// it cares about duplicated keys too thanks to Ordering::Equal
@@ -146,12 +200,16 @@ where
146200
dict.into_pyobject(py)?.into()
147201
}
148202
major::TAG => {
149-
let value = types::Tag::tag(r)?;
203+
let value = decode_arg(r, byte)?;
150204
if value != 42 {
151205
return Err(anyhow!("Non-42 tags are not supported"));
152206
}
153207

154-
let cid = <types::Bytes<&[u8]>>::decode(r)?.0;
208+
let content_byte = peek_one(r)?;
209+
if dec::if_major(content_byte) != major::BYTES {
210+
return Err(anyhow!("Invalid CID"));
211+
}
212+
let cid = decode_seg(r, content_byte)?;
155213

156214
// we expect CIDs to have a leading zero byte
157215
if cid.len() <= 1 || cid[0] != 0 {
@@ -182,7 +240,7 @@ where
182240
py.None()
183241
}
184242
marker::F32 => {
185-
let value = f32::decode(r)?;
243+
let value = f32::from_bits(decode_arg(r, byte)? as u32);
186244
if !value.is_finite() {
187245
return Err(anyhow!(
188246
"Number out of range for f32 (NaNs are forbidden)".to_string()
@@ -191,7 +249,7 @@ where
191249
value.into_pyobject(py)?.into()
192250
}
193251
marker::F64 => {
194-
let value = f64::decode(r)?;
252+
let value = f64::from_bits(decode_arg(r, byte)?);
195253
if !value.is_finite() {
196254
return Err(anyhow!(
197255
"Number out of range for f64 (NaNs are forbidden)".to_string()

0 commit comments

Comments
 (0)