use super::no_follow::open_optional_no_follow;
use ic_host_artifacts::artifact::ArtifactError;
use std::{
fs::File,
io::{self, Read},
os::unix::fs::MetadataExt as _,
path::Path,
};
#[cfg(test)]
mod tests;
#[derive(Debug, thiserror::Error)]
pub enum PrivateFileReadError {
#[error("private file read failed")]
Read(#[source] ArtifactError),
#[error("private file permissions are {mode:o}, expected 600")]
Permissions {
mode: u32,
},
#[error("private file has {actual} links, expected one")]
LinkCount {
actual: u64,
},
#[error("private file must contain exactly {expected} bytes")]
Length {
expected: usize,
},
}
pub fn read_private_bytes<const N: usize>(
path: &Path,
) -> Result<Option<[u8; N]>, PrivateFileReadError> {
open_optional_no_follow(path)
.map_err(PrivateFileReadError::Read)?
.map(read_private_descriptor)
.transpose()
}
fn read_private_descriptor<const N: usize>(
mut file: File,
) -> Result<[u8; N], PrivateFileReadError> {
let io_error = |source| PrivateFileReadError::Read(ArtifactError::Io(source));
let metadata = file.metadata().map_err(io_error)?;
if !metadata.is_file() {
return Err(PrivateFileReadError::Read(ArtifactError::NotRegularFile));
}
if metadata.len() != N as u64 {
return Err(PrivateFileReadError::Length { expected: N });
}
let mode = metadata.mode() & 0o777;
if mode != 0o600 {
return Err(PrivateFileReadError::Permissions { mode });
}
if metadata.nlink() != 1 {
return Err(PrivateFileReadError::LinkCount {
actual: metadata.nlink(),
});
}
read_exact_private(&mut file)
}
fn read_exact_private<const N: usize>(
reader: &mut impl Read,
) -> Result<[u8; N], PrivateFileReadError> {
let io_error = |source| PrivateFileReadError::Read(ArtifactError::Io(source));
let mut bytes = [0; N];
reader.read_exact(&mut bytes).map_err(io_error)?;
let mut extra = [0];
loop {
match reader.read(&mut extra) {
Ok(0) => return Ok(bytes),
Ok(_) => return Err(PrivateFileReadError::Length { expected: N }),
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) => return Err(io_error(error)),
}
}
}