Files
pdfcraft/crates/cos/src/lib.rs
snipeTRandBrandon Thomas 9c2d9c02bf fix(crypt): R2–R4 passwords with non-ASCII characters open (PDFDocEncoding) (#905)
Revisions 2–4 of the standard security handler take the password in
PDFDocEncoding (ISO 32000-2 §7.6.4.3.2, Algorithm 2). PdfCraft authenticates
an encrypted file twice, and neither did that:
- pdfcraft-crypt took Latin-1 and dropped every other character, so "ı"
  (0x9A) and "€" (0xA0) vanished and "ılık€" became "lk";
- the renderer (vendored hayro-syntax, "TODO: Convert to PDFDocEncoding")
  used the UTF-8 bytes, so any non-ASCII password failed, even "Çok güzel",
  which Latin-1 and PDFDocEncoding write alike;
- the engine handed the renderer an owner-recovered user password as Latin-1
  characters, which the renderer then read as UTF-8 again.
Files that qpdf or Acrobat protected with such passwords could not be
opened with the right password.

- pdfcraft-crypt encodes R2–R4 passwords in PDFDocEncoding and, for a
  password with characters PDFDocEncoding lacks (ğ, ş, İ), also tries its
  UTF-8 bytes, which qpdf writes for those. The PDFDocEncoding table moves
  here from pdfcraft-cos (which depends on this crate) and cos uses it.
- hayro-syntax (vendored patch 14) tries an R2–R4 password as given, then
  in PDFDocEncoding.
- The engine passes a recovered user password to the renderer as UTF-8 when
  its bytes are UTF-8 and as PDFDocEncoding otherwise, so the renderer gets
  exactly those bytes back.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 17:10:39 -04:00

54 lines
2.3 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! pdfcraft-cos — the PDF object layer (L1, standalone with `pdfcraft-filters`).
//!
//! Parses the COS object graph lazily and tolerantly, keeps edits in a copy-on-write overlay
//! (cheap snapshots for undo), and writes documents back either incrementally (original bytes
//! untouched) or as a full, garbage-collected rewrite. The PDF object graph *is* PdfCraft's
//! document model (plan/architecture.md §5, ADR-0010).
//!
//! Done: every cross-reference form (tables, streams, hybrid `/XRefStm`), object streams, repair
//! by scanning, encryption on load and save (R2–R6, crypt filters, `/EFF`), incremental writing,
//! and full saves that pack objects into compressed object streams with a cross-reference stream.
//! Not yet: streaming very large files within a memory budget (`ByteSource`,
//! `core.lazy-loading`), and linearized output (`core.linearization`, M11). See the README.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod bytes;
mod document;
mod object;
pub mod page_labels;
mod parser;
mod security;
mod writer;
pub use bytes::Bytes;
pub use document::{Document, Revision, SourceIdentity, XrefEntry};
pub use object::{Dict, MAX_DECODED, Name, ObjRef, Object, PdfString, Stream};
pub use parser::{Lexer, parse_indirect};
pub use pdfcraft_crypt::{Algorithm, Auth, Method as CryptMethod, NewEncryption, Permissions, SecurityHandler, pdfdoc_byte, pdfdoc_char};
pub use writer::{SaveOptions, pdf_date, serialize, write_full, write_incremental};
#[derive(Debug, thiserror::Error, Clone, PartialEq)]
pub enum CosError {
#[error("not a PDF file (no %PDF- header)")]
NotPdf,
#[error("syntax error at byte {offset}: {detail}")]
Syntax { offset: usize, detail: String },
#[error("object {0} is missing")]
MissingObject(u32),
#[error("object {0} is not a dictionary")]
NotADictionary(ObjRef),
#[error("stream data could not be decoded: {0}")]
Filter(String),
#[error("the document is protected by a password")]
NeedsPassword,
#[error("the password is incorrect")]
WrongPassword,
#[error("unsupported security: {0}")]
Security(String),
#[error("internal lock poisoned")]
Poisoned,
#[error("this document was opened read-only with a decode limit and cannot be saved")]
ReadOnlyLimit,
}