From 7aba0e15af4f808818b0094c21ae7365a40c0fbf Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:10:59 +0000 Subject: [PATCH 1/2] Saturate table alternating-fill and index paragraph counts Found while testing crates/engine/src/cmd/table.rs:1396-1403 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/index.rs | 4 +++- crates/engine/src/cmd/table.rs | 8 ++++---- crates/engine/src/tests_table.rs | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/index.rs b/crates/engine/src/cmd/index.rs index bb6f0991..6ca36ee7 100644 --- a/crates/engine/src/cmd/index.rs +++ b/crates/engine/src/cmd/index.rs @@ -72,7 +72,9 @@ fn add_reference(s: &mut Session, p: &Value) -> Result { "currentPage" => IndexRange::CurrentPage, "toEndOfStory" => IndexRange::ToEndOfStory, "suppressPageRange" => IndexRange::SuppressPageRange, - "nextParagraphs" => IndexRange::NextParagraphs(p.get("paragraphs").and_then(Value::as_u64).unwrap_or(1) as u32), + "nextParagraphs" => { + IndexRange::NextParagraphs(p.get("paragraphs").and_then(Value::as_u64).map_or(1, |x| u32::try_from(x).unwrap_or(u32::MAX))) + } "see" => IndexRange::See(target()), "seeAlso" => IndexRange::SeeAlso(target()), r => return Err(bad("index.addReference", format!("unknown range `{r}`"))), diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index 7049c2f3..0780f65f 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -1393,14 +1393,14 @@ fn parse_alt(v: &Value) -> Option { } let d = AltFills::default(); Some(AltFills { - first: v.get("first").and_then(Value::as_u64).map_or(d.first, |x| x as u32), + first: v.get("first").and_then(Value::as_u64).map_or(d.first, |x| u32::try_from(x).unwrap_or(u32::MAX)), first_color: color_param(v, "firstColor").unwrap_or(d.first_color), first_tint: v.get("firstTint").and_then(Value::as_f64).map_or(d.first_tint, |x| x as f32), - next: v.get("next").and_then(Value::as_u64).map_or(d.next, |x| x as u32), + next: v.get("next").and_then(Value::as_u64).map_or(d.next, |x| u32::try_from(x).unwrap_or(u32::MAX)), next_color: color_param(v, "nextColor").unwrap_or(d.next_color), next_tint: v.get("nextTint").and_then(Value::as_f64).map_or(d.next_tint, |x| x as f32), - skip_first: v.get("skipFirst").and_then(Value::as_u64).map_or(0, |x| x as u32), - skip_last: v.get("skipLast").and_then(Value::as_u64).map_or(0, |x| x as u32), + skip_first: v.get("skipFirst").and_then(Value::as_u64).map_or(0, |x| u32::try_from(x).unwrap_or(u32::MAX)), + skip_last: v.get("skipLast").and_then(Value::as_u64).map_or(0, |x| u32::try_from(x).unwrap_or(u32::MAX)), }) } diff --git a/crates/engine/src/tests_table.rs b/crates/engine/src/tests_table.rs index 59d2f267..2fc4fd33 100644 --- a/crates/engine/src/tests_table.rs +++ b/crates/engine/src/tests_table.rs @@ -1097,3 +1097,26 @@ fn convert_rows_keeps_merged_cells_on_one_side() { assert!(!convert_enabled(&s, "Body")); check(&s); } + +#[test] +fn alternating_fill_counts_saturate_instead_of_wrapping() { + let (mut s, _, _) = session_with_frame(); + s.execute("table.insert", &json!({"rows": 3, "cols": 2})).unwrap(); + s.execute("table.options", &json!({"altRows": {"first": 4294967297u64, "next": 1, "skipFirst": 4294967296u64, "skipLast": 9999999999u64}})) + .unwrap(); + let t = s.execute("table.get", &json!({})).unwrap(); + let a = &t["options"]["altRows"]; + assert_eq!(a["first"], u32::MAX); + assert_eq!(a["skipFirst"], u32::MAX); + assert_eq!(a["skipLast"], u32::MAX); + assert_eq!(a["next"], 1); +} + +#[test] +fn index_next_paragraphs_saturates_instead_of_wrapping() { + let (mut s, _, _) = session_with_frame(); + s.execute("index.addReference", &json!({"topic": "Intro", "range": "nextParagraphs", "paragraphs": 4294967296u64})).unwrap(); + let l = s.execute("index.list", &json!({})).unwrap(); + assert_eq!(l[0]["range"]["type"], "nextParagraphs"); + assert_eq!(l[0]["range"]["value"], u32::MAX); +} From 50161d0d42c6c64e8b4d865514e07ab40d78d935 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 23 Oct 2026 17:15:22 -0400 Subject: [PATCH 2/2] Index: next-paragraph ranges saturate and read the paragraph list with get Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/index.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/index.rs b/crates/engine/src/cmd/index.rs index 6ca36ee7..38a6bc49 100644 --- a/crates/engine/src/cmd/index.rs +++ b/crates/engine/src/cmd/index.rs @@ -105,8 +105,9 @@ fn resolve(s: &Session, d: &Document, skip: Option) -> Vec<(StoryId, us IndexRange::ToEndOfStory => page(st.len().saturating_sub(1)), IndexRange::NextParagraphs(n) => { let ranges = st.para_ranges(); - let pi = (st.para_at(pos) + *n as usize).min(ranges.len() - 1); - page(ranges[pi].end.saturating_sub(1).max(ranges[pi].start)) + let n = usize::try_from(*n).unwrap_or(usize::MAX); + let pi = st.para_at(pos).saturating_add(n).min(ranges.len().saturating_sub(1)); + ranges.get(pi).and_then(|r| page(r.end.saturating_sub(1).max(r.start))) } _ => start, };