mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Merge pull request #2936 from dotCooCoo/fix/editing-data-hash-reads-utf16-desc
PDF and .ai import: editing data with a UTF-16 description is checked against its pages
This commit is contained in:
@@ -69,6 +69,33 @@ fn pages_changed_elsewhere_open_as_artwork_with_a_warning() {
|
||||
assert_eq!(r["warnings"][0], load::EDITING_DAMAGED, "{r}");
|
||||
}
|
||||
|
||||
/// `pdf` with its editing file's description rewritten by another app in UTF-16BE with a byte
|
||||
/// order mark: "hash " and the hash it held, padded with spaces to its length.
|
||||
fn utf16_described(pdf: &[u8]) -> Vec<u8> {
|
||||
let from = b"(VectorCraft editing data; page hash ";
|
||||
let at = pdf.windows(from.len()).position(|w| w == from).unwrap_or_else(|| panic!("no description"));
|
||||
let (hash, len) = (&pdf[at + from.len()..at + from.len() + 16], from.len() + 17);
|
||||
let mut desc = b"(\xFE\xFF".to_vec();
|
||||
desc.extend(b"hash ".iter().chain(hash).flat_map(|&c| [0, c]));
|
||||
desc.push(b')');
|
||||
desc.resize(len, b' ');
|
||||
patched(pdf, &pdf[at..at + len], &desc)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_changed_elsewhere_are_noticed_with_a_utf16_description() {
|
||||
let mut s = rich();
|
||||
let pdf = b64(&s.execute("document.exportPdf", &json!({"compression": {"compressText": false}})).unwrap());
|
||||
let ai = b64(&s.execute("document.export", &json!({"format": "ai", "compress": false})).unwrap());
|
||||
for (name, file) in [("edited.pdf", pdf), ("edited.ai", ai)] {
|
||||
let r = open(&mut s, name, &utf16_described(&file));
|
||||
assert_eq!((&r["warnings"], &r["restored"]), (&json!([]), &json!(true)), "{name}: the same pages: {r}");
|
||||
let edited = patched(&file, b"/MediaBox[0 0 300 200]", b"/MediaBox[0 0 300 201]");
|
||||
let r = open(&mut s, name, &utf16_described(&edited));
|
||||
assert_eq!((&r["warnings"][0], &r["restored"]), (&json!(load::EDITING_STALE), &json!(false)), "{name}: {r}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_legacy_payload_name_is_accepted() {
|
||||
let mut s = rich();
|
||||
|
||||
@@ -13,6 +13,7 @@ use krilla::metadata::DateTime;
|
||||
|
||||
use crate::PdfError;
|
||||
use crate::import_color::items;
|
||||
use crate::import_scan::text_string;
|
||||
use crate::lab_spot::find;
|
||||
|
||||
/// The embedded file holding the native document (its name among the PDF's attachments).
|
||||
@@ -84,8 +85,9 @@ pub(crate) fn editing_in(pdf: &Pdf) -> Option<Editing> {
|
||||
let files = spec.get::<Dict<'_>>(b"EF")?;
|
||||
let stream = files.get::<Stream<'_>>(b"F").or_else(|| files.get::<Stream<'_>>(b"UF"))?;
|
||||
let data = stream.decoded().map(|d| d.into_owned()).unwrap_or_default();
|
||||
// The hash ends the description; editing data described otherwise is trusted.
|
||||
let desc = spec.get::<PdfString<'_>>(b"Desc").map(|d| String::from_utf8_lossy(d.as_bytes()).into_owned()).unwrap_or_default();
|
||||
// The hash ends the description (a text string, in any of its encodings); editing data
|
||||
// described otherwise is trusted.
|
||||
let desc = spec.get::<PdfString<'_>>(b"Desc").map(|d| text_string(d.as_bytes())).unwrap_or_default();
|
||||
let intact = match desc.rsplit_once("hash ") {
|
||||
Some((_, hash)) => hash.trim() == page_hash(pdf),
|
||||
None => true,
|
||||
@@ -119,11 +121,12 @@ fn editing_spec<'a>(node: &Dict<'a>, depth: usize, seen: &mut HashSet<ObjectIden
|
||||
})
|
||||
}
|
||||
|
||||
/// A file specification naming the editing file (current or legacy name).
|
||||
/// A file specification naming the editing file (current or legacy name), in any text string
|
||||
/// encoding.
|
||||
fn is_editing_file(spec: &Dict<'_>) -> bool {
|
||||
[&b"UF"[..], b"F"].into_iter().filter_map(|k| spec.get::<PdfString<'_>>(k)).any(|name| {
|
||||
let name = name.as_bytes();
|
||||
name == EDITING_FILE.as_bytes() || name == LEGACY_EDITING_FILE.as_bytes()
|
||||
let name = text_string(name.as_bytes());
|
||||
name == EDITING_FILE || name == LEGACY_EDITING_FILE
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,35 @@ fn changed_pages_are_noticed() {
|
||||
assert!(editing(&r.bytes).unwrap().intact);
|
||||
}
|
||||
|
||||
/// `pdf` with its editing file's description rewritten by another app as "hash " and the hash it
|
||||
/// held, in the text string encoding `bom` starts (UTF-16BE or UTF-8), padded with spaces to its
|
||||
/// length.
|
||||
fn described(pdf: &[u8], bom: &[u8]) -> Vec<u8> {
|
||||
let from = b"(VectorCraft editing data; page hash ";
|
||||
let at = pdf.windows(from.len()).position(|w| w == from).unwrap_or_else(|| panic!("no description"));
|
||||
let (hash, len) = (&pdf[at + from.len()..at + from.len() + 16], from.len() + 17);
|
||||
let mut desc = [b"(", bom].concat();
|
||||
for &c in b"hash ".iter().chain(hash) {
|
||||
if bom.len() == 2 {
|
||||
desc.push(0);
|
||||
}
|
||||
desc.push(c);
|
||||
}
|
||||
desc.push(b')');
|
||||
desc.resize(len, b' ');
|
||||
patched(pdf, &pdf[at..at + len], &desc)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hash_is_read_from_a_description_in_any_encoding() {
|
||||
let r = pdf_with(Some(b"native"), false);
|
||||
let resized = patched(&r.bytes, b"/MediaBox[0 0 100 100]", b"/MediaBox[0 0 100 101]");
|
||||
for bom in [&[0xFE, 0xFF][..], &[0xEF, 0xBB, 0xBF]] {
|
||||
assert!(editing(&described(&r.bytes, bom)).unwrap().intact, "{bom:?}: the same pages");
|
||||
assert!(!editing(&described(&resized, bom)).unwrap().intact, "{bom:?}: another page size");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_legacy_name_is_read_and_plain_pdfs_have_no_editing_data() {
|
||||
let r = pdf_with(Some(b"legacy"), false);
|
||||
@@ -153,3 +182,20 @@ fn direct_name_tree_kids_are_all_searched() {
|
||||
let e = editing(&raw_pdf(&objects)).unwrap();
|
||||
assert_eq!(e.data, b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_name_written_as_utf16_is_found() {
|
||||
let utf16: String = EDITING_FILE.bytes().map(|c| format!("00{c:02X}")).collect();
|
||||
for key in ["UF", "F"] {
|
||||
let objects = [
|
||||
"<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles 3 0 R >> >>".to_string(),
|
||||
"<< /Type /Pages /Kids [6 0 R] /Count 1 >>".to_string(),
|
||||
"<< /Names [(a) 4 0 R] >>".to_string(),
|
||||
format!("<< /Type /Filespec /{key} <FEFF{utf16}> /EF << /F 5 0 R >> >>"),
|
||||
"<< /Length 5 >>\nstream\nhello\nendstream".to_string(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>".to_string(),
|
||||
];
|
||||
let e = editing(&raw_pdf(&objects)).unwrap_or_else(|| panic!("/{key}: not found"));
|
||||
assert_eq!(e.data, b"hello", "/{key}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user