orinium_browser/platform/io/
mod.rs1use anyhow::{Context, Result};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[allow(dead_code)]
8pub fn load_local_file(path: &str) -> Result<Vec<u8>> {
9 fs::read(path).with_context(|| format!("Failed to read file: {path}"))
10}
11
12fn resource_roots() -> Vec<PathBuf> {
17 let mut roots: Vec<PathBuf> = Vec::new();
18
19 if let Ok(exe) = std::env::current_exe()
21 && let Some(dir) = exe.parent()
22 {
23 roots.push(dir.join("resource"));
24 }
25
26 roots.push(PathBuf::from("resource"));
28
29 if let Ok(cd) = std::env::current_dir() {
31 roots.push(cd.join("resource"));
32 }
33
34 roots
35}
36
37pub(crate) fn load_resource_from(root: &Path, rel_path: &str) -> Result<Option<Vec<u8>>> {
43 let rel = Path::new(rel_path);
47 if rel.is_absolute()
48 || rel.components().any(|c| {
49 matches!(
50 c,
51 std::path::Component::ParentDir | std::path::Component::RootDir
52 )
53 })
54 {
55 anyhow::bail!("Refusing to load resource outside the bundle: {rel_path}");
56 }
57
58 let candidate = root.join(rel_path);
59 if !candidate.is_file() {
60 return Ok(None);
61 }
62
63 if let (Ok(resolved), Ok(root)) = (fs::canonicalize(&candidate), fs::canonicalize(root))
66 && !resolved.starts_with(&root)
67 {
68 anyhow::bail!("Resource escapes the bundle directory: {rel_path}");
69 }
70
71 Ok(Some(fs::read(&candidate).with_context(|| {
72 format!("Failed to read resource {:?}", candidate)
73 })?))
74}
75
76pub fn load_resource(rel_path: &str) -> Result<Vec<u8>> {
85 let roots = resource_roots();
86 for root in &roots {
87 if let Some(bytes) = load_resource_from(root, rel_path)? {
88 return Ok(bytes);
89 }
90 }
91
92 if roots.is_empty() {
94 load_resource_from(Path::new("resource"), rel_path)?;
95 }
96
97 anyhow::bail!("Resource not found: {}", rel_path)
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use std::sync::atomic::{AtomicUsize, Ordering};
104
105 #[test]
106 fn rejects_absolute_paths() {
107 assert!(load_resource("/etc/passwd").is_err());
108 }
109
110 #[test]
111 fn rejects_parent_directory_traversal() {
112 let err = load_resource("../../../../etc/passwd").unwrap_err();
113 assert!(
114 err.to_string().contains("outside the bundle"),
115 "unexpected error: {err}"
116 );
117 }
118
119 #[test]
120 fn loads_a_bundled_resource() {
121 let bytes = load_resource("user-agent.css").expect("user-agent.css is bundled");
122 assert!(!bytes.is_empty());
123 }
124
125 fn temp_dir() -> PathBuf {
127 static COUNTER: AtomicUsize = AtomicUsize::new(0);
128 let name = format!(
129 "orinium-io-{}-{}",
130 std::process::id(),
131 COUNTER.fetch_add(1, Ordering::Relaxed)
132 );
133 let dir = std::env::temp_dir().join(name);
134 fs::create_dir_all(&dir).unwrap();
135 dir
136 }
137
138 #[test]
139 fn loads_nested_path_inside_root() {
140 let root = temp_dir();
141 fs::create_dir_all(root.join("sub")).unwrap();
142 fs::write(root.join("sub/file.txt"), b"nested").unwrap();
143
144 let bytes = load_resource_from(&root, "sub/file.txt").unwrap();
145 assert_eq!(bytes.as_deref(), Some(&b"nested"[..]));
146 assert_eq!(load_resource_from(&root, "sub/missing.txt").unwrap(), None);
147
148 let _ = fs::remove_dir_all(&root);
149 }
150
151 #[test]
152 fn from_root_rejects_absolute_and_parent_paths() {
153 let root = temp_dir();
154 assert!(load_resource_from(&root, "/etc/passwd").is_err());
155 assert!(load_resource_from(&root, "../../etc/passwd").is_err());
156 let _ = fs::remove_dir_all(&root);
157 }
158
159 #[cfg(unix)]
160 #[test]
161 fn rejects_symlink_escaping_root() {
162 let root = temp_dir();
163 let outside = temp_dir();
164 fs::write(outside.join("secret.txt"), b"secret").unwrap();
165 std::os::unix::fs::symlink(outside.join("secret.txt"), root.join("link.txt")).unwrap();
166
167 let err = load_resource_from(&root, "link.txt").unwrap_err();
168 assert!(
169 err.to_string().contains("escapes the bundle"),
170 "unexpected error: {err}"
171 );
172
173 let _ = fs::remove_dir_all(&root);
174 let _ = fs::remove_dir_all(&outside);
175 }
176
177 #[cfg(unix)]
178 #[test]
179 fn allows_symlink_staying_inside_root() {
180 let root = temp_dir();
181 fs::write(root.join("real.txt"), b"inside").unwrap();
182 std::os::unix::fs::symlink(root.join("real.txt"), root.join("alias.txt")).unwrap();
183
184 let bytes = load_resource_from(&root, "alias.txt").unwrap();
185 assert_eq!(bytes.as_deref(), Some(&b"inside"[..]));
186
187 let _ = fs::remove_dir_all(&root);
188 }
189}