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
38pub use crate::fs_open_handles_block_removal as open_handles_block_removal;
41
42#[cfg(feature = "fs")]
43pub use crate::{
44 fs_create_private_file as create_private_file, fs_decode_path_bytes as decode_path_bytes,
45 fs_encode_path_bytes as encode_path_bytes, fs_file_identity as file_identity,
46 fs_is_link_handle as is_link_handle, fs_is_lock_conflict as is_lock_conflict,
47 fs_open_lock_file as open_lock_file, fs_open_read_no_follow as open_read_no_follow,
48 fs_path_identity as path_identity, fs_replace_file as replace_file,
49 fs_state_home_from_environment as state_home_from_environment,
50 fs_sync_directory as sync_directory, fs_try_lock_exclusive as try_lock_exclusive,
51 fs_unlock as unlock, fs_user_config_dir as user_config_dir, fs_user_data_dir as user_data_dir,
52 fs_user_run_data_root as user_run_data_root, fs_user_runtime_dir as user_runtime_dir,
53 fs_user_state_dir as user_state_dir,
54 fs_user_state_dir_from_environment as user_state_dir_from_environment,
55 FsFileIdentity as FileIdentity,
56};
57
58#[cfg(all(test, feature = "fs"))]
59mod tests {
60 use super::*;
61
62 const PRODUCT: &str = "rp-fs-facade-test";
63
64 #[test]
69 fn every_role_is_an_absolute_product_scoped_directory() {
70 for directory in [
71 user_runtime_dir(PRODUCT),
72 user_state_dir(PRODUCT),
73 user_run_data_root(PRODUCT),
74 ] {
75 assert!(
76 directory.is_absolute(),
77 "{} must be absolute",
78 directory.display()
79 );
80 assert!(
81 directory.to_string_lossy().contains(PRODUCT),
82 "{} must be scoped to the product",
83 directory.display()
84 );
85 }
86 }
87
88 #[test]
90 fn distinct_products_do_not_collide() {
91 let other = "rp-fs-facade-other";
92 assert_ne!(user_runtime_dir(PRODUCT), user_runtime_dir(other));
93 assert_ne!(user_state_dir(PRODUCT), user_state_dir(other));
94 assert_ne!(user_run_data_root(PRODUCT), user_run_data_root(other));
95 }
96
97 #[test]
99 fn a_file_has_one_identity_through_both_a_handle_and_its_path() {
100 let dir = std::env::temp_dir().join(format!("rp-fs-identity-{}", std::process::id()));
101 std::fs::create_dir_all(&dir).expect("create dir");
102 let path = dir.join("subject");
103 let file = std::fs::OpenOptions::new()
104 .read(true)
105 .write(true)
106 .create(true)
107 .truncate(true)
108 .open(&path)
109 .expect("create subject");
110
111 let by_handle = file_identity(&file).expect("identity by handle");
112 let by_path = path_identity(&path).expect("identity by path");
113 assert_eq!(by_handle, by_path);
114
115 drop(file);
116 let _ = std::fs::remove_dir_all(&dir);
117 }
118
119 #[test]
122 fn distinct_files_have_distinct_identities() {
123 let dir = std::env::temp_dir().join(format!("rp-fs-identity2-{}", std::process::id()));
124 std::fs::create_dir_all(&dir).expect("create dir");
125 let (first, second) = (dir.join("first"), dir.join("second"));
126 std::fs::write(&first, b"a").expect("write first");
127 std::fs::write(&second, b"b").expect("write second");
128
129 let a = path_identity(&first).expect("identity a");
130 let b = path_identity(&second).expect("identity b");
131 if a.is_some() {
132 assert_ne!(a, b);
133 }
134
135 let _ = std::fs::remove_dir_all(&dir);
136 }
137
138 #[test]
145 fn an_exclusive_lock_excludes_a_second_holder_until_released() {
146 let dir = std::env::temp_dir().join(format!("rp-fs-lock-{}", std::process::id()));
147 std::fs::create_dir_all(&dir).expect("create dir");
148 let path = dir.join("guard.lock");
149
150 let first = open_lock_file(&path).expect("open first");
151 let second = open_lock_file(&path).expect("open second");
152
153 try_lock_exclusive(&first).expect("first acquires");
154 let conflict = try_lock_exclusive(&second).expect_err("second must be refused");
155 assert!(
156 is_lock_conflict(&conflict),
157 "refusal must classify as a conflict, got {conflict:?}"
158 );
159
160 unlock(&first).expect("release first");
161 try_lock_exclusive(&second).expect("second acquires after release");
162 unlock(&second).expect("release second");
163
164 drop((first, second));
165 let _ = std::fs::remove_dir_all(&dir);
166 }
167
168 #[test]
171 fn an_unrelated_error_is_not_a_lock_conflict() {
172 let missing = std::env::temp_dir().join("rp-fs-lock-no-such-file");
173 let _ = std::fs::remove_file(&missing);
174 let error = std::fs::File::open(&missing).expect_err("must not exist");
175 assert!(!is_lock_conflict(&error));
176 }
177
178 #[test]
182 fn a_path_survives_encoding_and_decoding_unchanged() {
183 for original in [
184 std::path::PathBuf::from("relative/leaf.log"),
185 std::env::temp_dir()
186 .join("rp path with spaces")
187 .join("t.log"),
188 std::env::current_exe().expect("current image"),
189 ] {
190 let decoded =
191 decode_path_bytes(&encode_path_bytes(&original)).expect("decode what we encoded");
192 assert_eq!(decoded, original);
193 }
194 }
195
196 #[test]
198 fn an_empty_path_round_trips_as_empty() {
199 let empty = std::path::PathBuf::new();
200 assert!(encode_path_bytes(&empty).is_empty());
201 assert_eq!(
202 decode_path_bytes(&encode_path_bytes(&empty)).expect("decode empty"),
203 empty
204 );
205 }
206
207 #[test]
212 fn a_file_is_replaced_whether_or_not_the_target_exists() {
213 let dir = std::env::temp_dir().join(format!("rp-fs-replace-{}", std::process::id()));
214 std::fs::create_dir_all(&dir).expect("create dir");
215 let target = dir.join("manifest");
216
217 let first = dir.join("first.tmp");
218 std::fs::write(&first, b"first").expect("write first");
219 replace_file(&first, &target).expect("replace absent target");
220 assert_eq!(std::fs::read(&target).expect("read"), b"first");
221
222 let second = dir.join("second.tmp");
223 std::fs::write(&second, b"second").expect("write second");
224 replace_file(&second, &target).expect("replace existing target");
225 assert_eq!(std::fs::read(&target).expect("read"), b"second");
226
227 assert!(!first.exists());
229 assert!(!second.exists());
230
231 sync_directory(&dir).expect("sync the directory that records it");
232 let _ = std::fs::remove_dir_all(&dir);
233 }
234
235 #[test]
238 fn shared_data_is_its_own_role() {
239 let data = user_data_dir(PRODUCT);
240 assert!(data.is_absolute());
241 assert!(data.to_string_lossy().contains(PRODUCT));
242 }
243
244 #[test]
250 fn a_private_file_is_created_once_and_refuses_to_reopen() {
251 let dir = std::env::temp_dir().join(format!("rp-fs-private-{}", std::process::id()));
252 std::fs::create_dir_all(&dir).expect("create dir");
253 let path = dir.join("artifact.json");
254 let _ = std::fs::remove_file(&path);
255
256 {
257 let mut file = create_private_file(&path).expect("create private file");
258 use std::io::Write as _;
259 file.write_all(b"payload").expect("write");
260 }
261 assert_eq!(std::fs::read(&path).expect("read back"), b"payload");
262
263 let second = create_private_file(&path).expect_err("must not open over an existing file");
264 assert_eq!(second.kind(), std::io::ErrorKind::AlreadyExists);
265
266 let _ = std::fs::remove_dir_all(&dir);
267 }
268
269 #[test]
272 fn a_no_follow_open_reads_an_ordinary_file() {
273 let dir = std::env::temp_dir().join(format!("rp-fs-nofollow-{}", std::process::id()));
274 std::fs::create_dir_all(&dir).expect("create dir");
275 let path = dir.join("artifact.json");
276 std::fs::write(&path, b"payload").expect("write");
277
278 let mut file = open_read_no_follow(&path).expect("open ordinary file");
279 let metadata = file.metadata().expect("metadata");
280 assert!(metadata.is_file());
281 assert!(!is_link_handle(&metadata));
282 let mut body = Vec::new();
283 std::io::Read::read_to_end(&mut file, &mut body).expect("read");
284 assert_eq!(body, b"payload");
285 drop(file);
286
287 let _ = std::fs::remove_dir_all(&dir);
288 }
289
290 #[test]
293 fn roles_are_stable_across_calls() {
294 assert_eq!(user_runtime_dir(PRODUCT), user_runtime_dir(PRODUCT));
295 assert_eq!(user_state_dir(PRODUCT), user_state_dir(PRODUCT));
296 assert_eq!(user_run_data_root(PRODUCT), user_run_data_root(PRODUCT));
297 }
298}