onelf_format/
cache_layout.rs1use std::path::{Path, PathBuf};
25
26use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
27
28pub fn is_safe_owned_dir(path: &Path, uid: u32) -> bool {
32 let Ok(md) = std::fs::symlink_metadata(path) else {
33 return false;
34 };
35 md.is_dir() && md.uid() == uid && (md.mode() & 0o077) == 0
36}
37
38pub fn ensure_safe_dir(dir: &Path, uid: u32) -> bool {
43 if let Some(parent) = dir.parent() {
44 let _ = std::fs::create_dir_all(parent);
45 }
46 match std::fs::DirBuilder::new().mode(0o700).create(dir) {
47 Ok(()) => return true,
48 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
49 Err(_) => return false,
50 }
51 if let Ok(md) = std::fs::symlink_metadata(dir)
52 && md.is_dir()
53 && md.uid() == uid
54 && md.mode() & 0o077 != 0
55 {
56 let _ = std::fs::set_permissions(dir, PermissionsExt::from_mode(0o700));
57 }
58 is_safe_owned_dir(dir, uid)
59}
60
61pub fn resolve_root(uid: u32, fallback: Option<PathBuf>) -> Option<PathBuf> {
73 let abs = |v: std::ffi::OsString| -> Option<PathBuf> {
74 let p = PathBuf::from(v);
75 p.is_absolute().then_some(p)
76 };
77 let base = std::env::var_os("XDG_CACHE_HOME")
78 .and_then(abs)
79 .or_else(|| {
80 std::env::var_os("HOME")
81 .and_then(abs)
82 .map(|h| h.join(".cache"))
83 })
84 .or(fallback)?;
85 let onelf = base.join("onelf");
86 ensure_safe_dir(&onelf, uid).then_some(onelf)
87}
88
89pub fn pkg_dir(root: &Path, package_id: &str) -> PathBuf {
91 root.join("pkg").join(package_id)
92}
93
94pub fn ready_marker(root: &Path, package_id: &str) -> PathBuf {
97 root.join("pkg").join(format!(".{package_id}.ready"))
98}
99
100pub fn extract_tmp_dir(root: &Path, package_id: &str) -> PathBuf {
103 root.join("pkg").join(format!(".{package_id}.tmp"))
104}
105
106pub fn lock_path(root: &Path, package_id: &str) -> PathBuf {
108 root.join("lock").join(package_id)
109}
110
111pub fn extract_lock_path(root: &Path, package_id: &str) -> PathBuf {
115 root.join("lock").join(format!("{package_id}.extract"))
116}
117
118pub fn meta_path(root: &Path, package_id: &str) -> PathBuf {
120 root.join("meta").join(package_id)
121}
122
123pub fn platform_dir(root: &Path) -> PathBuf {
134 root.join("platform")
135}
136
137pub fn cas_lock_path(root: &Path) -> PathBuf {
138 root.join("lock").join("cas")
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn unique(tag: &str) -> std::path::PathBuf {
148 use std::sync::atomic::{AtomicU64, Ordering};
149 static SEQ: AtomicU64 = AtomicU64::new(0);
150 std::env::temp_dir().join(format!(
151 "onelf-{tag}-{}-{}",
152 std::process::id(),
153 SEQ.fetch_add(1, Ordering::Relaxed)
154 ))
155 }
156
157 fn uid() -> u32 {
160 let p = unique("uidprobe");
161 std::fs::write(&p, b"").unwrap();
162 let u = std::fs::symlink_metadata(&p).unwrap().uid();
163 let _ = std::fs::remove_file(&p);
164 u
165 }
166
167 #[test]
168 fn safe_dir_rules() {
169 let me = uid();
170 let dir = unique("layout");
171 let _ = std::fs::remove_dir_all(&dir);
172
173 assert!(ensure_safe_dir(&dir, me), "fresh dir is created 0700");
174 assert!(is_safe_owned_dir(&dir, me));
175
176 std::fs::set_permissions(&dir, PermissionsExt::from_mode(0o755)).unwrap();
177 assert!(!is_safe_owned_dir(&dir, me), "0755 is not owner-private");
178 assert!(ensure_safe_dir(&dir, me), "our own dir is tightened back");
179 assert!(is_safe_owned_dir(&dir, me));
180
181 assert!(
182 !is_safe_owned_dir(&dir, me.wrapping_add(1)),
183 "another uid's directory is refused, never chmod'd"
184 );
185
186 let _ = std::fs::remove_dir_all(&dir);
187 }
188
189 #[test]
190 fn missing_and_non_directory_are_unsafe() {
191 let me = uid();
192 assert!(!is_safe_owned_dir(Path::new("/nonexistent/onelf/xyz"), me));
193
194 let f = unique("layout-file");
195 std::fs::write(&f, b"x").unwrap();
196 assert!(!is_safe_owned_dir(&f, me), "a regular file is not a dir");
197 let _ = std::fs::remove_file(&f);
198 }
199
200 #[test]
206 fn relative_env_roots_are_not_trusted() {
207 let me = uid();
208 let fallback = unique("fb");
209 let _ = std::fs::remove_dir_all(&fallback);
210 std::fs::create_dir_all(&fallback).unwrap();
211
212 let prev_xdg = std::env::var_os("XDG_CACHE_HOME");
213 let prev_home = std::env::var_os("HOME");
214 unsafe {
216 std::env::set_var("XDG_CACHE_HOME", "relative/path");
217 std::env::remove_var("HOME");
218 }
219
220 let root = resolve_root(me, Some(fallback.clone()));
221
222 unsafe {
223 match prev_xdg {
224 Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
225 None => std::env::remove_var("XDG_CACHE_HOME"),
226 }
227 if let Some(v) = prev_home {
228 std::env::set_var("HOME", v);
229 }
230 }
231
232 assert_eq!(root, Some(fallback.join("onelf")));
233 let _ = std::fs::remove_dir_all(&fallback);
234 }
235
236 #[test]
237 fn layout_paths_are_stable() {
238 let root = Path::new("/c/onelf");
239 assert_eq!(pkg_dir(root, "ab"), Path::new("/c/onelf/pkg/ab"));
240 assert_eq!(
241 ready_marker(root, "ab"),
242 Path::new("/c/onelf/pkg/.ab.ready")
243 );
244 assert_eq!(lock_path(root, "ab"), Path::new("/c/onelf/lock/ab"));
245 assert_eq!(
246 extract_lock_path(root, "ab"),
247 Path::new("/c/onelf/lock/ab.extract")
248 );
249 assert_eq!(meta_path(root, "ab"), Path::new("/c/onelf/meta/ab"));
250 }
251}