Skip to main content

qcode/engine/
scratch.rs

1//! Folders of this machine that a container is handed for one piece of work: the folder a login
2//! is taken out into, the folder a sign-in window leaves addresses in, the context an image is
3//! built from.
4//!
5//! What passes through them can be the person's own: a login is a token that signs in to their
6//! account. So a folder is the person's alone (0700), under the session's own runtime folder when
7//! there is one, and made fresh under a name nobody could have guessed: another user of the
8//! machine can neither read it nor put a folder of their own where QCode is about to write. It is
9//! taken away when its owner is done with it.
10
11use std::io;
12use std::path::{Path, PathBuf};
13
14/// How many names are tried before making a folder is given up on. Each is random, so a clash is
15/// someone else's folder in the way, never bad luck; a few tries cover a clash and no more.
16const TRIES: usize = 8;
17
18/// A private folder, removed with everything in it when this is dropped.
19#[derive(Debug, PartialEq, Eq)]
20pub struct Scratch {
21    path: PathBuf,
22}
23
24impl Scratch {
25    /// Makes a new private folder for `purpose`, a word that ends up in its name so a person
26    /// looking at the folder can tell what left it.
27    ///
28    /// # Errors
29    ///
30    /// When no folder can be made in either place.
31    pub fn new(purpose: &str) -> io::Result<Self> {
32        let runtime = std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from);
33        Self::in_session(runtime.as_deref(), purpose)
34    }
35
36    /// The same, with the session's runtime folder given: that folder when it is usable, since
37    /// it is the person's own and gone when they log out, and this machine's temporary folder
38    /// otherwise.
39    fn in_session(runtime: Option<&Path>, purpose: &str) -> io::Result<Self> {
40        if let Some(dir) = runtime.filter(|dir| dir.is_absolute())
41            && let Ok(scratch) = Self::within(dir, purpose)
42        {
43            return Ok(scratch);
44        }
45        Self::within(&std::env::temp_dir(), purpose)
46    }
47
48    /// Makes a new private folder for `purpose` inside `parent`.
49    ///
50    /// # Errors
51    ///
52    /// When `parent` cannot be written to, or every name tried was taken.
53    pub fn within(parent: &Path, purpose: &str) -> io::Result<Self> {
54        let mut last = io::Error::from(io::ErrorKind::AlreadyExists);
55        for _ in 0..TRIES {
56            let path = parent.join(format!("qcode-{purpose}-{}", random_word()));
57            // Making the folder, never finding one: a path that is already there, whoever made
58            // it, is not taken over but passed by.
59            match make_private(&path) {
60                Ok(()) => return Ok(Self { path }),
61                Err(error) if error.kind() == io::ErrorKind::AlreadyExists => last = error,
62                Err(error) => return Err(error),
63            }
64        }
65        Err(last)
66    }
67
68    /// Where the folder is.
69    #[must_use]
70    pub fn path(&self) -> &Path {
71        &self.path
72    }
73}
74
75impl Drop for Scratch {
76    fn drop(&mut self) {
77        // A folder already taken away by its owner is what is wanted anyway.
78        let _ = std::fs::remove_dir_all(&self.path);
79    }
80}
81
82/// Makes one folder, readable by its owner alone from the moment it exists, so there is no
83/// instant in which it is open to others.
84fn make_private(path: &Path) -> io::Result<()> {
85    let mut builder = std::fs::DirBuilder::new();
86    #[cfg(unix)]
87    std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
88    builder.create(path)
89}
90
91/// A word nobody else on the machine can know in advance. The standard library's hasher keys come
92/// from the operating system's randomness, fresh for each `RandomState`, which is exactly that.
93fn random_word() -> String {
94    use std::hash::{BuildHasher, Hasher};
95    let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
96    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
97    hasher.write_u128(now.as_nanos());
98    hasher.write_u32(std::process::id());
99    format!("{:016x}", hasher.finish())
100}
101
102#[cfg(test)]
103mod tests {
104    use super::Scratch;
105    use std::path::PathBuf;
106
107    /// A parent folder of the test's own, so the checks never depend on what the session offers.
108    fn parent(name: &str) -> PathBuf {
109        let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos();
110        let dir = std::env::temp_dir().join(format!("qcode-scratch-test-{name}-{}-{stamp}", std::process::id()));
111        std::fs::create_dir_all(&dir).expect("a parent folder");
112        dir
113    }
114
115    #[cfg(unix)]
116    #[test]
117    fn the_folder_is_its_owners_alone() {
118        use std::os::unix::fs::PermissionsExt as _;
119        let dir = parent("mode");
120        let scratch = Scratch::within(&dir, "login-mode").expect("a folder");
121        let mode = std::fs::metadata(scratch.path()).expect("it is there").permissions().mode() & 0o777;
122        drop(scratch);
123        let _ = std::fs::remove_dir_all(&dir);
124        assert_eq!(mode, 0o700);
125    }
126
127    #[test]
128    fn two_folders_for_the_same_work_have_different_names() {
129        let dir = parent("names");
130        let one = Scratch::within(&dir, "login-same").expect("a folder");
131        let two = Scratch::within(&dir, "login-same").expect("a folder");
132        let (one_path, two_path) = (one.path().to_owned(), two.path().to_owned());
133        drop((one, two));
134        let _ = std::fs::remove_dir_all(&dir);
135        assert_ne!(one_path, two_path);
136        assert!(one_path.starts_with(&dir) && two_path.starts_with(&dir), "{}", one_path.display());
137    }
138
139    #[test]
140    fn the_folder_and_what_is_in_it_go_when_its_owner_is_done() {
141        let dir = parent("drop");
142        let scratch = Scratch::within(&dir, "login-drop").expect("a folder");
143        let path = scratch.path().to_owned();
144        std::fs::create_dir_all(path.join("inner")).expect("something inside");
145        std::fs::write(path.join("inner").join("token.json"), "{}").expect("a file inside");
146        drop(scratch);
147        let gone = !path.exists();
148        let _ = std::fs::remove_dir_all(&dir);
149        assert!(gone, "{} is still there", path.display());
150    }
151
152    #[test]
153    fn a_folder_someone_made_ahead_of_time_is_never_taken_over() {
154        let dir = parent("taken");
155        // A folder where QCode is about to make one, made ahead of time the way another user
156        // of the machine could.
157        let planted = dir.join("qcode-login-planted");
158        std::fs::create_dir_all(&planted).expect("a planted folder");
159        std::fs::write(planted.join("theirs"), "").expect("a file of theirs");
160        let made = super::make_private(&planted);
161        let kept = planted.join("theirs").exists();
162        let _ = std::fs::remove_dir_all(&dir);
163        let error = made.expect_err("an existing folder is not made again");
164        assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
165        assert!(kept);
166    }
167
168    #[test]
169    fn the_sessions_runtime_folder_is_used_when_it_can_be_and_the_temporary_folder_otherwise() {
170        let dir = parent("runtime");
171        let inside = Scratch::in_session(Some(&dir), "signin-runtime").expect("a folder");
172        let missing = dir.join("gone");
173        let elsewhere = Scratch::in_session(Some(&missing), "signin-runtime").expect("a folder");
174        let relative = Scratch::in_session(Some(std::path::Path::new("run")), "signin-runtime").expect("a folder");
175        let (inside_path, elsewhere_path, relative_path) =
176            (inside.path().to_owned(), elsewhere.path().to_owned(), relative.path().to_owned());
177        drop((inside, elsewhere, relative));
178        let _ = std::fs::remove_dir_all(&dir);
179        assert!(inside_path.starts_with(&dir), "{}", inside_path.display());
180        assert!(elsewhere_path.starts_with(std::env::temp_dir()), "{}", elsewhere_path.display());
181        assert!(relative_path.starts_with(std::env::temp_dir()), "{}", relative_path.display());
182    }
183}