fix(edit): refuse a mark on top of over-deep content before changing the page

This commit is contained in:
aiongg
2026-10-22 23:35:40 +00:00
parent 7a466ff5fc
commit 54152aed87
4 changed files with 60 additions and 3 deletions
+3
View File
@@ -1050,6 +1050,9 @@ fn write(doc: &mut Document, page: usize, c: &Content, obj: Option<ObjRef>) -> R
}
let taken = pres.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default();
let (content, res) = draw(doc, c, view, &taken, obj.is_some())?;
if obj.is_none() {
crate::check_on_top(doc, p)?;
}
for (k, v) in res.iter() {
let mut sub = pres.get(k).map(|s| doc.resolve(s)).and_then(|s| s.as_dict().cloned()).unwrap_or_default();
for (n2, o) in v.as_dict().into_iter().flat_map(|d| d.iter()) {
+1
View File
@@ -257,6 +257,7 @@ fn flatten_which(doc: &mut Document, pages: &[usize], which: Which) -> Result<us
.map(|(_, e)| e.clone())
.collect();
if !content.is_empty() {
crate::check_on_top(doc, &page)?;
// The page's own resources, with the appearances as XObjects.
let mut res = page.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
let mut xo = res.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default();
+30 -3
View File
@@ -631,9 +631,7 @@ fn place_tagged(doc: &mut Document, page: &pdfcraft_model::Page, tag: &str, cont
// closing it changes nothing already drawn.
Isolation::Unknown { unclosed, marked } => (behind_marks < list.len()).then_some((behind_marks, unclosed, marked)),
// Content left inside an optional-content sequence it cannot close could be hidden.
Isolation::TooDeep => {
return Err(EditError::Invalid("the page's marked content is nested too deeply to add anything on top of it".into()));
}
Isolation::TooDeep => return Err(too_deep_on_top()),
}
};
let mark_ref = doc.add(Object::Stream(Stream::flate(tagged(tag), &content)));
@@ -661,6 +659,26 @@ fn place_tagged(doc: &mut Document, page: &pdfcraft_model::Page, tag: &str, cont
Ok(mark_ref)
}
fn too_deep_on_top() -> EditError {
EditError::Invalid("the page's marked content is nested too deeply to add anything on top of it".into())
}
/// The refusal [`place_tagged`] gives for a mark on top of `page`, checked before an edit changes
/// anything (removing old marks, adding resources) so a refused edit leaves the document as it was.
fn check_on_top(doc: &Document, page: &pdfcraft_model::Page) -> Result<(), EditError> {
let obj = doc.get(page.obj);
let list = match obj.as_dict().and_then(|d| d.get(b"Contents")) {
None => return Ok(()),
Some(Object::Ref(r)) => doc.get(*r).as_array().cloned().unwrap_or_else(|| vec![Object::Ref(*r)]),
Some(Object::Array(a)) => a.clone(),
Some(other) => vec![other.clone()],
};
match isolation(doc, &list) {
Isolation::TooDeep => Err(too_deep_on_top()),
_ => Ok(()),
}
}
/// Put content tagged `tag` first inside the q/Q wrapper, so it applies to the page's original
/// content and nothing else (a header's shrink). A page without a wrapper has no original content
/// on it and is left alone.
@@ -680,6 +698,7 @@ fn place_inside_wrapper(doc: &mut Document, page: &pdfcraft_model::Page, tag: &s
pub fn stamp(doc: &mut Document, page: usize, tag: &str, content: Vec<u8>) -> Result<ObjRef, EditError> {
let all = page_list(doc);
check(&[page], all.len())?;
check_on_top(doc, &all[page])?;
add_resources(doc, &all[page], None, Some(&content))?;
let p = page_list(doc).swap_remove(page);
place_tagged(doc, &p, tag, content, false)
@@ -759,6 +778,9 @@ pub fn add_header_footer(doc: &mut Document, pages: &[usize], hf: &HeaderFooter,
.iter()
.map(|&i| hf.shrink.then(|| shrink_matrix(hf, all[i].display_size(doc), all[i].view_matrix(doc))).transpose())
.collect::<Result<Vec<_>, _>>()?;
for &i in pages {
check_on_top(doc, &all[i])?;
}
if replace {
remove_marks(doc, pages, MarkKind::HeaderFooter)?;
}
@@ -847,6 +869,11 @@ pub fn add_watermark(doc: &mut Document, pages: &[usize], wm: &Watermark, replac
{
return Err(EditError::Invalid("invalid watermark settings".into()));
}
if !wm.behind {
for &i in pages {
check_on_top(doc, &all[i])?;
}
}
if replace {
remove_marks(doc, pages, MarkKind::Watermark)?;
}
+26
View File
@@ -131,3 +131,29 @@ fn depth_budget_bounds_emitted_closures_and_overflow_is_refused_on_top() {
place_tagged(&mut doc, &page, MarkKind::Background.tag(), b"q Q\n".to_vec(), true).unwrap();
}
}
/// Marks refused on top of content nested past the depth budget leave the page as it was: old
/// marks are not removed and no resources are added before the refusal.
#[test]
fn a_mark_refused_on_top_changes_nothing() {
let mut over = b"/Artifact BMC\n".repeat(ISOLATION_MAX_DEPTH);
over.extend_from_slice(b"/Artifact BMC\n");
let mut doc = fixture(&[&over]);
let behind = Watermark { text: "OLD".into(), behind: true, ..Watermark::default() };
add_watermark(&mut doc, &[0], &behind, false).unwrap();
let page = page_list(&doc).remove(0).obj;
let before = (*doc.get(page)).clone();
let on_top = Watermark { text: "NEW".into(), ..Watermark::default() };
assert!(matches!(add_watermark(&mut doc, &[0], &on_top, true), Err(EditError::Invalid(_))));
assert_eq!((*doc.get(page)).clone(), before, "the old watermark stays");
let hf = HeaderFooter {
text: [String::new(), "Top".into(), String::new(), String::new(), String::new(), String::new()],
font: added::Family::Times,
..HeaderFooter::default()
};
assert!(matches!(add_header_footer(&mut doc, &[0], &hf, true, &Context::default()), Err(EditError::Invalid(_))));
assert_eq!((*doc.get(page)).clone(), before, "no fonts are added");
let mut stamped = doc.clone();
assert!(matches!(stamp(&mut stamped, 0, "Probe", b"/PCTimes 9 Tf\n".to_vec()), Err(EditError::Invalid(_))));
assert_eq!((*stamped.get(page)).clone(), before, "no fonts are added");
}