running_process_platform_internal/platform/
fs.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct RawDescriptor(usize);
23
24impl RawDescriptor {
25 pub(crate) fn from_value(value: usize) -> Self {
27 Self(value)
28 }
29
30 pub(crate) fn value(self) -> usize {
32 self.0
33 }
34}
35
36pub use crate::fs_write_all_to_descriptor as write_all_to_descriptor;
37
38#[cfg(feature = "fs")]
39pub use crate::{
40 fs_create_private_file as create_private_file, fs_decode_path_bytes as decode_path_bytes,
41 fs_encode_path_bytes as encode_path_bytes, fs_file_identity as file_identity,
42 fs_is_lock_conflict as is_lock_conflict, fs_open_lock_file as open_lock_file,
43 fs_path_identity as path_identity, fs_replace_file as replace_file,
44 fs_sync_directory as sync_directory, fs_try_lock_exclusive as try_lock_exclusive,
45 fs_unlock as unlock, fs_user_config_dir as user_config_dir, fs_user_data_dir as user_data_dir,
46 fs_user_run_data_root as user_run_data_root, fs_user_runtime_dir as user_runtime_dir,
47 fs_user_state_dir as user_state_dir, FsFileIdentity as FileIdentity,
48};
49
50#[cfg(all(test, feature = "fs"))]
51mod tests {
52 use super::*;
53
54 const PRODUCT: &str = "rp-fs-facade-test";
55
56 #[test]
61 fn every_role_is_an_absolute_product_scoped_directory() {
62 for directory in [
63 user_runtime_dir(PRODUCT),
64 user_state_dir(PRODUCT),
65 user_run_data_root(PRODUCT),
66 ] {
67 assert!(
68 directory.is_absolute(),
69 "{} must be absolute",
70 directory.display()
71 );
72 assert!(
73 directory.to_string_lossy().contains(PRODUCT),
74 "{} must be scoped to the product",
75 directory.display()
76 );
77 }
78 }
79
80 #[test]
82 fn distinct_products_do_not_collide() {
83 let other = "rp-fs-facade-other";
84 assert_ne!(user_runtime_dir(PRODUCT), user_runtime_dir(other));
85 assert_ne!(user_state_dir(PRODUCT), user_state_dir(other));
86 assert_ne!(user_run_data_root(PRODUCT), user_run_data_root(other));
87 }
88
89 #[test]
91 fn a_file_has_one_identity_through_both_a_handle_and_its_path() {
92 let dir = std::env::temp_dir().join(format!("rp-fs-identity-{}", std::process::id()));
93 std::fs::create_dir_all(&dir).expect("create dir");
94 let path = dir.join("subject");
95 let file = std::fs::OpenOptions::new()
96 .read(true)
97 .write(true)
98 .create(true)
99 .truncate(true)
100 .open(&path)
101 .expect("create subject");
102
103 let by_handle = file_identity(&file).expect("identity by handle");
104 let by_path = path_identity(&path).expect("identity by path");
105 assert_eq!(by_handle, by_path);
106
107 drop(file);
108 let _ = std::fs::remove_dir_all(&dir);
109 }
110
111 #[test]
114 fn distinct_files_have_distinct_identities() {
115 let dir = std::env::temp_dir().join(format!("rp-fs-identity2-{}", std::process::id()));
116 std::fs::create_dir_all(&dir).expect("create dir");
117 let (first, second) = (dir.join("first"), dir.join("second"));
118 std::fs::write(&first, b"a").expect("write first");
119 std::fs::write(&second, b"b").expect("write second");
120
121 let a = path_identity(&first).expect("identity a");
122 let b = path_identity(&second).expect("identity b");
123 if a.is_some() {
124 assert_ne!(a, b);
125 }
126
127 let _ = std::fs::remove_dir_all(&dir);
128 }
129
130 #[test]
137 fn an_exclusive_lock_excludes_a_second_holder_until_released() {
138 let dir = std::env::temp_dir().join(format!("rp-fs-lock-{}", std::process::id()));
139 std::fs::create_dir_all(&dir).expect("create dir");
140 let path = dir.join("guard.lock");
141
142 let first = open_lock_file(&path).expect("open first");
143 let second = open_lock_file(&path).expect("open second");
144
145 try_lock_exclusive(&first).expect("first acquires");
146 let conflict = try_lock_exclusive(&second).expect_err("second must be refused");
147 assert!(
148 is_lock_conflict(&conflict),
149 "refusal must classify as a conflict, got {conflict:?}"
150 );
151
152 unlock(&first).expect("release first");
153 try_lock_exclusive(&second).expect("second acquires after release");
154 unlock(&second).expect("release second");
155
156 drop((first, second));
157 let _ = std::fs::remove_dir_all(&dir);
158 }
159
160 #[test]
163 fn an_unrelated_error_is_not_a_lock_conflict() {
164 let missing = std::env::temp_dir().join("rp-fs-lock-no-such-file");
165 let _ = std::fs::remove_file(&missing);
166 let error = std::fs::File::open(&missing).expect_err("must not exist");
167 assert!(!is_lock_conflict(&error));
168 }
169
170 #[test]
174 fn a_path_survives_encoding_and_decoding_unchanged() {
175 for original in [
176 std::path::PathBuf::from("relative/leaf.log"),
177 std::env::temp_dir()
178 .join("rp path with spaces")
179 .join("t.log"),
180 std::env::current_exe().expect("current image"),
181 ] {
182 let decoded =
183 decode_path_bytes(&encode_path_bytes(&original)).expect("decode what we encoded");
184 assert_eq!(decoded, original);
185 }
186 }
187
188 #[test]
190 fn an_empty_path_round_trips_as_empty() {
191 let empty = std::path::PathBuf::new();
192 assert!(encode_path_bytes(&empty).is_empty());
193 assert_eq!(
194 decode_path_bytes(&encode_path_bytes(&empty)).expect("decode empty"),
195 empty
196 );
197 }
198
199 #[test]
204 fn a_file_is_replaced_whether_or_not_the_target_exists() {
205 let dir = std::env::temp_dir().join(format!("rp-fs-replace-{}", std::process::id()));
206 std::fs::create_dir_all(&dir).expect("create dir");
207 let target = dir.join("manifest");
208
209 let first = dir.join("first.tmp");
210 std::fs::write(&first, b"first").expect("write first");
211 replace_file(&first, &target).expect("replace absent target");
212 assert_eq!(std::fs::read(&target).expect("read"), b"first");
213
214 let second = dir.join("second.tmp");
215 std::fs::write(&second, b"second").expect("write second");
216 replace_file(&second, &target).expect("replace existing target");
217 assert_eq!(std::fs::read(&target).expect("read"), b"second");
218
219 assert!(!first.exists());
221 assert!(!second.exists());
222
223 sync_directory(&dir).expect("sync the directory that records it");
224 let _ = std::fs::remove_dir_all(&dir);
225 }
226
227 #[test]
230 fn shared_data_is_its_own_role() {
231 let data = user_data_dir(PRODUCT);
232 assert!(data.is_absolute());
233 assert!(data.to_string_lossy().contains(PRODUCT));
234 }
235
236 #[test]
242 fn a_private_file_is_created_once_and_refuses_to_reopen() {
243 let dir = std::env::temp_dir().join(format!("rp-fs-private-{}", std::process::id()));
244 std::fs::create_dir_all(&dir).expect("create dir");
245 let path = dir.join("artifact.json");
246 let _ = std::fs::remove_file(&path);
247
248 {
249 let mut file = create_private_file(&path).expect("create private file");
250 use std::io::Write as _;
251 file.write_all(b"payload").expect("write");
252 }
253 assert_eq!(std::fs::read(&path).expect("read back"), b"payload");
254
255 let second = create_private_file(&path).expect_err("must not open over an existing file");
256 assert_eq!(second.kind(), std::io::ErrorKind::AlreadyExists);
257
258 let _ = std::fs::remove_dir_all(&dir);
259 }
260
261 #[test]
264 fn roles_are_stable_across_calls() {
265 assert_eq!(user_runtime_dir(PRODUCT), user_runtime_dir(PRODUCT));
266 assert_eq!(user_state_dir(PRODUCT), user_state_dir(PRODUCT));
267 assert_eq!(user_run_data_root(PRODUCT), user_run_data_root(PRODUCT));
268 }
269}