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 lock_path(root: &Path, package_id: &str) -> PathBuf {
102 root.join("lock").join(package_id)
103}
104
105pub fn extract_lock_path(root: &Path, package_id: &str) -> PathBuf {
109 root.join("lock").join(format!("{package_id}.extract"))
110}
111
112pub fn meta_path(root: &Path, package_id: &str) -> PathBuf {
114 root.join("meta").join(package_id)
115}
116
117pub fn cas_lock_path(root: &Path) -> PathBuf {
126 root.join("lock").join("cas")
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn unique(tag: &str) -> std::path::PathBuf {
136 use std::sync::atomic::{AtomicU64, Ordering};
137 static SEQ: AtomicU64 = AtomicU64::new(0);
138 std::env::temp_dir().join(format!(
139 "onelf-{tag}-{}-{}",
140 std::process::id(),
141 SEQ.fetch_add(1, Ordering::Relaxed)
142 ))
143 }
144
145 fn uid() -> u32 {
148 let p = unique("uidprobe");
149 std::fs::write(&p, b"").unwrap();
150 let u = std::fs::symlink_metadata(&p).unwrap().uid();
151 let _ = std::fs::remove_file(&p);
152 u
153 }
154
155 #[test]
156 fn safe_dir_rules() {
157 let me = uid();
158 let dir = unique("layout");
159 let _ = std::fs::remove_dir_all(&dir);
160
161 assert!(ensure_safe_dir(&dir, me), "fresh dir is created 0700");
162 assert!(is_safe_owned_dir(&dir, me));
163
164 std::fs::set_permissions(&dir, PermissionsExt::from_mode(0o755)).unwrap();
165 assert!(!is_safe_owned_dir(&dir, me), "0755 is not owner-private");
166 assert!(ensure_safe_dir(&dir, me), "our own dir is tightened back");
167 assert!(is_safe_owned_dir(&dir, me));
168
169 assert!(
170 !is_safe_owned_dir(&dir, me.wrapping_add(1)),
171 "another uid's directory is refused, never chmod'd"
172 );
173
174 let _ = std::fs::remove_dir_all(&dir);
175 }
176
177 #[test]
178 fn missing_and_non_directory_are_unsafe() {
179 let me = uid();
180 assert!(!is_safe_owned_dir(Path::new("/nonexistent/onelf/xyz"), me));
181
182 let f = unique("layout-file");
183 std::fs::write(&f, b"x").unwrap();
184 assert!(!is_safe_owned_dir(&f, me), "a regular file is not a dir");
185 let _ = std::fs::remove_file(&f);
186 }
187
188 #[test]
194 fn relative_env_roots_are_not_trusted() {
195 let me = uid();
196 let fallback = unique("fb");
197 let _ = std::fs::remove_dir_all(&fallback);
198 std::fs::create_dir_all(&fallback).unwrap();
199
200 let prev_xdg = std::env::var_os("XDG_CACHE_HOME");
201 let prev_home = std::env::var_os("HOME");
202 unsafe {
204 std::env::set_var("XDG_CACHE_HOME", "relative/path");
205 std::env::remove_var("HOME");
206 }
207
208 let root = resolve_root(me, Some(fallback.clone()));
209
210 unsafe {
211 match prev_xdg {
212 Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
213 None => std::env::remove_var("XDG_CACHE_HOME"),
214 }
215 if let Some(v) = prev_home {
216 std::env::set_var("HOME", v);
217 }
218 }
219
220 assert_eq!(root, Some(fallback.join("onelf")));
221 let _ = std::fs::remove_dir_all(&fallback);
222 }
223
224 #[test]
225 fn layout_paths_are_stable() {
226 let root = Path::new("/c/onelf");
227 assert_eq!(pkg_dir(root, "ab"), Path::new("/c/onelf/pkg/ab"));
228 assert_eq!(
229 ready_marker(root, "ab"),
230 Path::new("/c/onelf/pkg/.ab.ready")
231 );
232 assert_eq!(lock_path(root, "ab"), Path::new("/c/onelf/lock/ab"));
233 assert_eq!(
234 extract_lock_path(root, "ab"),
235 Path::new("/c/onelf/lock/ab.extract")
236 );
237 assert_eq!(meta_path(root, "ab"), Path::new("/c/onelf/meta/ab"));
238 }
239}