easypdf_core/io/
pdf_input.rs1use std::path::{Path, PathBuf};
4
5use crate::{PdfError, Result};
6
7use crate::ResourceLimits;
8
9#[derive(Clone, Debug)]
11#[non_exhaustive]
12pub enum PdfInput {
13 Path(PathBuf),
15 Bytes(Vec<u8>),
17}
18
19impl PdfInput {
20 #[must_use]
22 pub fn from_path(path: impl Into<PathBuf>) -> Self {
23 Self::Path(path.into())
24 }
25
26 #[must_use]
28 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
29 Self::Bytes(bytes.into())
30 }
31
32 pub fn read(&self, limits: ResourceLimits) -> Result<Vec<u8>> {
38 match self {
39 Self::Path(path) => {
40 let metadata = std::fs::metadata(path)?;
41 if metadata.len() > limits.max_input_bytes() {
42 return Err(PdfError::ResourceLimitExceeded {
43 resource: "input_bytes",
44 limit: limits.max_input_bytes(),
45 actual: metadata.len(),
46 });
47 }
48 let bytes = std::fs::read(path)?;
49 let actual = u64::try_from(bytes.len()).map_err(|_| {
50 PdfError::Other("PDF input length cannot be represented as u64".to_string())
51 })?;
52 if actual > limits.max_input_bytes() {
53 return Err(PdfError::ResourceLimitExceeded {
54 resource: "input_bytes",
55 limit: limits.max_input_bytes(),
56 actual,
57 });
58 }
59 Ok(bytes)
60 }
61 Self::Bytes(bytes) => {
62 let length = u64::try_from(bytes.len()).map_err(|_| {
63 PdfError::Other("PDF input length cannot be represented as u64".to_string())
64 })?;
65 if length > limits.max_input_bytes() {
66 return Err(PdfError::ResourceLimitExceeded {
67 resource: "input_bytes",
68 limit: limits.max_input_bytes(),
69 actual: length,
70 });
71 }
72 Ok(bytes.clone())
73 }
74 }
75 }
76
77 #[must_use]
79 pub fn path(&self) -> Option<&Path> {
80 match self {
81 Self::Path(path) => Some(path.as_path()),
82 Self::Bytes(_) => None,
83 }
84 }
85}
86
87impl From<PathBuf> for PdfInput {
88 fn from(value: PathBuf) -> Self {
89 Self::Path(value)
90 }
91}
92
93impl From<&Path> for PdfInput {
94 fn from(value: &Path) -> Self {
95 Self::Path(value.to_path_buf())
96 }
97}
98
99impl From<Vec<u8>> for PdfInput {
100 fn from(value: Vec<u8>) -> Self {
101 Self::Bytes(value)
102 }
103}
104
105#[cfg(test)]
106#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn from_path_creates_path_variant() {
112 let input = PdfInput::from_path("/tmp/test.pdf");
113 assert!(matches!(input, PdfInput::Path(_)));
114 assert_eq!(input.path(), Some(std::path::Path::new("/tmp/test.pdf")));
115 }
116
117 #[test]
118 fn from_bytes_creates_bytes_variant() {
119 let input = PdfInput::from_bytes(vec![1, 2, 3]);
120 assert!(matches!(input, PdfInput::Bytes(_)));
121 assert!(input.path().is_none());
122 }
123
124 #[test]
125 fn from_path_with_pathbuf() {
126 let pb = std::path::PathBuf::from("/tmp/doc.pdf");
127 let input = PdfInput::from_path(pb);
128 assert_eq!(input.path(), Some(std::path::Path::new("/tmp/doc.pdf")));
129 }
130
131 #[test]
132 fn from_bytes_with_slice() {
133 let data: Vec<u8> = vec![0x25, 0x50, 0x44, 0x46]; let input = PdfInput::from_bytes(data.clone());
135 if let PdfInput::Bytes(b) = input {
136 assert_eq!(b, data);
137 } else {
138 panic!("expected Bytes variant");
139 }
140 }
141
142 #[test]
143 fn path_returns_none_for_bytes() {
144 let input = PdfInput::from_bytes(vec![1, 2]);
145 assert!(input.path().is_none());
146 }
147
148 #[test]
149 fn path_returns_some_for_path() {
150 let input = PdfInput::from_path("/tmp/a.pdf");
151 assert!(input.path().is_some());
152 }
153
154 #[test]
155 fn read_bytes_within_limits() {
156 let input = PdfInput::from_bytes(vec![1, 2, 3, 4]);
157 let limits = ResourceLimits::new();
158 let result = input.read(limits);
159 assert!(result.is_ok());
160 assert_eq!(result.unwrap(), vec![1, 2, 3, 4]);
161 }
162
163 #[test]
164 fn read_bytes_exceeds_limits() {
165 let data = vec![0u8; 2048];
166 let input = PdfInput::from_bytes(data);
167 let limits = ResourceLimits::new().with_max_input_bytes(1024);
168 let result = input.read(limits);
169 assert!(result.is_err());
170 let err = result.unwrap_err();
171 assert!(matches!(err, PdfError::ResourceLimitExceeded { .. }));
172 }
173
174 #[test]
175 fn read_path_nonexistent_file() {
176 let input = PdfInput::from_path("/nonexistent/path/file.pdf");
177 let limits = ResourceLimits::new();
178 let result = input.read(limits);
179 assert!(result.is_err());
180 assert!(matches!(result.unwrap_err(), PdfError::Io(_)));
181 }
182
183 #[test]
184 fn read_path_existing_file() {
185 let dir = std::env::temp_dir();
186 let path = dir.join("easypdf_test_input.txt");
187 std::fs::write(&path, b"hello pdf").unwrap();
188 let input = PdfInput::from_path(&path);
189 let limits = ResourceLimits::new();
190 let result = input.read(limits);
191 assert!(result.is_ok());
192 assert_eq!(result.unwrap(), b"hello pdf");
193 let _ = std::fs::remove_file(&path);
194 }
195
196 #[test]
197 fn read_path_file_exceeds_limits() {
198 let dir = std::env::temp_dir();
199 let path = dir.join("easypdf_test_large.txt");
200 std::fs::write(&path, vec![0u8; 2048]).unwrap();
201 let input = PdfInput::from_path(&path);
202 let limits = ResourceLimits::new().with_max_input_bytes(1024);
203 let result = input.read(limits);
204 assert!(result.is_err());
205 let _ = std::fs::remove_file(&path);
206 }
207
208 #[test]
209 fn from_pathbuf_conversion() {
210 let pb = std::path::PathBuf::from("/tmp/test.pdf");
211 let input: PdfInput = pb.into();
212 assert!(matches!(input, PdfInput::Path(_)));
213 }
214
215 #[test]
216 fn from_path_ref_conversion() {
217 let p = std::path::Path::new("/tmp/test.pdf");
218 let input: PdfInput = p.into();
219 assert!(matches!(input, PdfInput::Path(_)));
220 }
221
222 #[test]
223 fn from_vec_conversion() {
224 let v = vec![1, 2, 3];
225 let input: PdfInput = v.into();
226 assert!(matches!(input, PdfInput::Bytes(_)));
227 }
228
229 #[test]
230 fn clone_preserves_variant() {
231 let input = PdfInput::from_bytes(vec![1, 2, 3]);
232 let cloned = input.clone();
233 if let PdfInput::Bytes(b) = cloned {
234 assert_eq!(b, vec![1, 2, 3]);
235 } else {
236 panic!("expected Bytes");
237 }
238 }
239
240 #[test]
241 fn debug_format() {
242 let input = PdfInput::from_bytes(vec![1]);
243 let dbg = format!("{:?}", input);
244 assert!(dbg.contains("Bytes"));
245 }
246}