Skip to content

Commit 2ed082a

Browse files
authored
Fix 33 reproduced fuzzing and static-review defects (#8514)
* mmap: guard move() against dest/src past the mapping size An offset larger than the mapping made `size - dest` underflow, producing an out-of-range slice index panic. Check the high side first, as write() does. Assisted-by: Claude * exceptions: keep ImportError name/path/name_from out of an empty instance dict ImportError.__reduce__ unwrapped get_arg(0), which is None when the exception was constructed with no positional argument, aborting on pickle.dumps(ImportError()). Fall back to the full args tuple there, and expose name/path/name_from as class attributes defaulting to None so a bare ImportError() reduces to (cls, ()). Assisted-by: Claude * asyncio: stop unwrapping the _current_tasks downcast in current_task() _asyncio._current_tasks is a reassignable module attribute; the three sibling functions already degrade gracefully when it is not a dict, current_task() did not. Assisted-by: Claude * asyncio: raise TypeError when FutureIter.throw()'s exception class returns a non-exception exc_type.call() goes through a Python-controlled __new__, so its result can be any object; the downcast was unwrapped. Report it as a TypeError instead. Assisted-by: Claude * sequence: use a fallible reservation for sequence repetition The guard only rejects a repeat whose per-element size crosses MAX_MEMORY_SIZE, so (1,) * (10**12) still reached Vec::with_capacity and aborted in the allocator. Reserve fallibly and surface a MemoryError. Assisted-by: Claude * collections: reject an oversized deque repetition with MemoryError deque * sys.maxsize reached the allocator and aborted with a capacity overflow. Apply the MAX_MEMORY_SIZE guard the other sequences already carry. Assisted-by: Claude * itertools: raise OverflowError for an out-of-range r argument combinations/combinations_with_replacement/permutations narrowed r with to_usize().unwrap(), so r=2**64 panicked instead of raising. Assisted-by: Claude * math: stream the generic sumprod path instead of collecting both iterables The big-int path collected each argument into a Vec before multiplying, so an unbounded iterable exhausted memory. Advance both iterators in lockstep and accumulate, reporting a length mismatch when only one is exhausted. Assisted-by: Claude * _imp: raise TypeError for a second positional argument to find_frozen withdata is keyword-only and unimplemented; passing it positionally hit an unimplemented!() and aborted. Assisted-by: Claude * _typing: check _idfunc arity before indexing args _typing._idfunc() with no argument indexed args[0] out of bounds. Assisted-by: Claude * exceptions: require a sequence for the ExceptionGroup excs argument The argument was collected before being validated, so an unbounded iterable such as itertools.count() exhausted memory. Reject a non-sequence up front. Assisted-by: Claude * mmap: treat an inverted find/rfind range as empty find(b"x", 5, 2) built a slice whose start exceeded its end and panicked. Assisted-by: Claude * collections: give deque and defaultdict a GC traverse Neither type opted into traversal, so a reference cycle through a deque or a defaultdict was never collected. Assisted-by: Claude * itertools: opt the iterator types into GC traverse cycle and its siblings hold Python references but declared no traverse, so a cycle built through one of them leaked. Assisted-by: Claude * _ctypes: mask an out-of-range int instead of panicking c_char_p(2**64) and pointer item assignment called .expect() on the narrowing conversion. Wrap the value to the target width, which is what the C implementation stores. Assisted-by: Claude * hashlib: import _hashlib when a hash module is loaded The hash object type is a static type owned by _hashlib; calling _md5.md5() without _hashlib imported hit an uninitialized static type and panicked. Assisted-by: Claude * _csv: fall back to the built-in dialect defaults when none is registered The dialect table is empty until csv.py registers 'excel', so _csv.reader([]) unwrapped a missing entry and panicked. Assisted-by: Claude * builtins: reject surrogates in compile()/eval() source instead of panicking expect_str() panics on a str containing surrogates; convert with try_as_utf8 so eval(chr(0xd800)) raises. Assisted-by: Claude * _suggestions: require a list for _generate_suggestions candidates The argument was collected before validation, so an unbounded iterable exhausted memory. Assisted-by: Claude * lzma: size the filter chain before consuming it filters= was collected into a Vec before the length check, so an unbounded iterable exhausted memory. Take the length through the sequence protocol first. Assisted-by: Claude * classmethod: opt into GC traverse staticmethod already declares traverse; classmethod did not, so a cycle through the wrapped callable leaked. Assisted-by: Claude * posix: reject unbounded iterables in posix_spawn and setgroups argv, setsigdef, setsigmask and setgroups bound an ArgIterable and collected it before validating, so an infinite generator exhausted memory. Require a list/tuple for argv, validate signals while streaming, and take setgroups through the sequence protocol. Assisted-by: Claude * _ctypes: size Array slice assignment and _argtypes_ before collecting Both eagerly collected their argument, so an unbounded iterable exhausted memory before the length check ran. Assisted-by: Claude * sys: propagate the breakpointhook warning failure warn() was unwrapped, so an unimportable $PYTHONBREAKPOINT under -W error panicked instead of raising. Assisted-by: Claude * structseq: require the sequence argument when constructing a struct sequence A no-argument construction produced an empty backing tuple, and reading any named field then indexed out of bounds. Assisted-by: Claude * Guard the hash slot dispatch against unbounded recursion `PyObject::hash` invoked the type's hash slot directly, so an element-wise `__hash__` following a deeply nested object graph recursed one native frame per level and overflowed the stack. Wrap the dispatch in `with_recursion`, matching the repr and rich-compare dispatches in the same file, so `hash(x)` on a nested tuple/GenericAlias/slice raises `RecursionError`. Assisted-by: Claude * genericalias: guard the __parameters__ walk against unbounded recursion `make_parameters_from_slice` recursed into every list/tuple argument with nothing counting the frames, so `list[L]` for a self-referential or deeply nested `L` overflowed the native stack at subscript time. Wrap the descent in `with_recursion`, which makes the walk fallible; `PyGenericAlias::new`, `from_args` and `make_parameters` now return `PyResult` and every caller propagates. Assisted-by: Claude * Fix the type confusion in PyAtomicRef's Debug impl `PyAtomicRef<T>` stores a pointer to a `Py<T>`, as `Deref`, `load_raw`, `swap` and `Drop` all read it, but `Debug` cast it to a bare `T` and formatted the object header as payload bytes. For `PyFunction`, whose `code: PyAtomicRef<PyCode>` has a pointer-chasing `Debug`, that dereferenced header words and segfaulted. Cast to `PyObject` instead, which is what `Drop` already does and which also covers the `PyAtomicRef<PyObject>` and `PyAtomicRef<Option<T>>` instantiations that have no `Py<T>`. `_asyncio._enter_task` reached this through `{:?}` in its "Cannot enter into task" message; format the two tasks with their Python repr, which is what the message is meant to show. Assisted-by: Claude * _sre: disallow instantiating Match `Match` inherited `object.__new__`, so `M.__new__(M)` produced an instance whose `regs`/`string`/`pattern` were never filled in by a match run; the mapping subscript path read them and dereferenced garbage. The type has no public constructor, so mark it `DISALLOW_INSTANTIATION`, which makes `M.__new__(M)` raise `TypeError: cannot create 're.Match' instances`. Assisted-by: Claude * utils: return the empty repr instead of asserting a non-empty collection `collection_repr` took the first element with an `.expect()` justified by the caller's preceding non-empty check. Another thread clearing the collection between that check and the iteration made the iterator yield nothing and panicked the worker. Fall back to the caller-supplied empty form, which is the text those callers already produce for an empty collection. Assisted-by: Claude * itertools: advance cycle's index atomically `cycle.__next__` did `fetch_add(1)` and then reset the index to 0 in a separate store, so two threads replaying the saved items could both read an index past the end of `saved` and panic on the slice access. Do the advance and the wrap in one `fetch_update`. Assisted-by: Claude * _ctypes: reject a float argument to a foreign call without argtypes `conv_param`, the conversion used when `argtypes` is not set, converted its argument with `try_int`, which goes through `__int__` and so accepts a float. `libc.strlen(1.5)` therefore passed 1 where the callee expects a `char *` and the callee dereferenced it. Match `ConvParam`, which does a `PyLong_Check` and converts nothing: take the branch only for an `int` (or a subclass, so `True` still converts), and let a float fall through to "Don't know how to convert parameter". The branch below it converted a float to a C double, but `try_int` claimed every float before it could run, so it was dead; `CArgValue::Double` existed only for that branch and both go. Typed doubles are unaffected — they travel as `CArgValue::Typed` with code 'd'. Assisted-by: Claude * Report the iterator itself from PyIter's traverse `Traverse for PyIter<O>` delegated to the inherent `PyObject::traverse` of the object it wraps, so it reported that iterator's referents instead of the iterator. The iterator's own reference to those referents was then never subtracted during the collector's reference-subtraction pass, the referents kept a non-zero gc_refs, and every object reachable from them was classified as a root. Any cycle running through a type with a `PyIter` field therefore survived collection: `map`, `filter`, `zip`, `enumerate`, `reversed` and the `itertools` iterators all leaked, while the same cycle through a `list`, `tuple` or `list_iterator` collected. Report the wrapped object, as the `PyObjectRef`, `PyRef<T>` and `PyStackRef` impls do. `itertools.tee` still leaks: its shared buffer is a `PyRc<PyItertoolsTeeData>` rather than a Python object, so the collector cannot see through it. Assisted-by: Claude * Remove the obsolete expectedFailure on test_code_module.test_unicode_error Compiling a source string containing a lone surrogate now raises UnicodeEncodeError, so the test passes. Assisted-by: Claude * itertools: reserve the combination indices fallibly `combinations` and `combinations_with_replacement` built their index vector with an infallible allocation, so an `r` that passes the ssize_t check but does not fit in memory aborted the process instead of raising MemoryError. Assisted-by: Claude * Apply the struct sequence constructor's dict argument `structseq(sequence, dict)` discarded its second argument, so the hidden fields past `n_sequence_fields` — `tm_zone`, `st_atime` and friends — were always None when constructed directly or restored from a `(sequence, dict)` pickle, and a non-dict second argument was accepted silently. Take the dict, require it to be a dict, and fill the hidden slots the sequence did not cover from it. A key that names a field the sequence already supplied, or no field at all, is now a "got duplicate or unexpected field name(s)" TypeError instead of being dropped. Both arguments are bindable by name, as `sequence` and `dict`. `os.stat_result` and `os.statvfs_result` did not accept a second argument at all; they and `time.struct_time` now share the parsing. Assisted-by: Claude * _imp: report the argument count in find_frozen's arity error Assisted-by: Claude * Add regression tests for the reproduced crashers One case per catalog entry, each asserting the behavior the fix produces: recursion guards, the memory-unsafety sites, the overflow and unbounded allocation guards, the eager-collection rejections, the unwrap sites, and the cycles the collector now breaks. Every expected value was checked against CPython 3.14. Assisted-by: Claude * Tolerate a changed-size RuntimeError in the set repr stress test The mutator and reader threads race on purpose; a "changed size during iteration" RuntimeError is a valid outcome of that race and should not fail the test. The panic it guards against is not. Assisted-by: Claude * Move the crash regression tests into per-module snippets crash_regressions.py collected every reproduced crasher in one file. Split it into the snippet for the module each case exercises, and add stdlib_gc.py, stdlib_asyncio.py, stdlib_lzma.py, stdlib_threading_set_repr.py and stdlib_threading_itertools_cycle.py for the cases with no existing home. The suite runs every snippet under the host CPython too, so the checks only RustPython raises are guarded by sys.implementation.name: the hash and __parameters__ recursion depth, and the deque repeat overflow. The float ctypes argument accepts either TypeError or ctypes.ArgumentError. Assisted-by: Claude
1 parent c9a6244 commit 2ed082a

93 files changed

Lines changed: 1157 additions & 342 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Lib/test/test_code_module.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ def test_indentation_error(self):
128128
self.assertIsNone(self.sysmod.last_value.__traceback__)
129129
self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
130130

131-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'UnicodeDecodeError: invalid utf-8 sequence of 1 bytes from index 1\n\nnow exiti [truncated]... doesn't start with 'UnicodeEncodeError: '
132131
def test_unicode_error(self):
133132
self.infunc.side_effect = ["'\ud800'", EOFError('Finished')]
134133
self.console.interact()

Lib/test/test_structseq.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ def test_fields(self):
8787
self.assertEqual(t.n_unnamed_fields, 0)
8888
self.assertEqual(t.n_fields, time._STRUCT_TM_ITEMS)
8989

90-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument dict
9190
def test_constructor(self):
9291
t = time.struct_time
9392

@@ -111,7 +110,6 @@ def test_constructor(self):
111110
s = "123456789"
112111
self.assertEqual("".join(t(s)), s)
113112

114-
@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
115113
def test_constructor_with_duplicate_fields(self):
116114
t = time.struct_time
117115

@@ -125,7 +123,6 @@ def test_constructor_with_duplicate_fields(self):
125123
with self.assertRaisesRegex(TypeError, error_message):
126124
t("1234567890", dict={"error": 0, "tm_zone": "some zone", "tm_mon": 1})
127125

128-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
129126
def test_constructor_with_duplicate_unnamed_fields(self):
130127
assert os.stat_result.n_unnamed_fields > 0
131128
n_visible_fields = os.stat_result.n_sequence_fields
@@ -142,7 +139,6 @@ def test_constructor_with_duplicate_unnamed_fields(self):
142139
re.escape("got duplicate or unexpected field name(s)")):
143140
os.stat_result((*range(n_visible_fields), -1.0), {'st_atime': -1.0})
144141

