git.lucas.co / cce-model
3D model viewer: STL and OBJ
git clone https://git.lucas.co/cce-model.git

src/files.rs (3.3K)

 1 //! Which files the viewer can step through, and what a drop hands it.
 2 
 3 use std::path::{Path, PathBuf};
 4 
 5 /// True when the extension is one cce-mesh-io reads.
 6 pub fn readable(path: &Path) -> bool {
 7     path.extension()
 8         .and_then(|e| e.to_str())
 9         .is_some_and(|e| cce_mesh_io::EXTENSIONS.contains(&e.to_ascii_lowercase().as_str()))
10 }
11 
12 /// The models in `path`'s folder, by name (case aside), and where `path` is
13 /// among them — what ←/→ step through. A folder that cannot be read gives
14 /// the file alone.
15 pub fn siblings(path: &Path) -> (Vec<PathBuf>, usize) {
16     let mut files: Vec<PathBuf> = path
17         .parent()
18         .and_then(|dir| std::fs::read_dir(if dir.as_os_str().is_empty() { Path::new(".") } else { dir }).ok())
19         .into_iter()
20         .flatten()
21         .filter_map(|e| e.ok().map(|e| e.path()))
22         .filter(|p| p.is_file() && readable(p))
23         .collect();
24     files.sort_by_key(|p| p.file_name().map(|n| n.to_string_lossy().to_lowercase()));
25     let at = files.iter().position(|p| p.file_name() == path.file_name());
26     match at {
27         Some(i) => (files, i),
28         None => (vec![path.to_path_buf()], 0),
29     }
30 }
31 
32 /// The local paths in a `text/uri-list` drop (RFC 2483: one URI a line,
33 /// `#` lines are comments), percent-decoded. Anything not `file:` is left
34 /// out: the viewer opens files, not URLs.
35 pub fn dropped_paths(data: &[u8]) -> Vec<PathBuf> {
36     String::from_utf8_lossy(data)
37         .lines()
38         .map(str::trim)
39         .filter(|l| !l.is_empty() && !l.starts_with('#'))
40         .filter_map(|l| l.strip_prefix("file://"))
41         // file://host/path: only the local host (empty, or localhost).
42         .filter_map(|rest| rest.strip_prefix("localhost").or(Some(rest)).filter(|r| r.starts_with('/')))
43         .map(|p| PathBuf::from(percent_decode(p)))
44         .collect()
45 }
46 
47 fn percent_decode(s: &str) -> String {
48     let b = s.as_bytes();
49     let mut out = Vec::with_capacity(b.len());
50     let mut i = 0;
51     while i < b.len() {
52         if b[i] == b'%' && i + 2 < b.len() {
53             if let Ok(v) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
54                 out.push(v);
55                 i += 3;
56                 continue;
57             }
58         }
59         out.push(b[i]);
60         i += 1;
61     }
62     String::from_utf8_lossy(&out).into_owned()
63 }
64 
65 #[cfg(test)]
66 mod tests {
67     use super::*;
68 
69     #[test]
70     fn a_uri_list_gives_its_local_files() {
71         let list = b"# from cce-files\r\nfile:///home/me/My%20Parts/bracket.stl\r\nhttps://example.com/x.stl\r\nfile://localhost/tmp/a.glb\r\nfile://elsewhere/tmp/b.glb\r\n";
72         assert_eq!(dropped_paths(list), vec![PathBuf::from("/home/me/My Parts/bracket.stl"), PathBuf::from("/tmp/a.glb")]);
73     }
74 
75     #[test]
76     fn siblings_are_the_models_in_the_folder_in_name_order() {
77         let dir = std::env::temp_dir().join(format!("cce-model-files-{}", std::process::id()));
78         std::fs::create_dir_all(&dir).unwrap();
79         for f in ["b.STL", "a.obj", "notes.txt", "c.glb"] {
80             std::fs::write(dir.join(f), b"").unwrap();
81         }
82         let (files, at) = siblings(&dir.join("b.STL"));
83         std::fs::remove_dir_all(&dir).ok();
84         let names: Vec<_> = files.iter().map(|p| p.file_name().unwrap().to_str().unwrap()).collect();
85         assert_eq!(names, ["a.obj", "b.STL", "c.glb"]);
86         assert_eq!(at, 1);
87     }
88 }