Merge branch 'main' into fix/sec-optimize-never-upsample

This commit is contained in:
Dylan M. Taylor
2026-11-03 17:23:54 -05:00
committed by GitHub
5 changed files with 59 additions and 6 deletions
+7 -1
View File
@@ -55,7 +55,10 @@ impl Document {
if w <= 0 || h <= 0 || w > i64::from(MAX_SIDE) || h > i64::from(MAX_SIDE) || w.saturating_mul(h) > MAX_PIXELS as i64 {
return Err(SignatureImageError::PlacedSize.to_string());
}
let (_, bytes) = pdfcraft_create::image_file(&editor.cos, picture)?;
// Decoded within an allowance sized for the small picture a signature is, not the
// document's: a stream that inflates past it is refused before it is held whole.
let (_, bytes) =
pdfcraft_create::image_file_within(&editor.cos, picture, &mut pdfcraft_cos::ImageBudget::with_work(SIGNATURE_IMAGE_WORK))?;
let image = SignatureImage::from_bytes(&bytes).map_err(|e| e.to_string())?;
let opacity =
pdfcraft_annot::props(&editor.cos, page, index).map(|p| p.opacity).filter(|v| v.is_finite()).unwrap_or(1.0).clamp(0.0, 1.0) as f32;
@@ -90,6 +93,9 @@ pub fn upright_box(rotation: i64, at: [f64; 2], width: f64, height: f64) -> Opti
/// Saved signatures live in settings, so both source and normalized PNG are bounded.
pub const MAX_SIGNATURE_IMAGE_BYTES: usize = 4 << 20;
const MAX_PIXELS: u64 = 4 << 20;
/// Steps of decoding the signature's picture may take: every stage of its filters, for a
/// picture of at most [`MAX_PIXELS`] samples, fits in it many times over.
const SIGNATURE_IMAGE_WORK: u64 = 1 << 28;
const MAX_SIDE: u32 = 4096;
/// Larger sources (scans, phone photos) are decoded within these bounds, then scaled down.
const MAX_SOURCE_SIDE: u32 = 8192;
+4 -2
View File
@@ -746,10 +746,12 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
&& n.tag_name().name() == "template"
&& (n.tag_name().namespace().is_some_and(|ns| ns.contains("xfa-template")) || children(*n, "subform").next().is_some())
};
// The template is a packet of the XDP itself, a child of its root: one nested deeper is
// data (an imported dataset, say), which must not supply the form's scripts.
let template = if is_template(&root) {
root
} else {
root.descendants().find(is_template).ok_or_else(|| XfaError::Malformed("no <template> element".into()))?
root.children().find(is_template).ok_or_else(|| XfaError::Malformed("no <template> element".into()))?
};
let mut warnings = Vec::new();
let sf = children(template, "subform").next().ok_or_else(|| XfaError::Malformed("the template has no root subform".into()))?;
@@ -760,7 +762,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
}
// The form packet beside the template holds the state the form was saved in.
let saved = root
.descendants()
.children()
.find(|n| n.is_element() && n.tag_name().name() == "form" && n.tag_name().namespace().is_some_and(|ns| ns.contains("xfa-form")));
if let Some(saved_root) = saved.and_then(|f| children(f, "subform").next()) {
if let Some(p) = saved_root.attribute("presence") {
+6 -3
View File
@@ -17,7 +17,9 @@ mod strip;
pub use common::jpeg_size;
pub(crate) use common::rounded;
use common::{all_faces, content_hash, empty_form, hex32, jpeg_info, js_action, n, parse_hex32, rgb, rgb_array, standard_font, state_name};
use common::{
all_faces, content_hash, empty_form, hex32, jpeg_info, js_action, js_string, n, parse_hex32, rgb, rgb_array, standard_font, state_name,
};
pub(crate) use strip::USAGE_RIGHTS_REMOVED;
use strip::drop_usage_rights;
pub use strip::{Stripped, XfaShape, existing_layout, strip, xfa_shape};
@@ -205,8 +207,9 @@ impl Emitter<'_> {
}
if let WidgetKind::Date(pattern) = &w.kind {
let mut aa = Dict::new();
aa.set(b"F".to_vec(), js_action(&format!("AFDate_FormatEx(\"{pattern}\");")));
aa.set(b"K".to_vec(), js_action(&format!("AFDate_KeystrokeEx(\"{pattern}\");")));
let pattern = js_string(pattern);
aa.set(b"F".to_vec(), js_action(&format!("AFDate_FormatEx({pattern});")));
aa.set(b"K".to_vec(), js_action(&format!("AFDate_KeystrokeEx({pattern});")));
d.set(b"AA".to_vec(), Object::Dict(aa));
}
}
+32
View File
@@ -192,9 +192,41 @@ pub(super) fn empty_form(doc: &mut Document, w: f64, h: f64) -> Object {
Object::Ref(doc.add(Object::Stream(Stream::from_raw(d, Vec::new()))))
}
/// `s` as a JavaScript string literal, quotes included: a quote, a backslash or a line break in
/// it stays text instead of ending the string and running as code.
pub(super) fn js_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' | '\\' => {
out.push('\\');
out.push(c);
}
c if c.is_control() || c == '\u{2028}' || c == '\u{2029}' => out.push_str(&format!("\\u{:04x}", u32::from(c))),
c => out.push(c),
}
}
out.push('"');
out
}
pub(super) fn js_action(js: &str) -> Object {
let mut a = Dict::new();
a.set(b"S".to_vec(), Object::name("JavaScript"));
a.set(b"JS".to_vec(), Object::String(PdfString::literal(js.as_bytes().to_vec())));
Object::Dict(a)
}
#[cfg(test)]
mod js_string_tests {
use super::js_string;
/// A date pattern that tries to close the string and run code stays one string literal.
#[test]
fn a_pattern_cannot_leave_its_string() {
assert_eq!(js_string("yyyy-mm-dd"), "\"yyyy-mm-dd\"");
assert_eq!(js_string("x\");app.alert(1);//"), "\"x\\\");app.alert(1);//\"");
assert_eq!(js_string("a\\\nb\u{2028}"), "\"a\\\\\\u000ab\\u2028\"");
}
}
+10
View File
@@ -4531,3 +4531,13 @@ fn a_container_is_as_wide_as_a_turned_draw_stands() {
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
}
}
/// Only a template that is a packet of the XDP is the form's: one inside imported data (a
/// dataset holding XML that looks like a template) is not read, and neither are its scripts.
#[test]
fn a_template_inside_the_data_is_not_the_form() {
let nested = "<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"evil\"><event activity=\"initialize\"><script>xfa.host.messageBox(1)</script></event></subform></template></xfa:data></xfa:datasets></xdp:xdp>";
assert!(parse(nested).is_err(), "no template packet");
let real = "<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"form1\"/></template></xdp:xdp>";
assert_eq!(parse(real).unwrap().0.root.common.name.as_deref(), Some("form1"));
}