145-
@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
146142
def test_constructor_with_unknown_fields(self):
147143
t = time.struct_time
148144

@@ -185,7 +181,6 @@ def test_pickling(self):
185181
self.assertEqual(t2.tm_year, t.tm_year)
186182
self.assertEqual(t2.tm_zone, t.tm_zone)
187183

188-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
189184
def test_pickling_with_unnamed_fields(self):
190185
assert os.stat_result.n_unnamed_fields > 0
191186

@@ -220,7 +215,6 @@ def test_copying(self):
220215
self.assertIsNot(t3[0], t[0])
221216
self.assertIsNot(t3.tm_year, t.tm_year)
222217

223-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
224218
def test_copying_with_unnamed_fields(self):
225219
assert os.stat_result.n_unnamed_fields > 0
226220

crates/capi/src/genericaliasobject.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ pub unsafe extern "C" fn Py_GenericAlias(
1010
with_vm(|vm| {
1111
let origin = unsafe { &*origin }.to_owned();
1212
let args = unsafe { &*args }.to_owned();
13-
PyGenericAlias::from_args(origin, args, vm).into_pyobject(vm)
13+
PyGenericAlias::from_args(origin, args, vm).map(|alias| alias.into_pyobject(vm))
1414
})
1515
}

crates/stdlib/src/_asyncio.rs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ pub(crate) mod _asyncio {
1212
vm::{
1313
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
1414
builtins::{
15-
PyBaseException, PyBaseExceptionRef, PyDict, PyDictRef, PyGenericAlias, PyList,
16-
PyListRef, PyModule, PySet, PyTuple, PyType, PyTypeRef,
15+
PyBaseException, PyBaseExceptionRef, PyDict, PyGenericAlias, PyList, PyListRef,
16+
PyModule, PySet, PyTuple, PyType, PyTypeRef,
1717
},
1818
extend_module,
1919
function::{FuncArgs, KwArgs, OptionalArg, OptionalOption, PySetterValue},
@@ -779,7 +779,7 @@ pub(crate) mod _asyncio {
779779
cls: PyTypeRef,
780780
args: PyObjectRef,
781781
vm: &VirtualMachine,
782-
) -> PyGenericAlias {
782+
) -> PyResult<PyGenericAlias> {
783783
PyGenericAlias::from_args(cls, args, vm)
784784
}
785785
}
@@ -1036,7 +1036,7 @@ pub(crate) mod _asyncio {
10361036
)));
10371037
}
10381038

