Skip to content

Commit 44b4b15

Browse files
committed
Merge branch 'main' into fork/zzstoatzz/main
2 parents ae849b1 + f375d8f commit 44b4b15

7 files changed

Lines changed: 138 additions & 26 deletions

File tree

.github/workflows/release.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
- os: windows
2727
target: x86_64
28-
interpreter: pypy3.9 pypy3.10
28+
interpreter: pypy3.9 pypy3.10 pypy3.11
2929
- os: windows
3030
target: i686
3131
python-architecture: x86
@@ -36,15 +36,15 @@ jobs:
3636

3737
- os: macos
3838
target: x86_64
39-
interpreter: pypy3.9 pypy3.10
39+
interpreter: pypy3.9 pypy3.10 pypy3.11
4040
- os: macos
4141
target: aarch64
4242
# actions/setup-python@v5 does not support 3.8 and 3.9 on arm64
43-
interpreter: 3.8 3.9 pypy3.9 pypy3.10
43+
interpreter: 3.8 3.9 pypy3.9 pypy3.10 pypy3.11
4444

4545
- os: ubuntu
4646
target: x86_64
47-
interpreter: pypy3.9 pypy3.10
47+
interpreter: pypy3.9 pypy3.10 pypy3.11
4848
- os: ubuntu
4949
target: i686
5050
- os: ubuntu
@@ -90,7 +90,7 @@ jobs:
9090
target: ${{ matrix.target }}
9191
manylinux: ${{ matrix.manylinux || 'auto' }}
9292
container: ${{ matrix.container }}
93-
args: --release --out dist --interpreter ${{ matrix.maturin-interpreter || matrix.interpreter || '3.8 3.9 3.10 3.11 3.12 3.13 pypy3.9 pypy3.10' }} ${{ matrix.extra-build-args }}
93+
args: --release --out dist --interpreter ${{ matrix.maturin-interpreter || matrix.interpreter || '3.8 3.9 3.10 3.11 3.12 3.13 pypy3.9 pypy3.10 pypy3.11' }} ${{ matrix.extra-build-args }}
9494
rust-toolchain: 1.76.0
9595
docker-options: -e CI
9696

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ edition = "2021"
55
license = "MIT"
66
description = "Python binding to the Rust IPLD library"
77
authors = ["Ilya (Marshal) <ilya@marshal.dev>"]
8-
keywords = ["python", "binding", "library", "lib", "ipld", "cid", "multibase", "multihash", "dag", "cbor", "json", "pb", "dag-cbor", "dag-json"]
8+
keywords = ["ipld", "cid", "multibase", "multihash", "dag-cbor"]
99

1010
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1111
[lib]
1212
name = "libipld"
1313
crate-type = ["rlib", "cdylib"]
1414

1515
[dependencies]
16-
pyo3 = { version = "0.23.4", features = ["generate-import-lib", "anyhow"] }
17-
python3-dll-a = "0.2.13"
16+
pyo3 = { version = "0.25.1", features = ["generate-import-lib", "anyhow"] }
17+
python3-dll-a = "0.2.14"
1818
anyhow = "1.0.95"
1919
libipld = { version = "0.16.0", features = ["dag-cbor"] }
2020
multibase = "0.9.1"

profiling/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ structopt = "0.3.26"
1111
clap = "4.5.29"
1212

1313
[dependencies.pyo3]
14-
version = "0.23.4"
14+
version = "0.25.1"

