1use std::io;
12use std::path::{Path, PathBuf};
13
14const TRIES: usize = 8;
17
18#[derive(Debug, PartialEq, Eq)]
20pub struct Scratch {
21 path: PathBuf,
22}
23
24impl Scratch {
25 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 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 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 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 #[must_use]
70 pub fn path(&self) -> &Path {
71 &self.path
72 }
73}
74
75impl Drop for Scratch {
76 fn drop(&mut self) {
77 let _ = std::fs::remove_dir_all(&self.path);
79 }
80}
81
82fn 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
91fn 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 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 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}