git_policy_cli/private_storage.rs
1//! What:
2//! Private,
3//! crash-safe file primitives shared by every durable record of the wrapper:
4//! exclusive no-follow creation with private modes,
5//! file and directory sync,
6//! and
7//! no-follow reads that refuse links and non-regular files.
8//! Why:
9//! Lock records,
10//! transaction journals and capture records must survive a crash in a
11//! readable state,
12//! must never be written through a planted link,
13//! and must be private to
14//! the account.
15//! The incumbent's `src/trust/registry-io.ts` defines these semantics;
16//! both
17//! wrappers write and read the same files.
18//!
19//! In TS you'd write (pseudocode):
20//! ```ts
21//! await writePrivateFile({ path, bytes }); await syncDirectory(dirname(path));
22//! ```
23
24/// What:
25/// `File` is an open file handle;
26/// `OpenOptions` configures how one is opened.
27/// Why:
28/// Exclusive creation and no-follow opening need the configurable form.
29///
30/// In TS you'd write (pseudocode):
31/// ```ts
32/// import { open, constants } from 'node:fs/promises';
33/// ```
34use std::fs::{File, OpenOptions};
35/// The trait that gives files `.read_to_end` and `.take`.
36use std::io::Read;
37/// The trait that gives files `.write_all`.
38use std::io::Write;
39/// Owned operating-system text for the staging name.
40use std::ffi::OsString;
41/// `Path` is a borrowed filesystem path of raw bytes; `PathBuf` is the owned one.
42use std::path::{Path, PathBuf};
43/// Diagnostics for a decision no failure reports.
44use super::diagnostic_log::debug;
45/// Unguessable staging names.
46use super::random_id::random_uuid;
47
48/// Mode of every private directory the wrapper creates:
49/// owner only.
50pub const PRIVATE_DIRECTORY_MODE: u32 = 0o700;
51
52/// Mode of every private file the wrapper creates:
53/// owner read and write only.
54pub const PRIVATE_FILE_MODE: u32 = 0o600;
55
56/// What:
57/// Why a no-follow read refused a path.
58/// An `enum` is a closed set of alternatives.
59/// Why:
60/// A missing file is an expected answer for optional records;
61/// a link,
62/// a directory or an
63/// over-long file is unsafe or corrupt state that recovery must name,
64/// never follow.
65///
66/// In TS you'd write (pseudocode):
67/// ```ts
68/// type ReadRefusal = { kind: 'missing' } | { kind: 'not-regular' } | { kind: 'too-large' } | { kind: 'io'; error: Error };
69/// ```
70#[derive(Debug)]
71pub enum ReadRefusal {
72 /// Nothing exists at the path.
73 Missing,
74 /// The path names a link,
75 /// directory or other non-regular file.
76 NotRegular,
77 /// The file holds more bytes than the caller allows.
78 TooLarge,
79 /// The operating system refused the read for another reason.
80 Io(
81 /// The operating-system error that refused the read.
82 std::io::Error,
83 ),
84}
85
86/// What:
87/// Open options for an exclusive,
88/// no-follow,
89/// private-mode creation.
90/// `#[cfg(unix)]` compiles the block only on Unix-like systems.
91/// Why:
92/// `O_EXCL` makes a second writer fail instead of replacing the record;
93/// `O_NOFOLLOW`
94/// refuses a planted final link;
95/// mode `0600` keeps the record private from the start.
96///
97/// In TS you'd write (pseudocode):
98/// ```ts
99/// open(path, O_CREAT | O_EXCL | O_WRONLY | O_NOFOLLOW, 0o600)
100/// ```
101fn exclusive_options() -> OpenOptions {
102 // `mut` allows the builder to be configured step by step.
103 let mut options: OpenOptions = OpenOptions::new();
104 options.write(true).create_new(true);
105 #[cfg(unix)]
106 {
107 // The trait adds `.mode` and `.custom_flags` to open options on Unix.
108 /// The Unix open-option extensions,
109 /// for the mode and no-follow flags.
110 use std::os::unix::fs::OpenOptionsExt;
111 options
112 .mode(PRIVATE_FILE_MODE)
113 .custom_flags(libc::O_NOFOLLOW);
114 }
115 return options;
116}
117
118/// What:
119/// Set a path's permission bits to `mode`,
120/// on Unix only.
121/// `std::io::Result<()>` is "nothing,
122/// or an operating-system error".
123/// Why:
124/// The creation mode is filtered by the process `umask`;
125/// an explicit change makes the
126/// mode exactly private whatever the caller's `umask` is,
127/// as the incumbent's
128/// `protectPath` does.
129/// Windows keeps its inherited access rules (recorded under
130/// "Choices open to veto" in `doc/handover/cli-git-native-transactions.md`).
131///
132/// In TS you'd write (pseudocode):
133/// ```ts
134/// if (process.platform !== 'win32') await chmod(path, mode);
135/// ```
136pub fn protect_path(path: &Path, mode: u32) -> std::io::Result<()> {
137 #[cfg(unix)]
138 {
139 // The trait adds `.from_mode` to permissions on Unix.
140 /// The Unix permission extensions,
141 /// for the mode.
142 use std::os::unix::fs::PermissionsExt;
143 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
144 }
145 // `let _ = mode;` marks the argument as used where no mode exists.
146 // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
147 #[cfg(not(unix))]
148 let _ = (path, mode);
149 return Ok(());
150}
151
152/// What:
153/// The name of the staging file beside `path`: hidden, unique, ending in `.partial`.
154/// Why:
155/// Recovery finds records by their exact names, so a staging file never looks like one.
156///
157/// In TS you'd write (pseudocode):
158/// ```ts
159/// join(dirname(path), `.${basename(path)}.${randomUUID()}.partial`)
160/// ```
161fn staging_path(path: &Path) -> std::io::Result<PathBuf> {
162 let Some(name) = path.file_name() else {
163 return Err(std::io::Error::new(
164 std::io::ErrorKind::InvalidInput,
165 format!("{} names no file", path.display()),
166 ));
167 };
168 let unique: String = random_uuid().map_err(|error| { return std::io::Error::other(error.0); })?;
169 let mut staged: OsString = OsString::from(".");
170 staged.push(name);
171 staged.push(".");
172 staged.push(unique);
173 staged.push(".partial");
174 return Ok(path.with_file_name(staged));
175}
176
177/// What:
178/// Make `staged` appear at `path` in one step, failing with `AlreadyExists` when `path` is taken.
179/// Why:
180/// A hard link is atomic and exclusive. A filesystem that refuses hard links (`EPERM`, `ENOTSUP`, `EXDEV`) gets the
181/// rename after a check that nothing is at `path`, which is atomic and, with the writers of one record being
182/// one process, as exclusive as the callers need.
183///
184/// In TS you'd write (pseudocode):
185/// ```ts
186/// try { await link(staged, path); } catch (error) { if (error.code === 'EEXIST') throw error; await rename(staged, path); }
187/// ```
188fn publish_exclusive(staged: &Path, path: &Path) -> std::io::Result<()> {
189 match std::fs::hard_link(staged, path) {
190 Ok(()) => return Ok(()),
191 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => return Err(error),
192 Err(error) => {
193 debug(
194 "publishExclusive",
195 format!("the filesystem refused the link of {} ({error}); publishing by rename", path.display()).as_str(),
196 );
197 }
198 }
199 if std::fs::symlink_metadata(path).is_ok() {
200 return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists));
201 }
202 return std::fs::rename(staged, path);
203}
204
205/// What:
206/// Write `bytes` to a staging file beside `path`, sync it when `durable`, and publish it at `path`; any failure
207/// removes the staging file, so `path` holds all the bytes or does not exist.
208/// `mode` is applied to the file before it is published, when given.
209/// Why:
210/// A file created at its final name and then written is a record that a kill, or a full disk, between the two
211/// steps leaves empty or cut: recovery then stops every guarded command until a person removes it. Publishing a
212/// complete file by link makes the record appear whole or not at all, whether or not its bytes are synced; a record
213/// that the tearing table shows to be benign when a power loss tears it skips the sync.
214///
215/// In TS you'd write (pseudocode):
216/// ```ts
217/// const staged = stagingPath(path); try { await writeAndSync(staged, bytes); await publishExclusive(staged, path); } finally { await rm(staged, { force: true }); }
218/// ```
219fn write_then_publish(path: &Path, bytes: &[u8], mode: Option<u32>, durable: bool) -> std::io::Result<()> {
220 let staged: PathBuf = staging_path(path)?;
221 let mut file: File = exclusive_options().open(staged.as_path())?;
222 let filled: std::io::Result<()> = file.write_all(bytes).and_then(|()| {
223 if !durable {
224 return Ok(());
225 }
226 #[cfg(test)]
227 record_file_sync(path);
228 // `sync_all` flushes data and metadata, the incumbent's `handle.sync()`.
229 return file.sync_all();
230 });
231 drop(file);
232 let published: std::io::Result<()> = filled
233 .and_then(|()| {
234 return match mode {
235 Some(bits) => protect_path(staged.as_path(), bits),
236 None => Ok(()),
237 };
238 })
239 .and_then(|()| { return publish_exclusive(staged.as_path(), path); });
240 if let Some(message) = staging_removal_report(staged.as_path(), std::fs::remove_file(staged.as_path())) {
241 debug("writeThenPublish", message.as_str());
242 }
243 return published;
244}
245
246/// What:
247/// The diagnostic that the removal of a staging file calls for, or `None` when nothing needs saying.
248/// Why:
249/// The rename fallback already moved the staging file, so its absence is the expected end state, while any other
250/// failure leaves a stray `.partial` file that a person may want to know about; a function of the removal result
251/// lets a test see that choice, which no file on disk shows.
252///
253/// In TS you'd write (pseudocode):
254/// ```ts
255/// error === undefined || error.code === 'ENOENT' ? undefined : `removing the staging file ${staged} failed: ${error}`
256/// ```
257fn staging_removal_report(staged: &Path, removal: std::io::Result<()>) -> Option<String> {
258 return match removal {
259 Ok(()) => None,
260 // The rename fallback already moved it.
261 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
262 Err(error) => Some(format!("removing the staging file {} failed: {error}", staged.display())),
263 };
264}
265
266/// What:
267/// Write one private file exclusively,
268/// refusing links,
269/// then make its bytes durable.
270/// `&[u8]` borrows the exact bytes.
271/// Why:
272/// A journal state file is created once and never rewritten,
273/// so a crash leaves either
274/// no file or a complete one after the caller syncs the directory.
275/// The file is written under a staging name and published whole (`write_then_publish`), so that the promise
276/// also holds for a kill between the creation and the write and for a write cut short by a full disk.
277///
278/// In TS you'd write (pseudocode):
279/// ```ts
280/// async function writePrivateFile({ path, bytes }): Promise<void>;
281/// ```
282pub fn write_private_file(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
283 return write_then_publish(path, bytes, Some(PRIVATE_FILE_MODE), true);
284}
285
286/// What:
287/// Write one private file exclusively and publish it whole,
288/// without syncing its bytes.
289/// Why:
290/// A record that recovery completes or discards whatever a power loss left of it (the tearing table in
291/// `doc/planning/cli-git-spawn-and-sync-reduction.md`) gains nothing from a flush that makes every commit wait for the
292/// disk; a kill still leaves it whole or absent, because it is published by link as `write_private_file` does.
293///
294/// In TS you'd write (pseudocode):
295/// ```ts
296/// await writePrivateFile({ path, bytes, sync: false });
297/// ```
298pub fn write_private_file_unsynced(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
299 return write_then_publish(path, bytes, Some(PRIVATE_FILE_MODE), false);
300}
301
302/// What:
303/// Write one file exclusively with mode `0600`,
304/// refusing links,
305/// and sync its bytes,
306/// without the explicit mode change.
307/// Why:
308/// Owner-lock candidates are written this way by the incumbent's `writeCandidate`:
309/// the
310/// creation mode alone,
311/// then a sync before publication.
312/// The file is published whole, as `write_private_file` does.
313///
314/// In TS you'd write (pseudocode):
315/// ```ts
316/// const handle = await open(path, O_CREAT | O_EXCL | O_WRONLY | O_NOFOLLOW, 0o600); await handle.writeFile(bytes); await handle.sync();
317/// ```
318pub fn write_exclusive_synced(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
319 return write_then_publish(path, bytes, None, true);
320}
321
322/// What:
323/// Write one file exclusively with mode `0600` and publish it whole,
324/// without syncing its bytes.
325/// Why:
326/// The unsynced counterpart of `write_exclusive_synced`, for records whose torn state recovery treats as benign.
327///
328/// In TS you'd write (pseudocode):
329/// ```ts
330/// await writeExclusive({ path, bytes, sync: false });
331/// ```
332pub fn write_exclusive_unsynced(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
333 return write_then_publish(path, bytes, None, false);
334}
335
336/// What:
337/// Write one file exclusively with mode `0600`,
338/// refusing links,
339/// without syncing it.
340/// Why:
341/// Scratch bytes that are unlinked right after they are opened for reading are never recovered,
342/// so a sync would only make every caller wait for the disk;
343/// records that a crash must find stay on `write_exclusive_synced`.
344///
345/// In TS you'd write (pseudocode):
346/// ```ts
347/// const handle = await open(path, O_CREAT | O_EXCL | O_WRONLY | O_NOFOLLOW, 0o600); await handle.writeFile(bytes);
348/// ```
349pub fn write_exclusive_scratch(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
350 let mut file: File = exclusive_options().open(path)?;
351 return file.write_all(bytes);
352}
353
354/// What:
355/// Create one directory with mode `0700`;
356/// its parent must exist.
357/// Why:
358/// Lock candidates,
359/// transaction directories and stores are private to the account.
360/// Only the creation mode is applied,
361/// as the incumbent's `mkdir(path, { mode })`.
362///
363/// In TS you'd write (pseudocode):
364/// ```ts
365/// await mkdir(path, { mode: 0o700 });
366/// ```
367pub fn create_private_directory(path: &Path) -> std::io::Result<()> {
368 return private_directory_builder().create(path);
369}
370
371/// What:
372/// Create a directory and every missing parent with mode `0700`;
373/// an existing directory is accepted.
374/// Why:
375/// The per-user state directory may not exist yet,
376/// and the XDG rules ask for `0700` when a program creates one.
377/// Only the directories this call creates get the mode;
378/// existing parents keep theirs.
379///
380/// In TS you'd write (pseudocode):
381/// ```ts
382/// await mkdir(path, { recursive: true, mode: 0o700 });
383/// ```
384pub fn create_private_directory_all(path: &Path) -> std::io::Result<()> {
385 // `mut` allows the builder to be switched to recursive creation.
386 let mut builder: std::fs::DirBuilder = private_directory_builder();
387 builder.recursive(true);
388 return builder.create(path);
389}
390
391/// What:
392/// The directory builder that creates private directories on this platform.
393/// Why:
394/// Only Unix configures a mode;
395/// a separate function per platform keeps each body free of
396/// a `mut` the other platform would leave unused.
397///
398/// In TS you'd write (pseudocode):
399/// ```ts
400/// const builder = { mode: 0o700 };
401/// ```
402#[cfg(unix)]
403fn private_directory_builder() -> std::fs::DirBuilder {
404 // The trait adds `.mode` to directory builders on Unix.
405 /// The Unix directory-builder extensions,
406 /// for the mode.
407 use std::os::unix::fs::DirBuilderExt;
408 // `mut` allows the builder to be configured before it is returned.
409 let mut builder: std::fs::DirBuilder = std::fs::DirBuilder::new();
410 builder.mode(PRIVATE_DIRECTORY_MODE);
411 return builder;
412}
413
414/// What:
415/// The directory builder that creates private directories on this platform.
416/// Why:
417/// Windows has no mode to configure,
418/// as `private_directory_builder` on Unix explains.
419///
420/// In TS you'd write (pseudocode):
421/// ```ts
422/// const builder = {};
423/// ```
424// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
425#[cfg(not(unix))]
426fn private_directory_builder() -> std::fs::DirBuilder {
427 return std::fs::DirBuilder::new();
428}
429
430/// What:
431/// Whether a failed directory sync means the filesystem has no such operation, not that the flush failed.
432/// Why:
433/// `EINVAL`, `ENOTSUP` and `ENOSYS` are what a filesystem without the operation returns; `EIO`, `ENOSPC` and
434/// `EDQUOT` mean the flush itself failed and stay errors.
435///
436/// In TS you'd write (pseudocode):
437/// ```ts
438/// ['EINVAL', 'ENOTSUP', 'ENOSYS'].includes(error.code)
439/// ```
440#[cfg(unix)]
441fn directory_sync_is_unsupported(error: &std::io::Error) -> bool {
442 return matches!(error.raw_os_error(), Some(libc::EINVAL | libc::ENOTSUP | libc::ENOSYS));
443}
444
445/// What:
446/// The outcome of syncing the directory at `path`: a filesystem without the operation counts as synced, any other
447/// failure stays an error.
448/// Why:
449/// A filesystem with no directory flush (procfs, some network and userspace filesystems) answers with one of the
450/// unsupported codes; the entry cannot be made durable there by any means, and failing would stop every commit. A
451/// function of the sync result lets a test feed the flush failures (`EIO`, `ENOSPC`, `EDQUOT`) that no
452/// unprivileged filesystem produces.
453///
454/// In TS you'd write (pseudocode):
455/// ```ts
456/// if (error !== undefined && !['EINVAL', 'ENOTSUP', 'ENOSYS'].includes(error.code)) throw error;
457/// ```
458#[cfg(unix)]
459fn settle_directory_sync(path: &Path, outcome: std::io::Result<()>) -> std::io::Result<()> {
460 return match outcome {
461 Ok(()) => Ok(()),
462 Err(error) if directory_sync_is_unsupported(&error) => {
463 debug(
464 "syncDirectory",
465 format!("the filesystem of {} cannot sync a directory ({error}); skipped", path.display()).as_str(),
466 );
467 Ok(())
468 }
469 Err(error) => Err(error),
470 };
471}
472
473/// What:
474/// Make a directory's entries durable,
475/// where the platform supports directory handles.
476/// Why:
477/// A new or renamed entry survives a crash only after its directory is synced;
478/// Windows
479/// has no directory handle for this,
480/// as the incumbent's `syncDirectory` notes.
481///
482/// In TS you'd write (pseudocode):
483/// ```ts
484/// async function syncDirectory(path: string): Promise<void>;
485/// ```
486pub fn sync_directory(path: &Path) -> std::io::Result<()> {
487 #[cfg(test)]
488 record_directory_sync(path);
489 #[cfg(unix)]
490 {
491 // Opening a directory read-only and syncing it is the POSIX way to flush its entries.
492 let directory: File = File::open(path)?;
493 settle_directory_sync(path, directory.sync_all())?;
494 }
495 // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
496 #[cfg(not(unix))]
497 let _ = path;
498 return Ok(());
499}
500
501#[cfg(test)]
502thread_local! {
503 /// Paths of the files this thread asked `write_then_publish` to sync, in call order.
504 static FILE_SYNCS: std::cell::RefCell<Vec<std::path::PathBuf>> = const { std::cell::RefCell::new(Vec::new()) };
505}
506
507/// What:
508/// Remember that this thread synced a file it published at `path`.
509/// Why:
510/// Unit tests pin which records keep their file sync, which no file on disk shows.
511///
512/// In TS you'd write (pseudocode):
513/// ```ts
514/// syncedFiles.push(path);
515/// ```
516#[cfg(test)]
517fn record_file_sync(path: &Path) {
518 FILE_SYNCS.with(|syncs| {
519 syncs.borrow_mut().push(path.to_path_buf());
520 });
521}
522
523/// What:
524/// Take and clear the files this thread has synced so far.
525/// Why:
526/// A test calls it before the step under test to drop earlier syncs, and after it to read exactly that step's syncs.
527///
528/// In TS you'd write (pseudocode):
529/// ```ts
530/// const synced = syncedFiles.splice(0);
531/// ```
532#[cfg(test)]
533pub fn take_file_syncs() -> Vec<std::path::PathBuf> {
534 return FILE_SYNCS.with(|syncs| {
535 return std::mem::take(&mut *syncs.borrow_mut());
536 });
537}
538
539#[cfg(test)]
540thread_local! {
541 /// Canonical paths of the directories this thread asked `sync_directory` to flush, in call order.
542 static DIRECTORY_SYNCS: std::cell::RefCell<Vec<std::path::PathBuf>> = const { std::cell::RefCell::new(Vec::new()) };
543}
544
545/// What:
546/// Remember that this thread asked for a directory sync.
547/// Why:
548/// Unit tests prove that a helper syncs a directory exactly when it changed one, which no file on disk shows;
549/// the list is per thread, so parallel tests never see each other's syncs.
550///
551/// In TS you'd write (pseudocode):
552/// ```ts
553/// syncedDirectories.push(await realpath(path));
554/// ```
555#[cfg(test)]
556fn record_directory_sync(path: &Path) {
557 let canonical: std::path::PathBuf = std::fs::canonicalize(path).unwrap_or_else(|_| return path.to_path_buf());
558 DIRECTORY_SYNCS.with(|syncs| {
559 syncs.borrow_mut().push(canonical);
560 });
561}
562
563/// What:
564/// Take and clear the directories this thread has synced so far.
565/// Why:
566/// A test calls it before the step under test to drop earlier syncs, and after it to read exactly that step's syncs.
567///
568/// In TS you'd write (pseudocode):
569/// ```ts
570/// const synced = syncedDirectories.splice(0);
571/// ```
572#[cfg(test)]
573pub fn take_directory_syncs() -> Vec<std::path::PathBuf> {
574 return DIRECTORY_SYNCS.with(|syncs| {
575 return std::mem::take(&mut *syncs.borrow_mut());
576 });
577}
578
579/// What:
580/// Remove a directory tree,
581/// treating an already absent tree as removed.
582/// Why:
583/// Retired locks and transactions are removed by name after a rename;
584/// another process
585/// may have removed them first,
586/// which is the same end state (`rm -rf` with `force`).
587///
588/// In TS you'd write (pseudocode):
589/// ```ts
590/// await rm(path, { recursive: true, force: true });
591/// ```
592pub fn remove_tree(path: &Path) -> std::io::Result<()> {
593 match std::fs::remove_dir_all(path) {
594 Ok(()) => return Ok(()),
595 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
596 Err(error) => return Err(error),
597 }
598}
599
600/// What:
601/// Open a file for reading without following a final link.
602/// Why:
603/// Records are always files the wrappers created;
604/// a link planted at a record path must
605/// be refused,
606/// never read through.
607///
608/// In TS you'd write (pseudocode):
609/// ```ts
610/// await open(path, O_RDONLY | O_NOFOLLOW);
611/// ```
612fn open_no_follow(path: &Path) -> std::io::Result<File> {
613 // `mut` allows the builder to be configured step by step.
614 let mut options: OpenOptions = OpenOptions::new();
615 options.read(true);
616 #[cfg(unix)]
617 {
618 // The trait adds `.custom_flags` to open options on Unix.
619 /// The Unix open-option extensions,
620 /// for the no-follow flag.
621 use std::os::unix::fs::OpenOptionsExt;
622 options.custom_flags(libc::O_NOFOLLOW);
623 }
624 return options.open(path);
625}
626
627/// What:
628/// Read a whole regular file without following links,
629/// up to `limit` bytes.
630/// `u64` is an unsigned 64-bit size;
631/// `Result<Vec<u8>, ReadRefusal>` is "the bytes,
632/// or why
633/// not".
634/// Why:
635/// Recovery reads untrusted state:
636/// a link or a directory must be named as unsafe,
637/// and a
638/// runaway file must not be read into memory.
639///
640/// In TS you'd write (pseudocode):
641/// ```ts
642/// async function readPrivateFile(path: string, limit: number): Promise<Uint8Array>; // throws ReadRefusal
643/// ```
644pub fn read_regular_file(path: &Path, limit: u64) -> Result<Vec<u8>, ReadRefusal> {
645 let file: File = match open_no_follow(path) {
646 Ok(opened) => opened,
647 Err(error) => return Err(open_refusal(error)),
648 };
649 // The handle's own metadata, so a swap between the check and the read is impossible.
650 let metadata: std::fs::Metadata = file.metadata().map_err(ReadRefusal::Io)?;
651 if !metadata.is_file() {
652 return Err(ReadRefusal::NotRegular);
653 }
654 if metadata.len() > limit {
655 return Err(ReadRefusal::TooLarge);
656 }
657 // `.take(limit + 1)` caps the read even if the file grows after the size check.
658 let mut bytes: Vec<u8> = Vec::new();
659 file.take(limit + 1)
660 .read_to_end(&mut bytes)
661 .map_err(ReadRefusal::Io)?;
662 if bytes.len() as u64 > limit {
663 return Err(ReadRefusal::TooLarge);
664 }
665 return Ok(bytes);
666}
667
668/// What:
669/// Classify an open failure as missing,
670/// a refused link,
671/// or another error.
672/// Why:
673/// `O_NOFOLLOW` on a link fails with `ELOOP` on Linux and macOS;
674/// that is the unsafe-link
675/// answer,
676/// not an input-output failure.
677/// A path through a non-directory (`ENOTDIR`) names
678/// no file,
679/// which the incumbent treats like a missing one.
680///
681/// In TS you'd write (pseudocode):
682/// ```ts
683/// function openRefusal(error: NodeJS.ErrnoException): ReadRefusal;
684/// ```
685fn open_refusal(error: std::io::Error) -> ReadRefusal {
686 if error.kind() == std::io::ErrorKind::NotFound {
687 return ReadRefusal::Missing;
688 }
689 #[cfg(unix)]
690 {
691 if error.raw_os_error() == Some(libc::ENOTDIR) {
692 return ReadRefusal::Missing;
693 }
694 if error.raw_os_error() == Some(libc::ELOOP) {
695 return ReadRefusal::NotRegular;
696 }
697 }
698 return ReadRefusal::Io(error);
699}
700
701/// Disposable-directory controls stay out of the release executable.
702#[cfg(test)]
703#[path = "private_storage_tests.rs"]
704mod tests;
705
706/// Publication controls stay out of the release executable.
707#[cfg(test)]
708#[path = "private_storage_publish_tests.rs"]
709mod publish_tests;