profiling/src/profiles/encode_dag_cbor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub fn exec(iterations: u64) {
1414

1515
for _ in 0..iterations {
1616
Python::with_gil(|gil| {
17-
println!("{}", libipld::encode_dag_cbor(gil, &PyString::new_bound(gil, json_str)).is_ok());
17+
println!("{}", libipld::encode_dag_cbor(gil, &PyString::new(gil, json_str)).is_ok());
1818
});
1919
}
2020
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ authors = [
88
license = "MIT"
99
repository = "https://github.com/MarshalX/python-libipld"
1010
readme = "README.md"
11-
keywords = ["library", "lib", "ipld", "cid", "multibase", "multihash", "dag", "cbor", "json", "pb", "dag-cbor", "dag-json"]
11+
keywords = ["library", "lib", "ipld", "cid", "multibase", "multihash", "dag", "cbor", "dag-cbor"]
1212
requires-python = ">=3.8"
1313
classifiers = [
1414
"Development Status :: 5 - Production/Stable",

pytests/test_dag_cbor.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,95 @@ def test_recursion_limit_exceed_on_nested_maps() -> None:
135135
libipld.decode_dag_cbor(dag_cbor)
136136

137137
assert 'in DAG-CBOR decoding' in str(exc_info.value)
138+
139+
140+
def test_dag_cbor_decode_largest_unsigned_int_roundtrip() -> None:
141+
data = bytes.fromhex('1bffffffffffffffff')
142+
143+
decoded_result = libipld.decode_dag_cbor(data)
144+
assert decoded_result == 2**64 - 1
145+
146+
encoded_result = libipld.encode_dag_cbor(decoded_result)
147+
assert encoded_result == data
148+
149+
150+
def test_dag_cbor_decode_smallest_negative_int_roundtrip() -> None:
151+
data = bytes.fromhex('3bffffffffffffffff')
152+
153+
decoded_result = libipld.decode_dag_cbor(data)
154+
assert decoded_result == -(2**64)
155+
156+
encoded_result = libipld.encode_dag_cbor(decoded_result)
157+
assert encoded_result == data
158+
159+
160+
def test_dag_cbor_decode_invalid_utf8() -> None:
161+
with pytest.raises(ValueError) as exc_info:
162+
libipld.decode_dag_cbor(bytes.fromhex('62c328'))
163+
164+
165+
assert 'Invalid UTF-8 string' in str(exc_info.value)
166+
167+
168+
def test_dab_cbor_decode_map_int_key() -> None:
169+
dag_cbor = bytes.fromhex('a10000')
170+
with pytest.raises(ValueError) as exc_info:
171+
libipld.decode_dag_cbor(dag_cbor)
172+
173+
assert 'Map keys must be strings' in str(exc_info.value)
174+
175+
176+
def test_dab_cbor_encode_map_int_key() -> None:
177+
obj = {0: 'value'}
178+
with pytest.raises(ValueError) as exc_info:
179+
libipld.encode_dag_cbor(obj)
180+
181+
assert 'Map keys must be strings' in str(exc_info.value)
182+
183+
184+
def test_dag_cbor_decode_nan_f64_error() -> None:
185+
# fb7ff8000000000000 - IEEE 754 double precision NaN
186+
with pytest.raises(ValueError) as exc_info:
187+
libipld.decode_dag_cbor(bytes.fromhex('fb7ff8000000000000'))
188+
189+
assert 'number out of range for f64' in str(exc_info.value).lower()
190+
191+
192+
def test_dag_cbor_decode_positive_infinity_f64_error() -> None:
193+
# fb7ff0000000000000 - IEEE 754 double precision positive infinity
194+
with pytest.raises(ValueError) as exc_info:
195+
libipld.decode_dag_cbor(bytes.fromhex('fb7ff0000000000000'))
196+
197+
assert 'number out of range for f64' in str(exc_info.value).lower()
198+
199+
200+
def test_dag_cbor_decode_negative_infinity_f64_error() -> None:
201+
# fbfff0000000000000 - IEEE 754 double precision negative infinity
202+
with pytest.raises(ValueError) as exc_info:
203+
libipld.decode_dag_cbor(bytes.fromhex('fbfff0000000000000'))
204+
205+
assert 'number out of range for f64' in str(exc_info.value).lower()
206+
207+
208+
def test_dag_cbor_decode_nan_f32_error() -> None:
209+
# fa7fc00000 - IEEE 754 single precision NaN
210+
with pytest.raises(ValueError) as exc_info:
211+
libipld.decode_dag_cbor(bytes.fromhex('fa7fc00000'))
212+
213+
assert 'number out of range for f32' in str(exc_info.value).lower()
214+
215+
216+
def test_dag_cbor_decode_positive_infinity_f32_error() -> None:
217+
# fa7f800000 - IEEE 754 single precision positive infinity
218+
with pytest.raises(ValueError) as exc_info:
219+
libipld.decode_dag_cbor(bytes.fromhex('fa7f800000'))
220+
221+
assert 'number out of range for f32' in str(exc_info.value).lower()
222+
223+
224+
def test_dag_cbor_decode_negative_infinity_f32_error() -> None:
225+
# faff800000 - IEEE 754 single precision negative infinity
226+
with pytest.raises(ValueError) as exc_info:
227+
libipld.decode_dag_cbor(bytes.fromhex('faff800000'))
228+
229+
assert 'number out of range for f32' in str(exc_info.value).lower()

src/lib.rs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,24 @@ fn map_key_cmp(a: &Vec<u8>, b: &Vec<u8>) -> std::cmp::Ordering {
5151
}
5252
}
5353

54-
fn sort_map_keys(keys: &Bound<PyList>, len: usize) -> Vec<(PyBackedStr, usize)> {
54+
fn sort_map_keys(keys: &Bound<PyList>, len: usize) -> Result<Vec<(PyBackedStr, usize)>> {
5555
// Returns key and index.
5656
let mut keys_str = Vec::with_capacity(len);
5757
for i in 0..len {
58-
let item = keys.get_item(i).unwrap();
59-
let key = item.downcast::<PyString>().unwrap().to_owned();
60-
let backed_str = PyBackedStr::try_from(key).unwrap();
58+
let item = keys.get_item(i)?;
59+
let key = match item.downcast::<PyString>() {
60+
Ok(k) => k.to_owned(),
61+
Err(_) => return Err(anyhow!("Map keys must be strings")),
62+
};
63+
let backed_str = match PyBackedStr::try_from(key) {
64+
Ok(bs) => bs,
65+
Err(_) => return Err(anyhow!("Failed to convert PyString to PyBackedStr")),
66+
};
6167
keys_str.push((backed_str, i));
6268
}
6369

6470
if keys_str.len() < 2 {
65-
return keys_str;
71+
return Ok(keys_str);
6672
}
6773

6874
keys_str.sort_by(|a, b| {
@@ -78,7 +84,7 @@ fn sort_map_keys(keys: &Bound<PyList>, len: usize) -> Vec<(PyBackedStr, usize)>
7884
}
7985
});
8086

81-
keys_str
87+
Ok(keys_str)
8288
}
8389

8490
fn get_bytes_from_py_any<'py>(obj: &'py Bound<'py, PyAny>) -> PyResult<&'py [u8]> {
@@ -96,11 +102,13 @@ fn get_bytes_from_py_any<'py>(obj: &'py Bound<'py, PyAny>) -> PyResult<&'py [u8]
96102
}
97103
}
98104

99-
fn string_new_bound<'py>(py: Python<'py>, s: &[u8]) -> Bound<'py, PyString> {
105+
fn string_new_bound<'py>(py: Python<'py>, s: &[u8]) -> Result<Bound<'py, PyString>> {
106+
std::str::from_utf8(s).map_err(|e| anyhow!("Invalid UTF-8 string: {}", e))?;
107+
100108
let ptr = s.as_ptr() as *const c_char;
101109
let len = s.len() as ffi::Py_ssize_t;
102110
unsafe {
103-
Bound::from_owned_ptr(py, ffi::PyUnicode_FromStringAndSize(ptr, len)).downcast_into_unchecked()
111+
Ok(Bound::from_owned_ptr(py, ffi::PyUnicode_FromStringAndSize(ptr, len)).downcast_into_unchecked())
104112
}
105113
}
106114

