1use std::fs::{File, Permissions};
13use std::io::{self, Read, Write};
14use std::os::unix::fs::PermissionsExt;
15use std::path::{Path, PathBuf};
16
17use rustix::fs::{Mode, OFlags};
18use serde::Serialize;
19use serde::de::DeserializeOwned;
20
21use super::private_fs::{self, PRIVATE_FILE_MODE, PrivateDirectoryError, PrivateFileError};
22
23pub struct PrivateDocument {
26 path: PathBuf,
27 maximum_bytes: u64,
28}
29
30impl PrivateDocument {
31 pub fn new(path: impl Into<PathBuf>, maximum_bytes: u64) -> Self {
32 Self {
33 path: path.into(),
34 maximum_bytes,
35 }
36 }
37
38 pub fn path(&self) -> &Path {
39 &self.path
40 }
41
42 pub fn read<T: DeserializeOwned>(&self) -> Result<Option<T>, DocumentError> {
46 private_fs::prepare_directory(self.parent()?).map_err(DocumentError::Directory)?;
47
48 let Some(file) = self.open_existing()? else {
49 return Ok(None);
50 };
51 let length = self.validate(&file)?;
52 let contents = self.read_all(file, length)?;
53 serde_json::from_slice(&contents)
54 .map(Some)
55 .map_err(DocumentError::Parse)
56 }
57
58 pub fn prepare<T: Serialize>(&self, value: &T) -> Result<PreparedWrite, DocumentError> {
61 let directory = self.parent()?;
62 private_fs::prepare_directory(directory).map_err(DocumentError::Directory)?;
63
64 let mut contents = serde_json::to_vec_pretty(value).map_err(DocumentError::Serialize)?;
65 contents.push(b'\n');
66 if contents.len() as u64 > self.maximum_bytes {
67 return Err(DocumentError::SerializedTooLarge {
68 actual_bytes: contents.len() as u64,
69 maximum_bytes: self.maximum_bytes,
70 });
71 }
72
73 if let Some(file) = self.open_existing()? {
76 self.validate(&file)?;
77 }
78
79 let file = |action: &'static str| move |source| DocumentError::File { action, source };
80 let mut temporary =
81 tempfile::NamedTempFile::new_in(directory).map_err(file("create a temporary"))?;
82 temporary
83 .as_file()
84 .set_permissions(Permissions::from_mode(PRIVATE_FILE_MODE))
85 .map_err(file("secure a temporary"))?;
86 temporary
87 .write_all(&contents)
88 .map_err(file("write a temporary"))?;
89 temporary
90 .as_file_mut()
91 .sync_all()
92 .map_err(file("synchronize a temporary"))?;
93
94 Ok(PreparedWrite {
95 temporary,
96 path: self.path.clone(),
97 directory: directory.to_path_buf(),
98 })
99 }
100
101 pub fn claim(&self) -> Result<bool, DocumentError> {
108 private_fs::prepare_directory(self.parent()?).map_err(DocumentError::Directory)?;
109 let flags = OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC;
110 let mode = Mode::from_bits_retain(PRIVATE_FILE_MODE as rustix::fs::RawMode);
111 match rustix::fs::open(&self.path, flags, mode) {
112 Ok(_descriptor) => Ok(true),
113 Err(error) if error == rustix::io::Errno::EXIST => Ok(false),
114 Err(source) => Err(DocumentError::File {
115 action: "claim",
116 source: source.into(),
117 }),
118 }
119 }
120
121 pub fn remove(&self) -> Result<bool, DocumentError> {
123 match std::fs::remove_file(&self.path) {
124 Ok(()) => Ok(true),
125 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
126 Err(source) => Err(DocumentError::File {
127 action: "delete",
128 source,
129 }),
130 }
131 }
132
133 fn parent(&self) -> Result<&Path, DocumentError> {
134 self.path.parent().ok_or(DocumentError::NoParentDirectory)
135 }
136
137 fn open_existing(&self) -> Result<Option<File>, DocumentError> {
138 let flags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK;
139 match rustix::fs::open(&self.path, flags, Mode::empty()) {
140 Ok(descriptor) => Ok(Some(File::from(descriptor))),
141 Err(error) if error == rustix::io::Errno::NOENT => Ok(None),
142 Err(source) => Err(DocumentError::File {
143 action: "open",
144 source: source.into(),
145 }),
146 }
147 }
148
149 fn validate(&self, file: &File) -> Result<u64, DocumentError> {
150 let metadata = private_fs::verify_file(file).map_err(DocumentError::Unusable)?;
151 if metadata.len() > self.maximum_bytes {
152 return Err(DocumentError::TooLarge {
153 actual_bytes: metadata.len(),
154 maximum_bytes: self.maximum_bytes,
155 });
156 }
157 Ok(metadata.len())
158 }
159
160 fn read_all(&self, file: File, length: u64) -> Result<Vec<u8>, DocumentError> {
163 let mut contents = Vec::with_capacity(length as usize);
164 file.take(self.maximum_bytes + 1)
165 .read_to_end(&mut contents)
166 .map_err(|source| DocumentError::File {
167 action: "read",
168 source,
169 })?;
170 if contents.len() as u64 > self.maximum_bytes {
171 return Err(DocumentError::TooLarge {
172 actual_bytes: contents.len() as u64,
173 maximum_bytes: self.maximum_bytes,
174 });
175 }
176 Ok(contents)
177 }
178}
179
180pub struct PreparedWrite {
183 temporary: tempfile::NamedTempFile,
184 path: PathBuf,
185 directory: PathBuf,
186}
187
188impl PreparedWrite {
189 pub fn commit(self) -> Result<CommittedWrite, DocumentError> {
190 let Self {
191 temporary,
192 path,
193 directory,
194 } = self;
195 temporary
196 .persist(&path)
197 .map_err(|error| DocumentError::File {
198 action: "replace the",
199 source: error.error,
200 })?;
201 Ok(CommittedWrite { directory })
202 }
203}
204
205pub struct CommittedWrite {
208 directory: PathBuf,
209}
210
211impl CommittedWrite {
212 pub fn confirm_durability(self) -> Result<(), DocumentError> {
213 File::open(&self.directory)
214 .and_then(|directory| directory.sync_all())
215 .map_err(DocumentError::DurabilityUnconfirmed)
216 }
217}
218
219#[derive(Debug, thiserror::Error)]
220pub enum DocumentError {
221 #[error("the path has no parent directory")]
222 NoParentDirectory,
223
224 #[error("the containing directory is not usable")]
225 Directory(#[source] PrivateDirectoryError),
226
227 #[error("failed to {action} file")]
228 File {
229 action: &'static str,
230 #[source]
231 source: io::Error,
232 },
233
234 #[error("the file is not usable")]
235 Unusable(#[source] PrivateFileError),
236
237 #[error("the file holds {actual_bytes} bytes; maximum is {maximum_bytes} bytes")]
238 TooLarge {
239 actual_bytes: u64,
240 maximum_bytes: u64,
241 },
242
243 #[error("failed to serialize the document")]
244 Serialize(#[source] serde_json::Error),
245
246 #[error("the document serializes to {actual_bytes} bytes; maximum is {maximum_bytes} bytes")]
247 SerializedTooLarge {
248 actual_bytes: u64,
249 maximum_bytes: u64,
250 },
251
252 #[error("failed to parse the file")]
253 Parse(#[source] serde_json::Error),
254
255 #[error("the document was written, but its durability could not be confirmed")]
256 DurabilityUnconfirmed(#[source] io::Error),
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 const CEILING: u64 = 1024;
264
265 fn document() -> (tempfile::TempDir, PrivateDocument) {
268 let home = tempfile::tempdir().expect("a temporary directory");
269 let document = PrivateDocument::new(home.path().join("state").join("thing.json"), CEILING);
270 (home, document)
271 }
272
273 fn write(document: &PrivateDocument, value: &Vec<u32>) {
274 document
275 .prepare(value)
276 .expect("the document serializes")
277 .commit()
278 .expect("the write commits")
279 .confirm_durability()
280 .expect("the rename is durable");
281 }
282
283 #[test]
284 fn an_unwritten_document_reads_as_nothing() {
285 let (_home, document) = document();
286 let read: Option<Vec<u32>> = document.read().expect("the read succeeds");
287 assert!(read.is_none());
288 }
289
290 #[test]
291 fn a_committed_document_reads_back() {
292 let (_home, document) = document();
293 write(&document, &vec![1, 2, 3]);
294 assert_eq!(
295 document.read().expect("the read succeeds"),
296 Some(vec![1, 2, 3])
297 );
298 }
299
300 #[test]
301 fn a_commit_replaces_what_was_there() {
302 let (_home, document) = document();
303 write(&document, &vec![1]);
304 write(&document, &vec![2]);
305 assert_eq!(document.read().expect("the read succeeds"), Some(vec![2]));
306 }
307
308 #[test]
309 fn a_prepared_write_that_is_dropped_changes_nothing() {
310 let (_home, document) = document();
311 write(&document, &vec![1]);
312 drop(document.prepare(&vec![2]).expect("the document serializes"));
313 assert_eq!(document.read().expect("the read succeeds"), Some(vec![1]));
314 }
315
316 #[test]
317 fn a_document_over_the_ceiling_is_refused_before_it_is_written() {
318 let (_home, document) = document();
319 let oversized: Vec<u32> = (0..CEILING as u32).collect();
320 assert!(matches!(
321 document.prepare(&oversized),
322 Err(DocumentError::SerializedTooLarge { .. })
323 ));
324 assert!(!document.path().exists());
325 }
326
327 #[test]
328 fn a_file_left_readable_by_others_is_refused() {
329 let (_home, document) = document();
330 write(&document, &vec![1]);
331 std::fs::set_permissions(document.path(), Permissions::from_mode(0o644))
332 .expect("the mode changes");
333 assert!(matches!(
334 document.read::<Vec<u32>>(),
335 Err(DocumentError::Unusable(..))
336 ));
337 }
338
339 #[test]
340 fn removing_says_whether_anything_was_there() {
341 let (_home, document) = document();
342 write(&document, &vec![1]);
343 assert!(document.remove().expect("the delete succeeds"));
344 assert!(!document.remove().expect("the delete succeeds"));
345 }
346}