mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-07 13:22:42 +00:00
Merge branch 'main' into fix/sec-optimize-never-upsample
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user