Skip to content

Commit b15d7bc

Browse files
authored
Align LZMA compressor and filter validation (#9582)
* Align LZMA compressor and filter validation Use CPython exception types for incompatible compressor options and validate filter properties with the keyword parser ordering, including mutation during index conversion. Assisted-by: OpenAI Codex:model version not exposed * Use direct dict lookup for filter properties Assisted-by: OpenAI Codex:model version not exposed * Retire repaired LZMA expected failures Apply update_lib.cmd_auto_mark.apply_test_changes to the two repeated unexpected successes observed in the exact-head Ubuntu CI run. Preserve all test bodies and assertions. Assisted-by: OpenAI Codex:model version unavailable
1 parent dc84268 commit b15d7bc

2 files changed

Lines changed: 100 additions & 24 deletions

File tree

‎Lib/test/test_lzma.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,6 @@ class CompressDecompressFunctionTestCase(unittest.TestCase):
389389

390390
# Test error cases:
391391

392-
@unittest.expectedFailure # TODO: RUSTPYTHON; lzma.LZMAError: Failed to initialize encoder
393392
def test_bad_args(self):
394393
self.assertRaises(TypeError, lzma.compress)
395394
self.assertRaises(TypeError, lzma.compress, [])
@@ -689,7 +688,6 @@ def test_init_bad_filter_spec(self):
689688
LZMAFile(BytesIO(), "w",
690689
filters=[{"id": lzma.FILTER_X86, "foo": 0}])
691690

692-
@unittest.expectedFailure # TODO: RUSTPYTHON; lzma.LZMAError: Invalid format
693691
def test_init_with_preset_and_filters(self):
694692
with self.assertRaises(ValueError):
695693
LZMAFile(BytesIO(), "w", format=lzma.FORMAT_RAW,

‎crates/stdlib/src/lzma.rs‎

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -215,25 +215,109 @@ mod _lzma {
215215
spec: &PyObject,
216216
vm: &VirtualMachine,
217217
) -> PyResult<backend::FilterSpec> {
218-
let mut parsed = filter_spec_with_id(spec, vm)?;
219-
match parsed.id {
220-
FILTER_LZMA1 => {
221-
parsed.preset = get_dict_opt_u32(spec, "preset", vm)?;
222-
parsed.lc = get_dict_opt_u32(spec, "lc", vm)?;
223-
parsed.lp = get_dict_opt_u32(spec, "lp", vm)?;
224-
parsed.pb = get_dict_opt_u32(spec, "pb", vm)?;
225-
parsed.dict_size = get_dict_opt_u32(spec, "dict_size", vm)?;
218+
fn take_keyword(
219+
dict: &PyDict,
220+
key: &str,
221+
remaining: &mut usize,
222+
error: &str,
223+
vm: &VirtualMachine,
224+
) -> PyResult<Option<PyObjectRef>> {
225+
if *remaining == 0 {
226+
return Ok(None);
226227
}
227-
FILTER_LZMA2 => {
228+
let value = dict
229+
.inner_getitem_opt(key, vm)
230+
.map_err(|_| vm.new_value_error(error))?;
231+
if value.is_some() {
232+
*remaining -= 1;
233+
}
234+
Ok(value)
235+
}
236+
237+
fn field(
238+
dict: &PyDict,
239+
key: &str,
240+
remaining: &mut usize,
241+
error: &str,
242+
vm: &VirtualMachine,
243+
) -> PyResult<Option<u32>> {
244+
take_keyword(dict, key, remaining, error, vm)?
245+
.map(|value| {
246+
value
247+
.try_into_value::<u32>(vm)
248+
.map_err(|_| vm.new_value_error(error))
249+
})
250+
.transpose()
251+
}
252+
253+
let mut parsed = filter_spec_with_id(spec, vm)?;
254+
let dict = spec
255+
.downcast_ref::<PyDict>()
256+
.expect("filter ID was read from a dict");
257+
let (max, error) = match parsed.id {
258+
FILTER_LZMA1 | FILTER_LZMA2 => {
228259
parsed.preset = get_dict_opt_u32(spec, "preset", vm)?;
229-
parsed.dict_size = get_dict_opt_u32(spec, "dict_size", vm)?;
260+
if let Some(preset) = parsed.preset
261+
&& preset & !PRESET_EXTREME > 9
262+
{
263+
return Err(new_lzma_error(
264+
format!("Invalid compression preset: {preset}"),
265+
vm,
266+
));
267+
}
268+
(10, "Invalid filter specifier for LZMA filter")
230269
}
231-
FILTER_DELTA => parsed.dist = get_dict_opt_u32(spec, "dist", vm)?,
270+
FILTER_DELTA => (2, "Invalid filter specifier for delta filter"),
232271
FILTER_X86 | FILTER_POWERPC | FILTER_IA64 | FILTER_ARM | FILTER_ARMTHUMB
233-
| FILTER_SPARC => {
234-
parsed.start_offset = get_dict_opt_u32(spec, "start_offset", vm)?;
272+
| FILTER_SPARC => (2, "Invalid filter specifier for BCJ filter"),
273+
_ => return Ok(parsed),
274+
};
275+
// PyArg_ParseTupleAndKeywords snapshots the keyword count and stops
276+
// looking up optional fields when all of those keywords are consumed.
277+
let mut remaining = dict.__len__();
278+
if remaining > max {
279+
return Err(vm.new_value_error(error));
280+
}
281+
let _ = take_keyword(dict, "id", &mut remaining, error, vm)?;
282+
if matches!(parsed.id, FILTER_LZMA1 | FILTER_LZMA2) {
283+
let _ = take_keyword(dict, "preset", &mut remaining, error, vm)?;
284+
parsed.dict_size = field(dict, "dict_size", &mut remaining, error, vm)?;
285+
parsed.lc = field(dict, "lc", &mut remaining, error, vm)?;
286+
parsed.lp = field(dict, "lp", &mut remaining, error, vm)?;
287+
parsed.pb = field(dict, "pb", &mut remaining, error, vm)?;
288+
parsed.mode = field(dict, "mode", &mut remaining, error, vm)?;
289+
parsed.nice_len = field(dict, "nice_len", &mut remaining, error, vm)?;
290+
parsed.mf = field(dict, "mf", &mut remaining, error, vm)?;
291+
parsed.depth = field(dict, "depth", &mut remaining, error, vm)?;
292+
if remaining > 0 {
293+
check_filter_spec_keys(
294+
spec,
295+
&[
296+
"id",
297+
"preset",
298+
"dict_size",
299+
"lc",
300+
"lp",
301+
"pb",
302+
"mode",
303+
"nice_len",
304+
"mf",
305+
"depth",
306+
],
307+
error,
308+
vm,
309+
)?;
310+
}
311+
} else if parsed.id == FILTER_DELTA {
312+
parsed.dist = field(dict, "dist", &mut remaining, error, vm)?;
313+
if remaining > 0 {
314+
check_filter_spec_keys(spec, &["id", "dist"], error, vm)?;
315+
}
316+
} else {
317+
parsed.start_offset = field(dict, "start_offset", &mut remaining, error, vm)?;
318+
if remaining > 0 {
319+
check_filter_spec_keys(spec, &["id", "start_offset"], error, vm)?;
235320
}
236-
_ => {}
237321
}
238322
Ok(parsed)
239323
}
@@ -444,16 +528,10 @@ mod _lzma {
444528

445529
fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
446530
if args.format != FORMAT_XZ && args.check != -1 && args.check != CHECK_NONE {
447-
return Err(new_lzma_error(
448-
"Integrity checks are only supported by FORMAT_XZ",
449-
vm,
450-
));
531+
return Err(vm.new_value_error("Integrity checks are only supported by FORMAT_XZ"));
451532
}
452533
if args.preset.is_some() && args.filters.is_some() {
453-
return Err(new_lzma_error(
454-
"Cannot specify both preset and filter chain",
455-
vm,
456-
));
534+
return Err(vm.new_value_error("Cannot specify both preset and filter chain"));
457535
}
458536
let preset = match args.preset {
459537
Some(preset) => preset.try_into_value::<u32>(vm)?,

0 commit comments

Comments
 (0)