1use std::fmt;
4
5use crate::source::{Source, SourceError};
6
7pub const HEADER_SEARCH_WINDOW: u64 = 1024;
13
14const MARKER: &[u8] = b"%PDF-";
15const MAX_DIGITS: usize = 3;
17const TAIL: usize = MARKER.len() + 2 * MAX_DIGITS + 1;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct Version {
23 pub major: u8,
25 pub minor: u8,
27}
28
29impl Version {
30 pub const V1_7: Version = Version::new(1, 7);
32 pub const V2_0: Version = Version::new(2, 0);
34
35 pub const fn new(major: u8, minor: u8) -> Self {
37 Self { major, minor }
38 }
39
40 pub fn is_known(self) -> bool {
42 matches!((self.major, self.minor), (1, 0..=7) | (2, 0))
43 }
44}
45
46impl fmt::Display for Version {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{}.{}", self.major, self.minor)
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Header {
55 pub offset: u64,
59 pub version: Version,
62}
63
64impl Header {
65 pub fn has_leading_junk(&self) -> bool {
67 self.offset != 0
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum HeaderError {
74 NotFound,
76 MalformedVersion {
78 offset: u64,
80 },
81 Source(SourceError),
83}
84
85impl fmt::Display for HeaderError {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 Self::NotFound => write!(
89 f,
90 "no %PDF- header in the first {HEADER_SEARCH_WINDOW} bytes"
91 ),
92 Self::MalformedVersion { offset } => {
93 write!(f, "malformed version after %PDF- at byte {offset}")
94 }
95 Self::Source(err) => err.fmt(f),
96 }
97 }
98}
99
100impl std::error::Error for HeaderError {
101 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102 match self {
103 Self::Source(err) => Some(err),
104 _ => None,
105 }
106 }
107}
108
109impl From<SourceError> for HeaderError {
110 fn from(err: SourceError) -> Self {
111 Self::Source(err)
112 }
113}
114
115pub fn find_header<S: Source + ?Sized>(source: &S) -> Result<Header, HeaderError> {
121 let mut buf = [0u8; HEADER_SEARCH_WINDOW as usize + TAIL];
123 let mut filled = 0;
124 while filled < buf.len() {
125 let Some(rest) = buf.get_mut(filled..) else {
126 break;
127 };
128 let n = source.read_at(filled as u64, rest)?;
129 if n == 0 {
130 break;
131 }
132 filled += n;
133 }
134 let bytes = buf.get(..filled).unwrap_or_default();
135
136 let start = bytes
137 .windows(MARKER.len())
138 .take(HEADER_SEARCH_WINDOW as usize)
139 .position(|w| w == MARKER)
140 .ok_or(HeaderError::NotFound)?;
141 let offset = start as u64;
142 let after = bytes
143 .get(start + MARKER.len()..)
144 .ok_or(HeaderError::MalformedVersion { offset })?;
145 let version = parse_version(after).ok_or(HeaderError::MalformedVersion { offset })?;
146 Ok(Header { offset, version })
147}
148
149fn parse_version(bytes: &[u8]) -> Option<Version> {
151 let (major, rest) = parse_component(bytes)?;
152 let rest = rest.strip_prefix(b".")?;
153 let (minor, _) = parse_component(rest)?;
154 Some(Version::new(major, minor))
155}
156
157fn parse_component(bytes: &[u8]) -> Option<(u8, &[u8])> {
159 let digits = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
160 if digits == 0 || digits > MAX_DIGITS {
161 return None;
162 }
163 let (number, rest) = bytes.split_at_checked(digits)?;
164 let value = number
165 .iter()
166 .try_fold(0u16, |acc, &d| Some(acc * 10 + u16::from(d - b'0')))?;
167 Some((u8::try_from(value).ok()?, rest))
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn header(bytes: &[u8]) -> Result<Header, HeaderError> {
175 find_header(bytes)
176 }
177
178 #[test]
179 fn header_at_start() {
180 assert_eq!(
181 header(b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n"),
182 Ok(Header {
183 offset: 0,
184 version: Version::V1_7
185 })
186 );
187 }
188
189 #[test]
190 fn pdf_2_0() {
191 let h = header(b"%PDF-2.0\r\n").unwrap();
192 assert_eq!(h.version, Version::V2_0);
193 assert!(h.version.is_known());
194 assert!(!h.has_leading_junk());
195 }
196
197 #[test]
198 fn junk_before_header() {
199 let mut bytes = vec![b'x'; 500];
200 bytes.extend_from_slice(b"%PDF-1.4\n");
201 let h = header(&bytes).unwrap();
202 assert_eq!(h.offset, 500);
203 assert!(h.has_leading_junk());
204 assert_eq!(h.version, Version::new(1, 4));
205 }
206
207 #[test]
208 fn header_starting_at_last_window_byte() {
209 let mut bytes = vec![0u8; HEADER_SEARCH_WINDOW as usize - 1];
210 bytes.extend_from_slice(b"%PDF-1.3");
211 assert_eq!(header(&bytes).unwrap().offset, HEADER_SEARCH_WINDOW - 1);
212 }
213
214 #[test]
215 fn header_past_window_is_not_found() {
216 let mut bytes = vec![0u8; HEADER_SEARCH_WINDOW as usize];
217 bytes.extend_from_slice(b"%PDF-1.3");
218 assert_eq!(header(&bytes), Err(HeaderError::NotFound));
219 }
220
221 #[test]
222 fn empty_and_unrelated_input() {
223 assert_eq!(header(b""), Err(HeaderError::NotFound));
224 assert_eq!(header(b"%!PS-Adobe-3.0"), Err(HeaderError::NotFound));
225 assert_eq!(header(b"%PDF"), Err(HeaderError::NotFound));
226 }
227
228 #[test]
229 fn malformed_versions() {
230 for bad in [
231 &b"%PDF-"[..],
232 b"%PDF-x.y",
233 b"%PDF-1",
234 b"%PDF-1.",
235 b"%PDF-.7",
236 b"%PDF-1234.0",
237 b"%PDF-256.0",
238 b"%PDF-1.9999",
239 ] {
240 assert_eq!(
241 header(bad),
242 Err(HeaderError::MalformedVersion { offset: 0 }),
243 "{}",
244 String::from_utf8_lossy(bad)
245 );
246 }
247 }
248
249 #[test]
250 fn first_marker_wins_even_if_malformed() {
251 assert_eq!(
252 header(b"junk%PDF-?\n%PDF-1.7"),
253 Err(HeaderError::MalformedVersion { offset: 4 })
254 );
255 }
256
257 #[test]
258 fn unknown_but_well_formed_version() {
259 let h = header(b"%PDF-1.8").unwrap();
260 assert_eq!(h.version, Version::new(1, 8));
261 assert!(!h.version.is_known());
262 assert!(!Version::new(2, 1).is_known());
263 assert!(!Version::new(0, 9).is_known());
264 assert!(Version::new(1, 0).is_known());
265 }
266
267 #[test]
268 fn three_digit_components() {
269 assert_eq!(
270 header(b"%PDF-001.255").unwrap().version,
271 Version::new(1, 255)
272 );
273 }
274
275 #[test]
276 fn version_display() {
277 assert_eq!(Version::V1_7.to_string(), "1.7");
278 assert_eq!(Version::new(10, 12).to_string(), "10.12");
279 }
280
281 #[test]
282 fn error_display() {
283 assert_eq!(
284 HeaderError::NotFound.to_string(),
285 "no %PDF- header in the first 1024 bytes"
286 );
287 assert_eq!(
288 HeaderError::MalformedVersion { offset: 3 }.to_string(),
289 "malformed version after %PDF- at byte 3"
290 );
291 }
292
293 struct Trickle<'a>(&'a [u8]);
295
296 impl Source for Trickle<'_> {
297 fn len(&self) -> u64 {
298 self.0.len() as u64
299 }
300
301 fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize, SourceError> {
302 let end = buf.len().min(1);
303 self.0.read_at(offset, &mut buf[..end])
304 }
305 }
306
307 #[test]
308 fn short_reads_are_retried() {
309 let mut bytes = vec![b' '; 10];
310 bytes.extend_from_slice(b"%PDF-1.5");
311 assert_eq!(find_header(&Trickle(&bytes)).unwrap().offset, 10);
312 }
313
314 struct Failing;
315
316 impl Source for Failing {
317 fn len(&self) -> u64 {
318 100
319 }
320
321 fn read_at(&self, _: u64, _: &mut [u8]) -> Result<usize, SourceError> {
322 Err(SourceError::new("boom"))
323 }
324 }
325
326 #[test]
327 fn source_errors_propagate() {
328 let err = find_header(&Failing).unwrap_err();
329 assert_eq!(err, HeaderError::Source(SourceError::new("boom")));
330 assert!(std::error::Error::source(&err).is_some());
331 assert_eq!(err.to_string(), "source read failed: boom");
332 }
333}