use std::time::{Duration, Instant};
use stet_core::context::Context;
use stet_core::error::PsError;
fn run(source: &str, timeout: Option<Duration>) -> Result<(), PsError> {
let mut ctx = Context::new();
stet_ops::build_system_dict(&mut ctx);
ctx.exec_sync_fn = Some(stet_engine::eval::exec_sync);
ctx.set_timeout(timeout);
stet_engine::eval::parse_and_exec(&mut ctx, source.as_bytes())
}
#[test]
fn deeply_nested_procedure_does_not_overflow_the_stack() {
let source = format!("{}{} pop", "{".repeat(200_000), "}".repeat(200_000));
let _ = run(&source, None);
}
#[test]
fn self_referential_tint_transform_does_not_overflow_the_stack() {
let source = "\
/tintproc { } def\n\
/CS [ /Separation /Spot /DeviceGray { tintproc } ] def\n\
/tintproc { 0.5 CS setcolorspace 0.5 setcolor } def\n\
CS setcolorspace 0.5 setcolor\n";
let _ = run(source, None);
}
#[test]
fn infinite_loop_stops_at_the_deadline() {
let start = Instant::now();
let err = run("{ } loop", Some(Duration::from_millis(500)));
let elapsed = start.elapsed();
assert!(
matches!(err, Err(PsError::Timeout)),
"expected Timeout, got {err:?}"
);
assert!(
elapsed < Duration::from_secs(10),
"deadline was not honoured; ran for {elapsed:?}"
);
}
#[test]
fn unbounded_arithmetic_stops_at_the_deadline() {
let err = run(
"/n 0 def { /n n 1 add def } loop",
Some(Duration::from_millis(500)),
);
assert!(
matches!(err, Err(PsError::Timeout)),
"expected Timeout, got {err:?}"
);
}
#[test]
fn no_deadline_means_no_limit() {
assert!(
run("/n 0 def 1 1 200000 { pop /n n 1 add def } for", None).is_ok(),
"a long but terminating program must complete when no timeout is set"
);
}
#[test]
fn ordinary_nesting_still_runs() {
assert!(run("{ { { 1 2 add } exec } exec } exec pop", None).is_ok());
}
fn draws_an_image(source: &str) -> bool {
use stet_core::display_list::DisplayElement;
let mut ctx = Context::new();
stet_ops::build_system_dict(&mut ctx);
ctx.exec_sync_fn = Some(stet_engine::eval::exec_sync);
let device = stet_render::SkiaDevice::new(64, 64);
ctx.device = Some(Box::new(device));
let _ = stet_engine::eval::parse_and_exec(&mut ctx, source.as_bytes());
ctx.display_list
.elements()
.iter()
.any(|e| matches!(e, DisplayElement::Image { .. }))
}
#[test]
fn oversized_ps_images_are_refused() {
for (label, source) in [
(
"image",
"2000000000 2000000000 8 [1 0 0 1 0 0] { <00> } image",
),
(
"imagemask",
"2000000000 2000000000 true [1 0 0 1 0 0] { <00> } imagemask",
),
(
"colorimage",
"2000000000 2000000000 8 [1 0 0 1 0 0] { <00> } false 3 colorimage",
),
] {
assert!(
!draws_an_image(source),
"{label} must refuse a 2e9 x 2e9 image, not attempt it"
);
}
}
#[test]
fn prepress_scale_ps_image_is_accepted() {
let source = "\
/row 24000 string def\n\
0 1 23999 { row exch 128 put } for\n\
24000 16800 8 [24000 0 0 -16800 0 16800] { row } image\n";
assert!(
draws_an_image(source),
"a 24000x16800 image (403M px) is ordinary prepress and must draw"
);
}
fn run_with_vm_cap(source: &str, max_local_vm: usize) -> bool {
let mut ctx = Context::new();
stet_ops::build_system_dict(&mut ctx);
ctx.exec_sync_fn = Some(stet_engine::eval::exec_sync);
ctx.max_local_vm = max_local_vm;
ctx.set_timeout(Some(Duration::from_secs(20)));
stet_engine::eval::parse_and_exec(&mut ctx, source.as_bytes()).is_ok()
}
#[test]
fn oversized_single_allocation_is_refused() {
assert!(
!run_with_vm_cap("256000000 string pop", 64 * 1024 * 1024),
"a string far past the ceiling must raise VMerror"
);
assert!(
!run_with_vm_cap("64000000 array pop", 64 * 1024 * 1024),
"an array far past the ceiling must raise VMerror"
);
}
#[test]
fn accumulated_allocation_is_refused() {
assert!(
!run_with_vm_cap("{ 1000000 string pop } loop", 64 * 1024 * 1024),
"repeated small allocations must eventually hit the ceiling"
);
}
#[test]
fn global_vm_does_not_bypass_the_ceiling() {
assert!(
!run_with_vm_cap(
"true setglobal { 1000000 string pop } loop",
64 * 1024 * 1024
),
"allocating in global VM must still count against the ceiling"
);
}
#[test]
fn allocation_within_the_ceiling_succeeds() {
assert!(
run_with_vm_cap("1000000 string pop 100000 array pop", 64 * 1024 * 1024),
"a 1 MB string and a 100k array are ordinary and must not be refused"
);
}
#[test]
fn default_ceiling_admits_substantial_allocation() {
let mut ctx = Context::new();
stet_ops::build_system_dict(&mut ctx);
ctx.exec_sync_fn = Some(stet_engine::eval::exec_sync);
let expected: u64 = if usize::BITS >= 64 {
8 * 1024 * 1024 * 1024
} else {
(usize::MAX / 4) as u64
};
assert!(
ctx.max_local_vm as u64 >= expected,
"default MaxLocalVM should be generous; got {}",
ctx.max_local_vm
);
assert!(
stet_engine::eval::parse_and_exec(&mut ctx, b"0 1 63 { pop 1000000 string pop } for")
.is_ok(),
"64 MB of allocation must be fine under the default ceiling"
);
}