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 }