1use std::sync::Arc;
2
3use zpdf_core::{Error, ObjectId, ParseLimits, PdfDict, PdfObject, PdfStream, Result};
4
5use crate::lexer::Lexer;
6
7pub struct ObjectParser<'a> {
8 data: &'a [u8],
9 limits: &'a ParseLimits,
10}
11
12impl<'a> ObjectParser<'a> {
13 pub fn new(data: &'a [u8], limits: &'a ParseLimits) -> Self {
14 Self { data, limits }
15 }
16
17 pub fn parse_indirect_at(&self, offset: usize) -> Result<PdfObject> {
20 self.parse_indirect_with_id(offset).map(|(_, obj)| obj)
21 }
22
23 pub fn parse_indirect_with_id(&self, offset: usize) -> Result<(ObjectId, PdfObject)> {
29 let mut lex = Lexer::new(self.data, offset, self.limits);
30
31 let num_tok = lex.next_token()?;
32 let gen_tok = lex.next_token()?;
33 let id = match (&num_tok, &gen_tok) {
34 (PdfObject::Integer(n), PdfObject::Integer(g)) => {
35 match (u32::try_from(*n), u16::try_from(*g)) {
36 (Ok(n), Ok(g)) => ObjectId(n, g),
37 _ => {
38 return Err(Error::InvalidObject(
39 offset as u64,
40 format!("object header out of range: {n} {g} obj"),
41 ))
42 }
43 }
44 }
45 _ => {
46 return Err(Error::InvalidObject(
47 offset as u64,
48 "object header is not '<int> <int> obj'".into(),
49 ))
50 }
51 };
52
53 lex.skip_whitespace_and_comments();
54 self.expect_keyword(&mut lex, b"obj")?;
55
56 let obj = lex.next_token()?;
57 let obj = lex.maybe_resolve_ref(obj)?;
61
62 lex.skip_whitespace_and_comments();
64 if let PdfObject::Dict(dict) = &obj {
65 if self.starts_with_at(lex.pos(), b"stream") {
66 let stream = self.read_stream(dict.clone(), lex.pos())?;
67 return Ok((id, PdfObject::Stream(stream)));
68 }
69 }
70
71 Ok((id, obj))
72 }
73
74 fn expect_keyword(&self, lex: &mut Lexer, keyword: &[u8]) -> Result<()> {
75 let pos = lex.pos();
76 if self.data[pos..].starts_with(keyword) {
77 lex.set_pos(pos + keyword.len());
78 Ok(())
79 } else {
80 Err(Error::InvalidObject(
81 pos as u64,
82 format!(
83 "expected '{}', got '{}'",
84 String::from_utf8_lossy(keyword),
85 String::from_utf8_lossy(
86 &self.data[pos..self.data.len().min(pos + keyword.len())]
87 )
88 ),
89 ))
90 }
91 }
92
93 fn starts_with_at(&self, pos: usize, prefix: &[u8]) -> bool {
94 self.data.get(pos..).is_some_and(|s| s.starts_with(prefix))
95 }
96
97 fn read_stream(&self, dict: PdfDict, keyword_pos: usize) -> Result<PdfStream> {
98 let mut pos = keyword_pos + b"stream".len();
99
100 if self.data.get(pos) == Some(&b'\r') {
102 pos += 1;
103 }
104 if self.data.get(pos) == Some(&b'\n') {
105 pos += 1;
106 }
107
108 let declared = match dict.get("Length") {
115 Some(PdfObject::Integer(n)) if *n >= 0 => usize::try_from(*n).ok(),
116 _ => None,
117 };
118
119 let end = match declared {
120 Some(len)
121 if pos
122 .checked_add(len)
123 .is_some_and(|e| self.endstream_follows(e)) =>
124 {
125 pos + len
126 }
127 _ => self.scan_for_endstream(pos)?,
128 };
129
130 let length = (end - pos) as u64;
131 if length > self.limits.max_stream_bytes {
132 return Err(Error::StreamSizeLimit(self.limits.max_stream_bytes));
133 }
134
135 let stream_data = self.data[pos..end].to_vec();
136 Ok(PdfStream {
137 dict,
138 data: Arc::from(stream_data),
139 })
140 }
141
142 fn endstream_follows(&self, at: usize) -> bool {
145 let mut p = at;
146 while let Some(&b) = self.data.get(p) {
147 if matches!(b, b' ' | b'\t' | b'\r' | b'\n' | b'\x00' | b'\x0c') {
148 p += 1;
149 } else {
150 break;
151 }
152 }
153 self.data
154 .get(p..)
155 .is_some_and(|s| s.starts_with(b"endstream"))
156 }
157
158 fn scan_for_endstream(&self, pos: usize) -> Result<usize> {
163 let cap = usize::try_from(self.limits.max_stream_bytes)
164 .unwrap_or(usize::MAX)
165 .saturating_add(b"endstream".len() + 2);
166 let search_end = pos.saturating_add(cap).min(self.data.len());
167 let hay = self
168 .data
169 .get(pos..search_end)
170 .ok_or(Error::UnexpectedEof(pos as u64))?;
171 let rel = hay
172 .windows(b"endstream".len())
173 .position(|w| w == b"endstream")
174 .ok_or_else(|| {
175 Error::InvalidObject(pos as u64, "stream: no endstream within size limit".into())
176 })?;
177 let mut end = pos + rel;
178 if end > pos && self.data[end - 1] == b'\n' {
180 end -= 1;
181 if end > pos && self.data[end - 1] == b'\r' {
182 end -= 1;
183 }
184 } else if end > pos && self.data[end - 1] == b'\r' {
185 end -= 1;
186 }
187 Ok(end)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn parse_simple_indirect() {
197 let data = b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
198 let limits = ParseLimits::default();
199 let parser = ObjectParser::new(data, &limits);
200 let obj = parser.parse_indirect_at(0).unwrap();
201 match obj {
202 PdfObject::Dict(d) => {
203 assert_eq!(d.get_name("Type").unwrap(), "Catalog");
204 }
205 other => panic!("expected Dict, got {other:?}"),
206 }
207 }
208
209 #[test]
210 fn parse_stream_object() {
211 let content = b"BT /F1 12 Tf (Hello) Tj ET";
212 let obj_bytes = format!("5 0 obj\n<< /Length {} >>\nstream\n", content.len());
213 let mut data = obj_bytes.into_bytes();
214 data.extend_from_slice(content);
215 data.extend_from_slice(b"\nendstream\nendobj\n");
216
217 let limits = ParseLimits::default();
218 let parser = ObjectParser::new(&data, &limits);
219 let obj = parser.parse_indirect_at(0).unwrap();
220 match obj {
221 PdfObject::Stream(s) => {
222 assert_eq!(s.data.as_ref(), content);
223 assert_eq!(s.dict.get_i64("Length").unwrap(), content.len() as i64);
224 }
225 other => panic!("expected Stream, got {other:?}"),
226 }
227 }
228
229 #[test]
230 fn reject_oversized_stream_length() {
231 let limits = ParseLimits {
232 max_stream_bytes: 16,
233 ..Default::default()
234 };
235 let body = b"0123456789ABCDEFGHIJ"; let obj_bytes = format!("5 0 obj\n<< /Length {} >>\nstream\n", body.len());
237 let mut data = obj_bytes.into_bytes();
238 data.extend_from_slice(body);
239 data.extend_from_slice(b"\nendstream\nendobj\n");
240 let parser = ObjectParser::new(&data, &limits);
241 let err = parser.parse_indirect_at(0).unwrap_err();
242 assert!(matches!(err, Error::StreamSizeLimit(16)), "got {err:?}");
243 }
244
245 fn stream_data(data: &[u8]) -> Vec<u8> {
247 let limits = ParseLimits::default();
248 let parser = ObjectParser::new(data, &limits);
249 match parser.parse_indirect_at(0).unwrap() {
250 PdfObject::Stream(s) => s.data.to_vec(),
251 other => panic!("expected Stream, got {other:?}"),
252 }
253 }
254
255 #[test]
256 fn indirect_length_recovers_via_endstream_scan() {
257 let mut data = b"5 0 obj\n<< /Length 99 0 R >>\nstream\n".to_vec();
260 data.extend_from_slice(b"Hello, world!");
261 data.extend_from_slice(b"\nendstream\nendobj\n");
262 assert_eq!(stream_data(&data), b"Hello, world!");
263 }
264
265 #[test]
266 fn missing_length_recovers_via_endstream_scan() {
267 let mut data = b"5 0 obj\n<< /Type /Whatever >>\nstream\n".to_vec();
268 data.extend_from_slice(b"payload bytes");
269 data.extend_from_slice(b"\nendstream\nendobj\n");
270 assert_eq!(stream_data(&data), b"payload bytes");
271 }
272
273 #[test]
274 fn wrong_length_recovers_via_endstream_scan() {
275 let mut data = b"5 0 obj\n<< /Length 3 >>\nstream\n".to_vec();
278 data.extend_from_slice(b"Hello");
279 data.extend_from_slice(b"\nendstream\nendobj\n");
280 assert_eq!(stream_data(&data), b"Hello");
281 }
282
283 #[test]
284 fn negative_length_recovers_via_endstream_scan() {
285 let mut data = b"5 0 obj\n<< /Length -1 >>\nstream\n".to_vec();
286 data.extend_from_slice(b"abc");
287 data.extend_from_slice(b"\nendstream\nendobj\n");
288 assert_eq!(stream_data(&data), b"abc");
289 }
290
291 #[test]
292 fn correct_length_trusted_even_if_data_contains_endstream_bytes() {
293 let body: &[u8] = b"AAendstreamBB"; let mut data = format!("5 0 obj\n<< /Length {} >>\nstream\n", body.len()).into_bytes();
297 data.extend_from_slice(body);
298 data.extend_from_slice(b"\nendstream\nendobj\n");
299 assert_eq!(stream_data(&data), body);
300 }
301
302 #[test]
303 fn crlf_before_endstream_is_stripped_on_scan() {
304 let mut data = b"5 0 obj\n<< >>\nstream\n".to_vec();
306 data.extend_from_slice(b"data");
307 data.extend_from_slice(b"\r\nendstream\nendobj\n");
308 assert_eq!(stream_data(&data), b"data");
309 }
310
311 #[test]
312 fn parse_indirect_with_id_returns_header_id() {
313 let data = b"7 2 obj\n<< /Type /Catalog >>\nendobj\n";
314 let limits = ParseLimits::default();
315 let parser = ObjectParser::new(data, &limits);
316 let (id, obj) = parser.parse_indirect_with_id(0).unwrap();
317 assert_eq!(id, ObjectId(7, 2));
318 assert!(obj.as_dict().is_ok());
319 }
320
321 #[test]
322 fn parse_indirect_with_id_rejects_non_integer_header() {
323 let data = b"/Name 0 obj\n42\nendobj\n";
324 let limits = ParseLimits::default();
325 let parser = ObjectParser::new(data, &limits);
326 assert!(parser.parse_indirect_with_id(0).is_err());
327 }
328
329 #[test]
330 fn top_level_ref_body_parses_as_ref() {
331 let data = b"4 0 obj\n5 0 R\nendobj\n";
333 let limits = ParseLimits::default();
334 let parser = ObjectParser::new(data, &limits);
335 let obj = parser.parse_indirect_at(0).unwrap();
336 assert_eq!(obj, PdfObject::Ref(ObjectId(5, 0)));
337 }
338
339 #[test]
340 fn deeply_nested_value_in_indirect_object_errors() {
341 let limits = ParseLimits {
343 max_object_depth: 4,
344 ..Default::default()
345 };
346 let n = 20usize;
347 let mut inner = String::new();
348 for _ in 0..n {
349 inner.push('[');
350 }
351 inner.push('1');
352 for _ in 0..n {
353 inner.push(']');
354 }
355 let data = format!("1 0 obj\n{inner}\nendobj\n").into_bytes();
356 let parser = ObjectParser::new(&data, &limits);
357 let err = parser.parse_indirect_at(0).unwrap_err();
358 assert!(matches!(err, Error::RecursionLimit(4)), "got {err:?}");
359 }
360}