use camino::{Utf8Path, Utf8PathBuf};
use cap_std::{
ambient_authority,
fs::{Dir, File, OpenOptions},
};
use color_eyre::eyre::{Report, eyre};
use super::Sha256Hex;
use crate::{
error::{BootstrapErrorKind, BootstrapResult},
extensions::extension_error,
};
pub(super) enum Destination {
Identical(File),
Absent,
NotRegular,
Different,
Unreadable(std::io::Error),
}
impl Destination {
pub(super) fn rewrite_reason(&self) -> Option<String> {
match self {
Self::Identical(_) => None,
Self::Absent => Some("nothing is there".to_owned()),
Self::NotRegular => Some("what is there is not a regular file".to_owned()),
Self::Different => Some("what is there holds different bytes".to_owned()),
Self::Unreadable(err) => Some(format!("what is there cannot be read: {err}")),
}
}
}
fn require_regular(file: File) -> Result<File, Destination> {
match file.metadata() {
Ok(metadata) if metadata.is_file() => Ok(file),
Ok(_) => Err(Destination::NotRegular),
Err(err) => Err(Destination::Unreadable(err)),
}
}
fn compare_digest(file: File, bytes: &[u8]) -> Destination {
match Sha256Hex::of_reader(&file) {
Ok(digest) if digest == Sha256Hex::of_bytes(bytes) => Destination::Identical(file),
Ok(_) => Destination::Different,
Err(err) => Destination::Unreadable(err),
}
}
pub(super) struct InstallTree {
dir: Dir,
root: Utf8PathBuf,
}
impl InstallTree {
pub(super) fn open(root: &Utf8Path) -> BootstrapResult<Self> {
let failed = |report| extension_error(BootstrapErrorKind::ExtensionInstallFailed, report);
match std::fs::symlink_metadata(root) {
Ok(metadata) if metadata.is_dir() => {}
Ok(metadata) => {
return Err(failed(eyre!(
"{root} is a {:?}, not a directory; the installation tree must not route \
through a symlink",
metadata.file_type()
)));
}
Err(err) => return Err(failed(eyre!("cannot inspect {root}: {err}"))),
}
let dir = Dir::open_ambient_dir(root.as_std_path(), ambient_authority())
.map_err(|err| failed(eyre!("cannot open installation tree {root}: {err}")))?;
Ok(Self {
dir,
root: root.to_path_buf(),
})
}
pub(super) fn path_of(&self, relative: &Utf8Path) -> Utf8PathBuf { self.root.join(relative) }
pub(super) fn require_real_parents(&self, relative: &Utf8Path) -> Result<(), Report> {
let components: Vec<&str> = relative
.components()
.map(|component| component.as_str())
.collect();
let parents = components.len().saturating_sub(1);
let mut current = Utf8PathBuf::new();
for component in components.into_iter().take(parents) {
current.push(component);
match self.dir.symlink_metadata(current.as_std_path()) {
Ok(metadata) if metadata.is_dir() => {}
Ok(metadata) => {
return Err(eyre!(
"{} is a {:?}, not a directory; the installation tree must not route \
through a symlink",
self.path_of(¤t),
metadata.file_type()
));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => break,
Err(err) => {
return Err(eyre!("cannot inspect {}: {err}", self.path_of(¤t)));
}
}
}
Ok(())
}
pub(super) fn create_parents(&self, relative: &Utf8Path) -> Result<(), Report> {
let Some(parent) = relative
.parent()
.filter(|parent| !parent.as_str().is_empty())
else {
return Ok(());
};
self.dir
.create_dir_all(parent.as_std_path())
.map_err(|err| eyre!("cannot create {}: {err}", self.path_of(parent)))
}
pub(super) fn inspect_destination(&self, relative: &Utf8Path, bytes: &[u8]) -> Destination {
match self.open_destination(relative).and_then(require_regular) {
Ok(file) => compare_digest(file, bytes),
Err(found) => found,
}
}
fn open_destination(&self, relative: &Utf8Path) -> Result<File, Destination> {
match self
.dir
.open_with(relative.as_std_path(), &read_no_follow())
{
Ok(file) => Ok(file),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Err(Destination::Absent),
Err(err) => Err(Destination::Unreadable(err)),
}
}
pub(super) fn create_temp_beside(
&self,
relative: &Utf8Path,
) -> Result<(File, Utf8PathBuf), Report> {
let parent = relative
.parent()
.filter(|parent| !parent.as_str().is_empty());
let mut last: Option<std::io::Error> = None;
for _ in 0..TEMP_NAME_ATTEMPTS {
let name = temp_name();
let candidate = parent.map_or_else(|| Utf8PathBuf::from(&name), |dir| dir.join(&name));
match self
.dir
.open_with(candidate.as_std_path(), &create_new_write())
{
Ok(file) => return Ok((file, candidate)),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => last = Some(err),
Err(err) => {
return Err(eyre!(
"cannot create temporary file in {}: {err}",
self.path_of(parent.unwrap_or_else(|| Utf8Path::new("")))
));
}
}
}
Err(eyre!(
"cannot create a temporary file in {} after {TEMP_NAME_ATTEMPTS} attempts: {}",
self.path_of(parent.unwrap_or_else(|| Utf8Path::new(""))),
last.map_or_else(|| "no error recorded".to_owned(), |err| err.to_string())
))
}
pub(super) fn place(&self, temp: &Utf8Path, relative: &Utf8Path) -> Result<(), Report> {
self.dir
.rename(temp.as_std_path(), &self.dir, relative.as_std_path())
.map_err(|err| {
eyre!(
"cannot move file into place at {}: {err}",
self.path_of(relative)
)
})
}
pub(super) fn discard_temp(&self, temp: &Utf8Path) {
drop(self.dir.remove_file(temp.as_std_path()));
}
}
const TEMP_NAME_ATTEMPTS: u8 = 16;
fn temp_name() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos: u32 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |since| since.subsec_nanos());
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
format!(".pg-embed-{}-{nanos}-{count}.tmp", std::process::id())
}
fn read_no_follow() -> OpenOptions {
let mut options = OpenOptions::new();
options.read(true);
no_follow(&mut options);
options
}
fn create_new_write() -> OpenOptions {
let mut options = OpenOptions::new();
options.write(true).create_new(true);
no_follow(&mut options);
options
}
#[cfg(unix)]
fn no_follow(options: &mut OpenOptions) {
use cap_std::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NOFOLLOW);
}
#[cfg(not(unix))]
fn no_follow(_options: &mut OpenOptions) {}