1039-
let exc = if exc_type.fast_isinstance(vm.ctx.types.type_type) {
1039+
let exc: PyBaseExceptionRef = if exc_type.fast_isinstance(vm.ctx.types.type_type) {
10401040
// exc_type is a class
10411041
let exc_class: PyTypeRef = exc_type.clone().downcast().unwrap();
10421042
// Must be a subclass of BaseException
@@ -1047,12 +1047,23 @@ pub(crate) mod _asyncio {
10471047
}
10481048

10491049
let val = exc_val.unwrap_or_none(vm);
1050-
if vm.is_none(&val) {
1050+
let exc = if vm.is_none(&val) {
10511051
exc_type.call((), vm)?
10521052
} else if val.fast_isinstance(&exc_class) {
10531053
val
10541054
} else {
10551055
exc_type.call((val,), vm)?
1056+
};
1057+
match exc.downcast() {
1058+
Ok(exc) => exc,
1059+
Err(obj) => {
1060+
let exc_class_repr = exc_class.as_object().repr(vm)?;
1061+
vm.new_type_error(format!(
1062+
"calling {} should have returned an instance of BaseException, not {}",
1063+
exc_class_repr.as_wtf8(),
1064+
obj.class()
1065+
))
1066+
}
10561067
}
10571068
} else if exc_type.fast_isinstance(vm.ctx.exceptions.base_exception_type) {
10581069
// exc_type is an exception instance
@@ -1063,7 +1074,7 @@ pub(crate) mod _asyncio {
10631074
vm.new_type_error("instance exception may not have a separate value")
10641075
);
10651076
}
1066-
exc_type
1077+
exc_type.downcast().unwrap()
10671078
} else {
10681079
// exc_type is neither a class nor an exception instance
10691080
return Err(vm.new_type_error(format!(
@@ -1075,10 +1086,11 @@ pub(crate) mod _asyncio {
10751086
if let OptionalArg::Present(tb) = exc_tb
10761087
&& !vm.is_none(&tb)
10771088
{
1078-
exc.set_attr(vm.ctx.intern_str("__traceback__"), tb, vm)?;
1089+
exc.as_object()
1090+
.set_attr(vm.ctx.intern_str("__traceback__"), tb, vm)?;
10791091
}
10801092

1081-
Err(exc.downcast().unwrap())
1093+
Err(exc)
10821094
}
10831095

10841096
#[pymethod]
@@ -1840,7 +1852,7 @@ pub(crate) mod _asyncio {
18401852
cls: PyTypeRef,
18411853
args: PyObjectRef,
18421854
vm: &VirtualMachine,
1843-
) -> PyGenericAlias {
1855+
) -> PyResult<PyGenericAlias> {
18441856
PyGenericAlias::from_args(cls, args, vm)
18451857
}
18461858
}
@@ -2405,7 +2417,9 @@ pub(crate) mod _asyncio {
24052417

24062418
// Slow path: look up in the module-level dict for cross-thread queries
24072419
let current_tasks = get_current_tasks_dict(vm)?;
2408-
let dict: PyDictRef = current_tasks.downcast().unwrap();
2420+
let Ok(dict) = current_tasks.downcast::<PyDict>() else {
2421+
return Ok(vm.ctx.none());
2422+
};
24092423

24102424
match dict.get_item(&*loop_obj, vm) {
24112425
Ok(task) => Ok(task),
@@ -2485,15 +2499,17 @@ pub(crate) mod _asyncio {
24852499
#[pyfunction]
24862500
fn _enter_task(loop_: PyObjectRef, task: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
24872501
// Per-thread check, matching CPython's ts->asyncio_running_task
2488-
{
2489-
let running_task = vm.asyncio_running_task.borrow();
2490-
if running_task.is_some() {
2491-
return Err(vm.new_runtime_error(format!(
2492-
"Cannot enter into task {:?} while another task {:?} is being executed.",
2493-
task,
2494-
running_task.as_ref().unwrap()
2495-
)));
2496-
}
2502+
let running_task = vm.asyncio_running_task.borrow().clone();
2503+
if let Some(running_task) = running_task {
2504+
let task_repr = task.repr(vm)?;
2505+
let running_task_repr = running_task.repr(vm)?;
2506+
return Err(vm.new_runtime_error(wtf8_concat!(
2507+
"Cannot enter into task ",
2508+
task_repr.as_wtf8(),
2509+
" while another task ",
2510+
running_task_repr.as_wtf8(),
2511+
" is being executed."
2512+
)));
24972513
}
24982514

24992515
*vm.asyncio_running_task.borrow_mut() = Some(task.clone());

crates/stdlib/src/_queue.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ mod _queue {
282282
cls: PyTypeRef,
283283
args: PyObjectRef,
284284
vm: &VirtualMachine,
285-
) -> PyGenericAlias {
285+
) -> PyResult<PyGenericAlias> {
286286
PyGenericAlias::from_args(cls, args, vm)
287287
}
288288
}

crates/stdlib/src/array.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1234,7 +1234,7 @@ pub mod array {
12341234
cls: PyTypeRef,
12351235
args: PyObjectRef,
12361236
vm: &VirtualMachine,
1237-
) -> PyGenericAlias {
1237+
) -> PyResult<PyGenericAlias> {
12381238
PyGenericAlias::from_args(cls, args, vm)
12391239
}
12401240
}

crates/stdlib/src/blake2.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ pub(crate) use _blake2::module_def;
55
#[pymodule]
66
mod _blake2 {
77
use crate::hashlib::_hashlib::{BlakeHashArgs, local_blake2b, local_blake2s};
8-
use crate::vm::{PyPayload, PyResult, VirtualMachine};
8+
use crate::vm::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule};
99

1010
#[pyattr(name = "_GIL_MINSIZE")]
1111
const GIL_MINSIZE: u16 = 2048;
@@ -43,4 +43,11 @@ mod _blake2 {
4343
fn blake2s(args: BlakeHashArgs, vm: &VirtualMachine) -> PyResult {
4444
Ok(local_blake2s(args, vm)?.into_pyobject(vm))
4545
}
46+
47+
#[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")]
48+
pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py<PyModule>) -> PyResult<()> {
49+
let _ = vm.import("_hashlib", 0);
50+
__module_exec(vm, module);
51+
Ok(())
52+
}
4653
}

crates/stdlib/src/contextvars.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ mod _contextvars {
462462
cls: PyTypeRef,
463463
args: PyObjectRef,
464464
vm: &VirtualMachine,
465-
) -> PyGenericAlias {
465+
) -> PyResult<PyGenericAlias> {
466466
PyGenericAlias::from_args(cls, args, vm)
467467
}
468468
}
@@ -562,7 +562,7 @@ mod _contextvars {
562562
cls: PyTypeRef,
563563
args: PyObjectRef,
564564
vm: &VirtualMachine,
565-
) -> PyGenericAlias {
565+
) -> PyResult<PyGenericAlias> {
566566
PyGenericAlias::from_args(cls, args, vm)
567567
}
568568

crates/stdlib/src/csv.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -779,11 +779,16 @@ mod _csv {
779779
// TODO: Maybe need to update the obj from HashMap
780780
}
781781
DialectItem::Obj(o) => Ok(self.update_py_dialect(o.clone())),
782-
DialectItem::None => {
783-
let g = GLOBAL_HASHMAP.lock();
784-
let res = g.get("excel").unwrap().clone();
785-
Ok(self.update_py_dialect(res))
786-
}
782+
DialectItem::None => Ok(self.update_py_dialect(PyDialect {
783+
delimiter: b',',
784+
quotechar: Some(b'"'),
785+
escapechar: None,
786+
doublequote: true,
787+
skipinitialspace: false,
788+
lineterminator: "\r\n".to_owned(),
789+
quoting: QuoteStyle::Minimal,
790+
strict: false,
791+
})),
787792
}
788793
}
789794

crates/stdlib/src/lzma.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -337,40 +337,43 @@ mod _lzma {
337337
}
338338

339339
fn parse_filter_chain_spec(
340-
filter_specs: Vec<PyObjectRef>,
340+
filter_specs: PyObjectRef,
341341
vm: &VirtualMachine,
342342
) -> PyResult<Filters> {
343343
const LZMA_FILTERS_MAX: usize = 4;
344-
if filter_specs.len() > LZMA_FILTERS_MAX {
344+
let filter_specs_len = filter_specs.length(vm)?;
345+
if filter_specs_len > LZMA_FILTERS_MAX {
345346
return Err(new_lzma_error(
346347
format!("Too many filters - liblzma supports a maximum of {LZMA_FILTERS_MAX}"),
347348
vm,
348349
));
349350
}
350351

352+
let filter_specs = filter_specs.try_sequence(vm)?;
351353
let mut filters = Filters::new();
352-
for spec in &filter_specs {
353-
let filter_id = get_dict_opt_u64(spec, "id", vm)?
354+
for i in 0..filter_specs_len {
355+
let spec = filter_specs.get_item(i as isize, vm)?;
356+
let filter_id = get_dict_opt_u64(&spec, "id", vm)?
354357
.ok_or_else(|| vm.new_value_error("Filter specifier must have an \"id\" entry"))?;
355358

356359
match filter_id {
357360
FILTER_LZMA1 => {
358-
let opts = parse_filter_spec_lzma(spec, vm)?;
361+
let opts = parse_filter_spec_lzma(&spec, vm)?;
359362
filters.lzma1(&opts);
360363
}
361364
FILTER_LZMA2 => {
362-
let opts = parse_filter_spec_lzma(spec, vm)?;
365+
let opts = parse_filter_spec_lzma(&spec, vm)?;
363366
filters.lzma2(&opts);
364367
}
365368
FILTER_DELTA => {
366-
let dist = parse_filter_spec_delta(spec, vm)?;
369+
let dist = parse_filter_spec_delta(&spec, vm)?;
367370
filters
368371
.delta_properties(&[(dist - 1) as u8])
369372
.map_err(|e| catch_lzma_error(e, vm))?;
370373
}
371374
FILTER_X86 | FILTER_POWERPC | FILTER_IA64 | FILTER_ARM | FILTER_ARMTHUMB
372375
| FILTER_SPARC => {
373-
let start_offset = parse_filter_spec_bcj(spec, vm)?;
376+
let start_offset = parse_filter_spec_bcj(&spec, vm)?;
374377
add_bcj_filter(&mut filters, filter_id, start_offset)
375378
.map_err(|e| catch_lzma_error(e, vm))?;
376379
}
@@ -570,7 +573,7 @@ mod _lzma {
570573
#[pyarg(any, optional)]
571574
memlimit: Option<u64>,
572575
#[pyarg(any, optional)]
573-
filters: Option<Vec<PyObjectRef>>,
576+
filters: Option<PyObjectRef>,
574577
}
575578

576579
impl Constructor for LZMADecompressor {
@@ -735,7 +738,7 @@ mod _lzma {
735738
fn init_xz(
736739
check: i32,
737740
preset: u32,
738-
filters: Option<Vec<PyObjectRef>>,
741+
filters: Option<PyObjectRef>,
739742
vm: &VirtualMachine,
740743
) -> PyResult<Stream> {
741744
let real_check =
@@ -751,10 +754,11 @@ mod _lzma {
751754

752755
fn init_alone(
753756
preset: u32,
754-
filter_specs: Option<Vec<PyObjectRef>>,
757+
filter_specs: Option<PyObjectRef>,
755758
vm: &VirtualMachine,
756759
) -> PyResult<Stream> {
757-
if let Some(_filter_specs) = filter_specs {
760+
if let Some(filter_specs) = filter_specs {
761+
filter_specs.length(vm)?;
758762
// TODO: validate single LZMA1 filter and use its options
759763
let options = LzmaOptions::new_preset(preset).map_err(|_| {
760764
new_lzma_error(format!("Invalid compression preset: {preset}"), vm)
@@ -768,10 +772,7 @@ mod _lzma {
768772
}
769773
}
770774

771-
fn init_raw(
772-
filter_specs: Option<Vec<PyObjectRef>>,
773-
vm: &VirtualMachine,
774-
) -> PyResult<Stream> {
775+
fn init_raw(filter_specs: Option<PyObjectRef>, vm: &VirtualMachine) -> PyResult<Stream> {
775776
let filter_specs = filter_specs
776777
.ok_or_else(|| vm.new_value_error("Must specify filters for FORMAT_RAW"))?;
777778
let filters = parse_filter_chain_spec(filter_specs, vm)?;
@@ -788,7 +789,7 @@ mod _lzma {
788789
#[pyarg(any, optional)]
789790
preset: Option<PyObjectRef>,
790791
#[pyarg(any, optional)]
791-
filters: Option<Vec<PyObjectRef>>,
792+
filters: Option<PyObjectRef>,
792793
}
793794

794795
impl Constructor for LZMACompressor {

0 commit comments

Comments
 (0)