Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions crates/monty/src/modules/json/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
exception_private::{ExcType, RunResult},
heap::{DropWithHeap, HeapData, HeapGuard, HeapId, HeapReadOutput},
intern::StaticStrings,
resource::ResourceTracker,
resource::{ResourceTracker, check_repeat_size},
sorting::{apply_permutation, sort_indices},
types::{PyTrait, long_int::check_bigint_str_digits_limit, str::allocate_string},
value::Value,
Expand Down Expand Up @@ -251,13 +251,14 @@ fn parse_indent_value(value: Value, vm: &mut VM<'_, impl ResourceTracker>) -> Ru
match value {
Value::None => Ok(None),
Value::Bool(flag) => Ok(Some(" ".repeat(usize::from(*flag)))),
Value::Int(count) => spaces_from_indent_count(*count),
Value::Int(count) => spaces_from_indent_count(*count, vm.heap.tracker()),
Value::InternString(string_id) => Ok(Some(vm.interns.get_str(*string_id).to_owned())),
Value::Ref(heap_id) => match vm.heap.read(*heap_id) {
HeapReadOutput::Str(string) => Ok(Some(string.get(vm.heap).as_str().to_owned())),
HeapReadOutput::LongInt(long_int) => {
spaces_from_indent_count(long_int.get(vm.heap).to_i64().ok_or_else(ExcType::overflow_c_ssize_t)?)
}
HeapReadOutput::LongInt(long_int) => spaces_from_indent_count(
long_int.get(vm.heap).to_i64().ok_or_else(ExcType::overflow_c_ssize_t)?,
vm.heap.tracker(),
),
_ => Err(ExcType::type_error("indent must be None, an integer or a string")),
},
_ => Err(ExcType::type_error("indent must be None, an integer or a string")),
Expand All @@ -268,12 +269,20 @@ fn parse_indent_value(value: Value, vm: &mut VM<'_, impl ResourceTracker>) -> Ru
///
/// Zero and negative values return an empty indent string, which keeps pretty
/// printing enabled while omitting leading spaces on each line like CPython.
fn spaces_from_indent_count(count: i64) -> RunResult<Option<String>> {
///
/// The requested count is validated against the resource tracker before the
/// repeat is materialized: the indent string is built on the native heap and
/// then repeated once per nesting level by `write_indent`, so an unbounded
/// count would otherwise allocate well past the configured memory limit.
fn spaces_from_indent_count(count: i64, tracker: &impl ResourceTracker) -> RunResult<Option<String>> {
if count <= 0 {
Ok(Some(String::new()))
} else {
match usize::try_from(count) {
Ok(count) => Ok(Some(" ".repeat(count))),
Ok(count) => {
check_repeat_size(count, 1, tracker)?;
Ok(Some(" ".repeat(count)))
}
Err(_) => Err(ExcType::overflow_c_ssize_t()),
}
}
Expand Down
34 changes: 34 additions & 0 deletions crates/monty/tests/resource_limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2039,3 +2039,37 @@ len(x) + len(d) + len(s)
);
assert_eq!(result.unwrap(), MontyObject::Int(300));
}

// === json.dumps resource-limit tests ===

/// Test that `json.dumps(..., indent=N)` is rejected before allocation when the
/// requested indent string would exceed the configured memory limit.
#[test]
fn json_dumps_indent_memory_limit() {
let code = "import json\njson.dumps([1], indent=1000000)";
let ex = MontyRun::new(code.to_owned(), "test.py", vec![]).unwrap();

let limits = ResourceLimits::new().max_memory(100_000);
let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);

assert!(result.is_err(), "large indent should be rejected before allocation");
let exc = result.unwrap_err();
assert_eq!(exc.exc_type(), ExcType::MemoryError);
assert!(
exc.message().is_some_and(|m| m.contains("memory limit exceeded")),
"expected memory limit error, got: {exc}"
);
}

/// Test that small `json.dumps(..., indent=N)` works within limits.
#[test]
fn json_dumps_indent_within_limit() {
let code = "import json\njson.dumps([1, 2], indent=2)";
let ex = MontyRun::new(code.to_owned(), "test.py", vec![]).unwrap();

let limits = ResourceLimits::new().max_memory(100_000);
let result = ex.run(vec![], LimitedTracker::new(limits), PrintWriter::Stdout);

assert!(result.is_ok(), "small indent should succeed");
assert_eq!(result.unwrap(), MontyObject::String("[\n 1,\n 2\n]".to_owned()));
}
Loading