Skip to main content

easyofd_reader/
resource_locator.rs

1//! 资源定位器,用于在 OFD 容器中定位和管理资源路径。
2//!
3//! 对应 Java: org.ofdrw.reader.ResourceLocator
4//!
5//! Java 版 `ResourceLocator` 基于解压后的文件系统目录导航。
6//! Rust 版适配为 ZIP 归档内的虚拟路径导航,不依赖文件系统。
7
8use crate::error_path_exception::ErrorPathException;
9
10/// 资源定位器,维护 OFD 容器内的当前工作目录路径。
11///
12/// 对应 Java: `org.ofdrw.reader.ResourceLocator`
13///
14/// 提供 `cd`(切换目录)、`pwd`(打印当前目录)、`save`/`restore`
15/// (保存/恢复目录栈)等操作,用于在 OFD ZIP 归档内构建绝对路径。
16#[derive(Debug, Clone)]
17pub struct ResourceLocator {
18    /// 当前工作目录路径段。
19    work_dir: Vec<String>,
20    /// 保存的路径栈(每次 save 入栈,restore 出栈)。
21    saved_stack: Vec<Vec<String>>,
22}
23
24impl ResourceLocator {
25    /// 创建新的资源定位器,默认位于根目录 "/"。
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            work_dir: vec!["/".to_string()],
30            saved_stack: Vec::new(),
31        }
32    }
33
34    /// 保存当前工作路径到栈中。
35    ///
36    /// 对应 Java: `ResourceLocator.save()`
37    pub fn save(&mut self) {
38        self.saved_stack.push(self.work_dir.clone());
39    }
40
41    /// 还原上一次保存的工作路径。
42    ///
43    /// 如果没有保存过路径,则不做任何操作。
44    ///
45    /// 对应 Java: `ResourceLocator.restore()`
46    pub fn restore(&mut self) {
47        if let Some(saved) = self.saved_stack.pop() {
48            self.work_dir = saved;
49        }
50    }
51
52    /// 切换到指定路径。
53    ///
54    /// 对应 Java: `ResourceLocator.cd(String)`
55    ///
56    /// # 错误
57    ///
58    /// 路径无效时返回 [`ErrorPathException`]。
59    pub fn cd(&mut self, path: &str) -> Result<(), ErrorPathException> {
60        if path.is_empty() {
61            return Ok(());
62        }
63        let path = path.trim();
64        if path == "/" {
65            self.work_dir.clear();
66            self.work_dir.push("/".to_string());
67            return Ok(());
68        }
69        // 解析为绝对路径
70        let abs_path = self.to_absolute_path(path);
71        // 更新工作目录
72        self.work_dir.clear();
73        self.work_dir.push("/".to_string());
74        for segment in abs_path.split('/') {
75            let segment = segment.trim();
76            if segment.is_empty() || segment == "." {
77                continue;
78            }
79            self.work_dir.push(segment.to_string());
80        }
81        Ok(())
82    }
83
84    /// 重置工作路径到根目录。
85    ///
86    /// 对应 Java: `ResourceLocator.restWd()`
87    pub fn reset(&mut self) {
88        self.work_dir.clear();
89        self.work_dir.push("/".to_string());
90    }
91
92    /// 打印当前工作目录路径。
93    ///
94    /// 对应 Java: `ResourceLocator.pwd()`
95    #[must_use]
96    pub fn pwd(&self) -> String {
97        Self::pwd_of(&self.work_dir)
98    }
99
100    /// 将路径转换为绝对路径。
101    ///
102    /// 对应 Java: `ResourceLocator.toAbsolutePath(String)`
103    #[must_use]
104    pub fn to_absolute_path(&self, path: &str) -> String {
105        if path.is_empty() {
106            return self.pwd();
107        }
108        let path = path.trim();
109        let mut segments: Vec<String> = if path.starts_with('/') {
110            vec!["/".to_string()]
111        } else {
112            self.work_dir.clone()
113        };
114
115        for item in path.split('/') {
116            let item = item.trim();
117            if item == "." || item.is_empty() {
118                continue;
119            }
120            if item == ".." {
121                segments.pop();
122                if segments.is_empty() {
123                    segments.push("/".to_string());
124                }
125            } else {
126                segments.push(item.to_string());
127            }
128        }
129        Self::pwd_of(&segments)
130    }
131
132    /// 获取以当前路径为基础的容器内绝对路径。
133    ///
134    /// 对应 Java: `ResourceLocator.getAbsTo(ST_Loc)`
135    #[must_use]
136    pub fn get_abs_to(&self, path: &str) -> String {
137        if path.is_empty() {
138            return self.pwd();
139        }
140        if path.starts_with('/') {
141            return path.to_string();
142        }
143        // 查找最后一个 '/' 分隔文件名和目录部分
144        if let Some(idx) = path.rfind('/') {
145            let dir_part = &path[..=idx];
146            let file_part = &path[idx + 1..];
147            let wd = self.work_dir.clone();
148            let abs_dir = Self::to_absolute_path_of(&wd, dir_part);
149            if abs_dir.ends_with('/') {
150                format!("{abs_dir}{file_part}")
151            } else {
152                format!("{abs_dir}/{file_part}")
153            }
154        } else {
155            let pwd = self.pwd();
156            if pwd.ends_with('/') {
157                format!("{pwd}{path}")
158            } else {
159                format!("{pwd}/{path}")
160            }
161        }
162    }
163
164    /// 内部辅助:计算路径段列表的 pwd。
165    fn pwd_of(segments: &[String]) -> String {
166        if segments.len() <= 1 {
167            return "/".to_string();
168        }
169        let mut result = String::new();
170        for (i, item) in segments.iter().enumerate() {
171            let item = item.trim();
172            if item.is_empty() {
173                continue;
174            }
175            result.push_str(item);
176            if item != "/" && i != segments.len() - 1 {
177                result.push('/');
178            }
179        }
180        result
181    }
182
183    /// 内部辅助:从给定 segments 计算绝对路径。
184    fn to_absolute_path_of(segments: &[String], path: &str) -> String {
185        if path.is_empty() {
186            return Self::pwd_of(segments);
187        }
188        let path = path.trim();
189        let mut work: Vec<String> = if path.starts_with('/') {
190            vec!["/".to_string()]
191        } else {
192            segments.to_vec()
193        };
194
195        for item in path.split('/') {
196            let item = item.trim();
197            if item == "." || item.is_empty() {
198                continue;
199            }
200            if item == ".." {
201                work.pop();
202                if work.is_empty() {
203                    work.push("/".to_string());
204                }
205            } else {
206                work.push(item.to_string());
207            }
208        }
209        Self::pwd_of(&work)
210    }
211}
212
213impl Default for ResourceLocator {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219/// OFD 容器路径模式匹配辅助。
220///
221/// 对应 Java: `ResourceLocator` 中的静态 Pattern 字段。
222pub mod patterns {
223    /// 匹配 Doc_N 目录。
224    pub fn is_doc_dir(segment: &str) -> bool {
225        segment.starts_with("Doc_")
226            && !segment[4..].is_empty()
227            && segment[4..].chars().all(|c| c.is_ascii_digit())
228    }
229
230    /// 匹配 Page_N 目录。
231    pub fn is_page_dir(segment: &str) -> bool {
232        segment.starts_with("Page_")
233            && !segment[5..].is_empty()
234            && segment[5..].chars().all(|c| c.is_ascii_digit())
235    }
236
237    /// 匹配 Sign_N 目录。
238    pub fn is_sign_dir(segment: &str) -> bool {
239        segment.starts_with("Sign_")
240            && !segment[5..].is_empty()
241            && segment[5..].chars().all(|c| c.is_ascii_digit())
242    }
243
244    /// 匹配 Res 目录。
245    pub fn is_res_dir(segment: &str) -> bool {
246        segment == "Res"
247    }
248
249    /// 匹配 Pages 目录。
250    pub fn is_pages_dir(segment: &str) -> bool {
251        segment == "Pages"
252    }
253
254    /// 匹配 Signs 目录。
255    pub fn is_signs_dir(segment: &str) -> bool {
256        segment == "Signs"
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_new_default_root() {
266        let rl = ResourceLocator::new();
267        assert_eq!(rl.pwd(), "/");
268    }
269
270    #[test]
271    fn test_cd_root() {
272        let mut rl = ResourceLocator::new();
273        rl.cd("/Doc_0").unwrap();
274        assert_eq!(rl.pwd(), "/Doc_0");
275        rl.cd("/").unwrap();
276        assert_eq!(rl.pwd(), "/");
277    }
278
279    #[test]
280    fn test_cd_relative() {
281        let mut rl = ResourceLocator::new();
282        rl.cd("Doc_0").unwrap();
283        assert_eq!(rl.pwd(), "/Doc_0");
284        rl.cd("Pages").unwrap();
285        assert_eq!(rl.pwd(), "/Doc_0/Pages");
286    }
287
288    #[test]
289    fn test_cd_parent() {
290        let mut rl = ResourceLocator::new();
291        rl.cd("/Doc_0/Pages/Page_0").unwrap();
292        rl.cd("..").unwrap();
293        assert_eq!(rl.pwd(), "/Doc_0/Pages");
294    }
295
296    #[test]
297    fn test_save_restore() {
298        let mut rl = ResourceLocator::new();
299        rl.cd("/Doc_0").unwrap();
300        rl.save();
301        rl.cd("Pages").unwrap();
302        assert_eq!(rl.pwd(), "/Doc_0/Pages");
303        rl.restore();
304        assert_eq!(rl.pwd(), "/Doc_0");
305    }
306
307    #[test]
308    fn test_save_restore_nested() {
309        let mut rl = ResourceLocator::new();
310        rl.cd("/Doc_0").unwrap();
311        rl.save();
312        rl.cd("Pages").unwrap();
313        rl.save();
314        rl.cd("Page_0").unwrap();
315        assert_eq!(rl.pwd(), "/Doc_0/Pages/Page_0");
316        rl.restore();
317        assert_eq!(rl.pwd(), "/Doc_0/Pages");
318        rl.restore();
319        assert_eq!(rl.pwd(), "/Doc_0");
320    }
321
322    #[test]
323    fn test_restore_empty_stack() {
324        let mut rl = ResourceLocator::new();
325        rl.cd("/Doc_0").unwrap();
326        // restore without save should not change anything
327        rl.restore();
328        assert_eq!(rl.pwd(), "/Doc_0");
329    }
330
331    #[test]
332    fn test_to_absolute_path_absolute() {
333        let mut rl = ResourceLocator::new();
334        rl.cd("/Doc_0/Pages").unwrap();
335        assert_eq!(rl.to_absolute_path("/Doc_1/Res"), "/Doc_1/Res");
336    }
337
338    #[test]
339    fn test_to_absolute_path_relative() {
340        let mut rl = ResourceLocator::new();
341        rl.cd("/Doc_0").unwrap();
342        assert_eq!(rl.to_absolute_path("Res/image.png"), "/Doc_0/Res/image.png");
343    }
344
345    #[test]
346    fn test_to_absolute_path_parent() {
347        let mut rl = ResourceLocator::new();
348        rl.cd("/Doc_0/Pages/Page_0").unwrap();
349        assert_eq!(rl.to_absolute_path("../Res"), "/Doc_0/Pages/Res");
350    }
351
352    #[test]
353    fn test_get_abs_to_absolute() {
354        let rl = ResourceLocator::new();
355        assert_eq!(rl.get_abs_to("/Doc_0/Res"), "/Doc_0/Res");
356    }
357
358    #[test]
359    fn test_get_abs_to_relative() {
360        let mut rl = ResourceLocator::new();
361        rl.cd("/Doc_0").unwrap();
362        assert_eq!(rl.get_abs_to("Res/image.png"), "/Doc_0/Res/image.png");
363    }
364
365    #[test]
366    fn test_get_abs_to_filename_only() {
367        let mut rl = ResourceLocator::new();
368        rl.cd("/Doc_0/Pages/Page_0").unwrap();
369        assert_eq!(
370            rl.get_abs_to("Content.xml"),
371            "/Doc_0/Pages/Page_0/Content.xml"
372        );
373    }
374
375    #[test]
376    fn test_reset() {
377        let mut rl = ResourceLocator::new();
378        rl.cd("/Doc_0/Pages").unwrap();
379        rl.reset();
380        assert_eq!(rl.pwd(), "/");
381    }
382
383    #[test]
384    fn test_cd_empty_string() {
385        let mut rl = ResourceLocator::new();
386        rl.cd("/Doc_0").unwrap();
387        rl.cd("").unwrap();
388        assert_eq!(rl.pwd(), "/Doc_0");
389    }
390
391    #[test]
392    fn test_patterns() {
393        assert!(patterns::is_doc_dir("Doc_0"));
394        assert!(patterns::is_doc_dir("Doc_12"));
395        assert!(!patterns::is_doc_dir("Doc_"));
396        assert!(!patterns::is_doc_dir("Pages"));
397
398        assert!(patterns::is_page_dir("Page_0"));
399        assert!(patterns::is_page_dir("Page_99"));
400        assert!(!patterns::is_page_dir("Page_"));
401
402        assert!(patterns::is_sign_dir("Sign_0"));
403        assert!(patterns::is_res_dir("Res"));
404        assert!(patterns::is_pages_dir("Pages"));
405        assert!(patterns::is_signs_dir("Signs"));
406    }
407}