Skip to main content

easypdf_core/io/
pdf_input.rs

1//! PDF 输入来源。
2
3use std::path::{Path, PathBuf};
4
5use crate::{PdfError, Result};
6
7use crate::ResourceLimits;
8
9/// 可从文件路径或内存字节读取的 PDF 输入。
10#[derive(Clone, Debug)]
11#[non_exhaustive]
12pub enum PdfInput {
13    /// 文件系统路径。
14    Path(PathBuf),
15    /// 已在内存中的 PDF 字节。
16    Bytes(Vec<u8>),
17}
18
19impl PdfInput {
20    /// 创建路径输入。
21    #[must_use]
22    pub fn from_path(path: impl Into<PathBuf>) -> Self {
23        Self::Path(path.into())
24    }
25
26    /// 创建内存字节输入。
27    #[must_use]
28    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
29        Self::Bytes(bytes.into())
30    }
31
32    /// 在资源限制内读取全部输入字节。
33    ///
34    /// # Errors
35    ///
36    /// 输入不可读或超过字节上限时返回错误。
37    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    /// 当输入来自文件系统时返回路径。
78    #[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]; // %PDF
134        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}