@@ -122,14 +130,14 @@ fn decode_dag_cbor_to_pyobject<R: Read + Seek>(
122130
let major = decode::read_major(r)?;
123131
Ok(match major.kind() {
124132
MajorKind::UnsignedInt => decode::read_uint(r, major)?.into_pyobject(py)?.into(),
125-
MajorKind::NegativeInt => (-1 - decode::read_uint(r, major)? as i64).into_pyobject(py)?.into(),
133+
MajorKind::NegativeInt => (-1 - decode::read_uint(r, major)? as i128).into_pyobject(py)?.into(),
126134
MajorKind::ByteString => {
127135
let len = decode::read_uint(r, major)?;
128136
PyBytes::new(py, &decode::read_bytes(r, len)?).into_pyobject(py)?.into()
129137
}
130138
MajorKind::TextString => {
131139
let len = decode::read_uint(r, major)?;
132-
string_new_bound(py, &decode::read_bytes(r, len)?).into_pyobject(py)?.into()
140+
string_new_bound(py, &decode::read_bytes(r, len)?)?.into_pyobject(py)?.into()
133141
}
134142
MajorKind::Array => {
135143
let len: ffi::Py_ssize_t = decode_len(decode::read_uint(r, major)?)?.try_into()?;
@@ -167,7 +175,7 @@ fn decode_dag_cbor_to_pyobject<R: Read + Seek>(
167175
}
168176
}
169177

170-
let key_py = string_new_bound(py, key.as_slice()).into_pyobject(py)?;
178+
let key_py = string_new_bound(py, key.as_slice())?.into_pyobject(py)?;
171179
prev_key = Some(key);
172180

173181
let value_py = decode_dag_cbor_to_pyobject(py, r, depth + 1)?;
@@ -191,8 +199,20 @@ fn decode_dag_cbor_to_pyobject<R: Read + Seek>(
191199
cbor::FALSE => false.into_pyobject(py)?.into_any().unbind(),
192200
cbor::TRUE => true.into_pyobject(py)?.into_any().unbind(),
193201
cbor::NULL => py.None(),
194-
cbor::F32 => decode::read_f32(r)?.into_pyobject(py)?.into(),
195-
cbor::F64 => decode::read_f64(r)?.into_pyobject(py)?.into(),
202+
cbor::F32 => {
203+
let value = decode::read_f32(r)?;
204+
if !value.is_finite() {
205+
return Err(anyhow!("Number out of range for f32 (NaNs are forbidden)".to_string()));
206+
}
207+
value.into_pyobject(py)?.into()
208+
},
209+
cbor::F64 => {
210+
let value = decode::read_f64(r)?;
211+
if !value.is_finite() {
212+
return Err(anyhow!("Number out of range for f64 (NaNs are forbidden)".to_string()));
213+
}
214+
value.into_pyobject(py)?.into()
215+
},
196216
_ => return Err(anyhow!("Unsupported major type".to_string())),
197217
},
198218
})
@@ -231,7 +251,7 @@ fn encode_dag_cbor_from_pyobject<'py, W: Write>(
231251

232252
Ok(())
233253
} else if obj.is_instance_of::<PyInt>() {
234-
let i: i64 = obj.extract()?;
254+
let i: i128 = obj.extract()?;
235255

236256
if i.is_negative() {
237257
encode::write_u64(w, MajorKind::NegativeInt, -(i + 1) as u64)?
@@ -252,7 +272,7 @@ fn encode_dag_cbor_from_pyobject<'py, W: Write>(
252272
Ok(())
253273
} else if let Ok(map) = obj.downcast::<PyDict>() {
254274
let len = map.len();
255-
let keys = sort_map_keys(&map.keys(), len);
275+
let keys = sort_map_keys(&map.keys(), len)?;
256276
let values = map.values();
257277

258278
encode::write_u64(w, MajorKind::Map, len as u64)?;

0 commit comments

Comments
 (0)