wowlab_fs/
private_file.rs1use std::io::{Read as _, Write as _};
12
13use crate::{
14 atomic, directory,
15 error::{Error, Operation, Result},
16 path::Path,
17};
18
19#[cfg(unix)]
20const PRIVATE_FILE_MODE: u32 = 0o600;
21
22pub fn load_or_create_with<F>(path: &Path, create: F) -> Result<Vec<u8>>
38where
39 F: FnOnce() -> Vec<u8>,
40{
41 match load(path) {
42 Ok(contents) => return Ok(contents),
43 Err(error) if error.is_not_found() => {}
44 Err(error) => return Err(error),
45 }
46
47 let parent = path.parent().ok_or_else(|| {
48 Error::new(
49 Operation::CreateFile,
50 path,
51 std::io::Error::new(
52 std::io::ErrorKind::InvalidInput,
53 "private file destination has no parent",
54 ),
55 )
56 })?;
57
58 directory::ensure(parent)?;
59
60 let contents = create();
61 let mut temporary = tempfile::NamedTempFile::new_in(parent)
62 .map_err(|source| Error::new(Operation::CreateFile, path, source))?;
63
64 restrict(temporary.as_file(), path)?;
65 temporary
66 .write_all(&contents)
67 .map_err(|source| Error::new(Operation::WriteFile, path, source))?;
68 temporary
69 .flush()
70 .map_err(|source| Error::new(Operation::WriteFile, path, source))?;
71 temporary
72 .as_file()
73 .sync_all()
74 .map_err(|source| Error::new(Operation::Sync, path, source))?;
75
76 match temporary.persist_noclobber(path) {
77 Ok(file) => {
78 file.sync_all()
79 .map_err(|source| Error::new(Operation::Sync, path, source))?;
80 atomic::sync_parent(parent, path)?;
81
82 Ok(contents)
83 }
84 Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => load(path),
85 Err(error) => Err(Error::new(Operation::Rename, path, error.error)),
86 }
87}
88
89fn load(path: &Path) -> Result<Vec<u8>> {
90 let entry = std::fs::symlink_metadata(path)
91 .map_err(|source| Error::new(Operation::Inspect, path, source))?;
92
93 if !entry.file_type().is_file() {
94 return Err(not_regular_file(path));
95 }
96
97 let mut options = std::fs::OpenOptions::new();
98
99 options.read(true);
100
101 #[cfg(unix)]
102 {
103 use std::os::unix::fs::OpenOptionsExt as _;
104
105 options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
106 };
107
108 let mut file = options
109 .open(path)
110 .map_err(|source| Error::new(Operation::OpenFile, path, source))?;
111 let opened_entry = file
112 .metadata()
113 .map_err(|source| Error::new(Operation::Inspect, path, source))?;
114
115 if !opened_entry.file_type().is_file() {
116 return Err(not_regular_file(path));
117 }
118
119 restrict(&file, path)?;
120
121 let mut contents = Vec::new();
122
123 file.read_to_end(&mut contents)
124 .map_err(|source| Error::new(Operation::ReadFile, path, source))?;
125
126 Ok(contents)
127}
128
129fn not_regular_file(path: &Path) -> Error {
130 Error::new(
131 Operation::OpenFile,
132 path,
133 std::io::Error::new(
134 std::io::ErrorKind::InvalidData,
135 "private state must be a regular file",
136 ),
137 )
138}
139
140#[cfg(unix)]
141fn restrict(file: &std::fs::File, path: &Path) -> Result<()> {
142 use std::os::unix::fs::PermissionsExt as _;
143
144 file.set_permissions(std::fs::Permissions::from_mode(PRIVATE_FILE_MODE))
145 .map_err(|source| Error::new(Operation::SecureFile, path, source))
146}
147
148#[cfg(not(unix))]
149fn restrict(_file: &std::fs::File, _path: &Path) -> Result<()> {
150 Ok(())
151}
152
153#[cfg(test)]
154mod tests {
155 use std::sync::{
156 Arc, Barrier,
157 atomic::{AtomicU8, Ordering},
158 };
159
160 use googletest::prelude::*;
161
162 use super::{PRIVATE_FILE_MODE, load_or_create_with};
163 use crate::{directory, file, temporary::Directory};
164
165 #[gtest]
166 fn created_bytes_roundtrip_without_visible_temporary_entries() -> Result<()> {
167 let directory = Directory::new().or_fail()?;
168 let path = directory.path().join("identity");
169
170 let created = load_or_create_with(&path, || b"private".to_vec()).or_fail()?;
171 let loaded = load_or_create_with(&path, || b"replacement".to_vec()).or_fail()?;
172
173 verify_that!(created, eq(b"private"))?;
174 verify_that!(loaded, eq(b"private"))?;
175
176 verify_that!(directory::entries(directory.path()).or_fail()?, len(eq(1)))
177 }
178
179 #[gtest]
180 fn creation_ensures_missing_parent_directories() -> Result<()> {
181 let directory = Directory::new().or_fail()?;
182 let path = directory.path().join("config/node/identity");
183
184 let contents = load_or_create_with(&path, || b"private".to_vec()).or_fail()?;
185
186 verify_that!(contents, eq(b"private"))?;
187
188 verify_that!(file::read_bytes(&path).or_fail()?, eq(b"private"))
189 }
190
191 #[gtest]
192 fn existing_non_regular_entry_is_rejected() -> Result<()> {
193 let directory = Directory::new().or_fail()?;
194 let path = directory.path().join("identity");
195
196 directory::ensure(&path).or_fail()?;
197
198 verify_that!(
199 load_or_create_with(&path, Vec::new),
200 err(displays_as(contains_substring("regular file")))
201 )
202 }
203
204 #[gtest]
205 fn concurrent_creators_return_the_committed_winner() -> Result<()> {
206 let directory = Directory::new().or_fail()?;
207 let path = Arc::new(directory.path().join("identity"));
208 let barrier = Arc::new(Barrier::new(2));
209 let sequence = Arc::new(AtomicU8::new(1));
210 let mut threads = Vec::new();
211
212 for _ in 0..2 {
213 let path = Arc::clone(&path);
214 let barrier = Arc::clone(&barrier);
215 let sequence = Arc::clone(&sequence);
216
217 threads.push(std::thread::spawn(move || {
218 load_or_create_with(&path, || {
219 let byte = sequence.fetch_add(1, Ordering::Relaxed);
220
221 barrier.wait();
222
223 vec![byte]
224 })
225 }));
226 }
227
228 let left = threads.remove(0).join().or_fail()?.or_fail()?;
229 let right = threads.remove(0).join().or_fail()?.or_fail()?;
230
231 verify_that!(left, eq(&right))?;
232
233 verify_that!(file::read_bytes(&path).or_fail()?, eq(&left))
234 }
235
236 #[cfg(unix)]
237 #[gtest]
238 fn created_and_loaded_files_are_restricted_before_use() -> Result<()> {
239 use std::os::unix::fs::PermissionsExt as _;
240
241 let directory = Directory::new().or_fail()?;
242 let path = directory.path().join("identity");
243
244 load_or_create_with(&path, || b"private".to_vec()).or_fail()?;
245 let created_mode = std::fs::metadata(&path).or_fail()?.permissions().mode() & 0o777;
246
247 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).or_fail()?;
248 load_or_create_with(&path, Vec::new).or_fail()?;
249 let loaded_mode = std::fs::metadata(&path).or_fail()?.permissions().mode() & 0o777;
250
251 verify_that!(created_mode, eq(PRIVATE_FILE_MODE))?;
252
253 verify_that!(loaded_mode, eq(PRIVATE_FILE_MODE))
254 }
255
256 #[cfg(unix)]
257 #[gtest]
258 fn final_symbolic_link_is_rejected() -> Result<()> {
259 use std::os::unix::fs::symlink;
260
261 let directory = Directory::new().or_fail()?;
262 let target = directory.path().join("target");
263 let path = directory.path().join("identity");
264
265 file::write_bytes(&target, b"private").or_fail()?;
266 symlink(&target, &path).or_fail()?;
267
268 verify_that!(
269 load_or_create_with(&path, Vec::new),
270 err(displays_as(contains_substring("regular file")))
271 )
272 }
273}