Skip to main content

ic_host_fs/read/private/
mod.rs

1//! Fixed-size private reads with explicit admission failures.
2
3use super::no_follow::open_optional_no_follow;
4use ic_host_artifacts::artifact::ArtifactError;
5use std::{
6    fs::File,
7    io::{self, Read},
8    os::unix::fs::MetadataExt as _,
9    path::Path,
10};
11
12#[cfg(test)]
13mod tests;
14
15/// A private-file read failed without returning partial bytes.
16#[derive(Debug, thiserror::Error)]
17pub enum PrivateFileReadError {
18    /// Opening, file admission, metadata or reading failed.
19    #[error("private file read failed")]
20    Read(#[source] ArtifactError),
21    /// The opened descriptor does not have exactly owner read/write permissions.
22    #[error("private file permissions are {mode:o}, expected 600")]
23    Permissions {
24        /// Observed Unix permission bits, masked to `0o777`.
25        mode: u32,
26    },
27    /// The opened file does not have exactly one directory link.
28    #[error("private file has {actual} links, expected one")]
29    LinkCount {
30        /// Observed number of links to the inode.
31        actual: u64,
32    },
33    /// Metadata or a trailing byte proves the file is not the required length.
34    #[error("private file must contain exactly {expected} bytes")]
35    Length {
36        /// Required complete byte length.
37        expected: usize,
38    },
39}
40
41/// Read exactly `N` private bytes through a no-follow regular descriptor.
42///
43/// Only an initially missing pathname returns `Ok(None)`. The opened descriptor
44/// must have permission bits `0o600`, one link and length `N`; a trailing read
45/// also rejects growth. I/O and admission failures remain distinct from absence.
46/// No file is created, repaired, removed or replaced. Consumers own whether an
47/// error disables an optional feature or stops an operation; it never means a
48/// key can safely be regenerated. Contents are not included in error messages.
49///
50/// Ancestors must be trusted. Descriptor selection does not freeze contents or
51/// permissions; concurrent writers and ownership policy remain caller-owned.
52/// `N` is caller-selected and allocates a fixed array on the stack.
53///
54/// # Errors
55/// Returns typed open/read failures, permission/link rejection or invalid length.
56pub fn read_private_bytes<const N: usize>(
57    path: &Path,
58) -> Result<Option<[u8; N]>, PrivateFileReadError> {
59    open_optional_no_follow(path)
60        .map_err(PrivateFileReadError::Read)?
61        .map(read_private_descriptor)
62        .transpose()
63}
64
65fn read_private_descriptor<const N: usize>(
66    mut file: File,
67) -> Result<[u8; N], PrivateFileReadError> {
68    let io_error = |source| PrivateFileReadError::Read(ArtifactError::Io(source));
69    let metadata = file.metadata().map_err(io_error)?;
70    if !metadata.is_file() {
71        return Err(PrivateFileReadError::Read(ArtifactError::NotRegularFile));
72    }
73    if metadata.len() != N as u64 {
74        return Err(PrivateFileReadError::Length { expected: N });
75    }
76    let mode = metadata.mode() & 0o777;
77    if mode != 0o600 {
78        return Err(PrivateFileReadError::Permissions { mode });
79    }
80    if metadata.nlink() != 1 {
81        return Err(PrivateFileReadError::LinkCount {
82            actual: metadata.nlink(),
83        });
84    }
85    read_exact_private(&mut file)
86}
87
88fn read_exact_private<const N: usize>(
89    reader: &mut impl Read,
90) -> Result<[u8; N], PrivateFileReadError> {
91    let io_error = |source| PrivateFileReadError::Read(ArtifactError::Io(source));
92    let mut bytes = [0; N];
93    reader.read_exact(&mut bytes).map_err(io_error)?;
94    let mut extra = [0];
95    loop {
96        match reader.read(&mut extra) {
97            Ok(0) => return Ok(bytes),
98            Ok(_) => return Err(PrivateFileReadError::Length { expected: N }),
99            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
100            Err(error) => return Err(io_error(error)),
101        }
102    }
103}