Skip to main content

rpdfium_core/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = "Core types and utilities for rpdfium — a faithful Rust port of PDFium."]
3//!
4//! This crate provides the foundational types shared across all rpdfium crates:
5//!
6//! - [`PdfError`] and [`ParseError`] — structured error types
7//! - [`PdfString`] — encoding-aware PDF string with PDFDocEncoding support
8//! - [`Name`] — PDF name objects with predefined constants
9//! - [`Point`], [`Size`], [`Vector2D`], [`Rect`], [`RectI`], [`Matrix`] — geometric types
10//! - [`PdfSource`] — trait for PDF source data abstraction
11//! - [`ParsingMode`] and [`OpenOptions`] — configuration types
12//! - Security limit constants in the [`fx_system`] module
13//! - [`Diagnostic`] types for structured error reporting
14
15pub mod bytestring;
16pub mod cfx_bitstream;
17pub mod constants;
18pub mod diagnostic;
19pub mod error;
20pub mod fx_bidi;
21pub mod fx_coordinates;
22pub mod fx_stream;
23pub mod fx_system;
24pub mod name;
25pub mod widestring;
26
27// Re-export primary types at the crate root for convenience.
28pub use bytestring::{PdfString, PdfStringEncoding, pdfdoc_to_char};
29pub use diagnostic::{DiagCategory, DiagLocation, Diagnostic, DiagnosticCollector, Severity};
30pub use error::{ObjectId, ParseError, PdfError, PdfResult};
31pub use fx_bidi::{
32    BidiChar, BidiDirection, BidiSegment, BidiString, Direction, Segment, bidi_class_of,
33    mirror_char,
34};
35pub use fx_coordinates::{Matrix, Point, Rect, RectI, Size, Vector2D};
36pub use fx_stream::{CountingWriter, MemoryStream, PdfSource, PdfWrite};
37pub use fx_system::is_float_zero;
38pub use name::Name;
39pub use widestring::WideString;
40
41/// Parsing mode controlling how strictly the PDF specification is enforced.
42///
43/// - [`Strict`](ParsingMode::Strict): Return errors on spec violations. Useful
44///   for testing, validation, and PDF conformance checking.
45/// - [`Lenient`](ParsingMode::Lenient) (default): Accept malformed input
46///   wherever possible, recovering gracefully, and report issues via `tracing`.
47///   This replicates PDFium's permissive behavior.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
49pub enum ParsingMode {
50    /// Return errors on spec violations.
51    Strict,
52    /// Recover where possible, log warnings via tracing.
53    #[default]
54    Lenient,
55}
56
57impl std::fmt::Display for ParsingMode {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            ParsingMode::Strict => write!(f, "strict"),
61            ParsingMode::Lenient => write!(f, "lenient"),
62        }
63    }
64}
65
66/// Options for opening a PDF document.
67///
68/// All fields have sensible defaults via the [`Default`] implementation.
69/// Soft limits can be adjusted for trusted inputs that exceed the defaults.
70#[derive(Debug, Clone)]
71pub struct OpenOptions {
72    /// Parsing mode (default: [`ParsingMode::Lenient`]).
73    pub parsing_mode: ParsingMode,
74
75    /// Password for encrypted documents (default: `None`).
76    pub password: Option<String>,
77
78    /// Maximum decompressed stream size in bytes (default: 256 MB).
79    ///
80    /// Prevents decompression bombs.
81    pub max_decompressed_stream_size: u64,
82
83    /// Maximum compression ratio before aborting (default: 1000:1).
84    ///
85    /// A ratio exceeding this strongly indicates a zip bomb.
86    pub max_compression_ratio: u32,
87
88    /// Maximum content stream operators per page (default: 10,000,000).
89    pub max_operators_per_page: u64,
90
91    /// Maximum distance to scan for `endstream` keyword (default: 16 MB).
92    pub max_endstream_scan_distance: u64,
93}
94
95impl OpenOptions {
96    /// Set the parsing mode.
97    pub fn with_parsing_mode(mut self, mode: ParsingMode) -> Self {
98        self.parsing_mode = mode;
99        self
100    }
101
102    /// Set the password for encrypted documents.
103    pub fn with_password(mut self, password: impl Into<String>) -> Self {
104        self.password = Some(password.into());
105        self
106    }
107
108    /// Set the maximum decompressed stream size in bytes.
109    pub fn with_max_decompressed_stream_size(mut self, size: u64) -> Self {
110        self.max_decompressed_stream_size = size;
111        self
112    }
113
114    /// Set the maximum compression ratio before aborting.
115    pub fn with_max_compression_ratio(mut self, ratio: u32) -> Self {
116        self.max_compression_ratio = ratio;
117        self
118    }
119
120    /// Set the maximum content stream operators per page.
121    pub fn with_max_operators_per_page(mut self, count: u64) -> Self {
122        self.max_operators_per_page = count;
123        self
124    }
125
126    /// Set the maximum distance to scan for `endstream`.
127    pub fn with_max_endstream_scan_distance(mut self, distance: u64) -> Self {
128        self.max_endstream_scan_distance = distance;
129        self
130    }
131}
132
133impl Default for OpenOptions {
134    fn default() -> Self {
135        Self {
136            parsing_mode: ParsingMode::default(),
137            password: None,
138            max_decompressed_stream_size: fx_system::DEFAULT_MAX_DECOMPRESSED_STREAM_SIZE,
139            max_compression_ratio: fx_system::DEFAULT_MAX_COMPRESSION_RATIO,
140            max_operators_per_page: fx_system::DEFAULT_MAX_OPERATORS_PER_PAGE,
141            max_endstream_scan_distance: fx_system::DEFAULT_MAX_ENDSTREAM_SCAN_DISTANCE,
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_parsing_mode_default_is_lenient() {
152        assert_eq!(ParsingMode::default(), ParsingMode::Lenient);
153    }
154
155    #[test]
156    fn test_parsing_mode_display() {
157        assert_eq!(format!("{}", ParsingMode::Strict), "strict");
158        assert_eq!(format!("{}", ParsingMode::Lenient), "lenient");
159    }
160
161    #[test]
162    fn test_open_options_default() {
163        let opts = OpenOptions::default();
164        assert_eq!(opts.parsing_mode, ParsingMode::Lenient);
165        assert!(opts.password.is_none());
166        assert_eq!(
167            opts.max_decompressed_stream_size,
168            fx_system::DEFAULT_MAX_DECOMPRESSED_STREAM_SIZE
169        );
170        assert_eq!(
171            opts.max_compression_ratio,
172            fx_system::DEFAULT_MAX_COMPRESSION_RATIO
173        );
174        assert_eq!(
175            opts.max_operators_per_page,
176            fx_system::DEFAULT_MAX_OPERATORS_PER_PAGE
177        );
178        assert_eq!(
179            opts.max_endstream_scan_distance,
180            fx_system::DEFAULT_MAX_ENDSTREAM_SCAN_DISTANCE
181        );
182    }
183
184    #[test]
185    fn test_open_options_custom() {
186        let opts = OpenOptions {
187            parsing_mode: ParsingMode::Strict,
188            password: Some("secret".to_string()),
189            max_decompressed_stream_size: 512 * 1024 * 1024,
190            ..OpenOptions::default()
191        };
192        assert_eq!(opts.parsing_mode, ParsingMode::Strict);
193        assert_eq!(opts.password.as_deref(), Some("secret"));
194        assert_eq!(opts.max_decompressed_stream_size, 512 * 1024 * 1024);
195    }
196
197    #[test]
198    fn test_open_options_builder_parsing_mode() {
199        let opts = OpenOptions::default().with_parsing_mode(ParsingMode::Strict);
200        assert_eq!(opts.parsing_mode, ParsingMode::Strict);
201    }
202
203    #[test]
204    fn test_open_options_builder_password() {
205        let opts = OpenOptions::default().with_password("secret");
206        assert_eq!(opts.password.as_deref(), Some("secret"));
207    }
208
209    #[test]
210    fn test_open_options_builder_max_decompressed_stream_size() {
211        let opts = OpenOptions::default().with_max_decompressed_stream_size(1024);
212        assert_eq!(opts.max_decompressed_stream_size, 1024);
213    }
214
215    #[test]
216    fn test_open_options_builder_max_compression_ratio() {
217        let opts = OpenOptions::default().with_max_compression_ratio(500);
218        assert_eq!(opts.max_compression_ratio, 500);
219    }
220
221    #[test]
222    fn test_open_options_builder_max_operators() {
223        let opts = OpenOptions::default().with_max_operators_per_page(5000);
224        assert_eq!(opts.max_operators_per_page, 5000);
225    }
226
227    #[test]
228    fn test_open_options_builder_max_endstream_scan() {
229        let opts = OpenOptions::default().with_max_endstream_scan_distance(8192);
230        assert_eq!(opts.max_endstream_scan_distance, 8192);
231    }
232
233    #[test]
234    fn test_open_options_builder_chaining() {
235        let opts = OpenOptions::default()
236            .with_parsing_mode(ParsingMode::Strict)
237            .with_password("pass")
238            .with_max_decompressed_stream_size(512)
239            .with_max_compression_ratio(100)
240            .with_max_operators_per_page(1000)
241            .with_max_endstream_scan_distance(4096);
242        assert_eq!(opts.parsing_mode, ParsingMode::Strict);
243        assert_eq!(opts.password.as_deref(), Some("pass"));
244        assert_eq!(opts.max_decompressed_stream_size, 512);
245        assert_eq!(opts.max_compression_ratio, 100);
246        assert_eq!(opts.max_operators_per_page, 1000);
247        assert_eq!(opts.max_endstream_scan_distance, 4096);
248    }
249
250    #[test]
251    fn test_parsing_mode_is_send_sync() {
252        fn assert_send_sync<T: Send + Sync>() {}
253        assert_send_sync::<ParsingMode>();
254    }
255
256    #[test]
257    fn test_open_options_is_send_sync() {
258        fn assert_send_sync<T: Send + Sync>() {}
259        assert_send_sync::<OpenOptions>();
260    }
261
262    #[test]
263    fn test_parsing_mode_equality() {
264        assert_eq!(ParsingMode::Strict, ParsingMode::Strict);
265        assert_eq!(ParsingMode::Lenient, ParsingMode::Lenient);
266        assert_ne!(ParsingMode::Strict, ParsingMode::Lenient);
267    }
268
269    #[test]
270    fn test_all_core_types_reexported() {
271        // Verify key types are accessible from crate root
272        let _: ObjectId = ObjectId {
273            number: 1,
274            generation: 0,
275        };
276        let _: PdfString = PdfString::from_bytes(vec![]);
277        let _: Name = Name::from("Test");
278        let _: Point = Point::new(0.0, 0.0);
279        let _: Size = Size::new(0.0, 0.0);
280        let _: Rect = Rect::new(0.0, 0.0, 1.0, 1.0);
281        let _: Matrix = Matrix::identity();
282        let _: ParsingMode = ParsingMode::default();
283        let _: OpenOptions = OpenOptions::default();
284        let _: DiagnosticCollector = DiagnosticCollector::new();
285    }
286}