use std::io::Read;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::fs::File;
use color_eyre::eyre::{Report, eyre};
use super::{
install::{
ARCHIVE_DECOMPRESSED_CAP,
ENTRY_DECOMPRESSED_CAP,
EntryPathExt,
PlannedFile,
install_failed,
invalid,
open_archive,
},
layout::classify_entry_path,
tree::{Destination, InstallTree},
};
use crate::error::BootstrapResult;
pub(super) fn write_all(
path: &Utf8Path,
bytes: &[u8],
planned: &[PlannedFile],
install_dir: &Utf8Path,
) -> BootstrapResult<()> {
let owner = tree_owner(install_dir)?;
let context = WriteContext {
tree: InstallTree::open(install_dir)?,
owner,
};
let mut written: Vec<Utf8PathBuf> = Vec::new();
let mut budget = ARCHIVE_DECOMPRESSED_CAP;
let planned_by_path: std::collections::BTreeMap<&Utf8Path, &PlannedFile> = planned
.iter()
.map(|file| (file.relative.as_path(), file))
.collect();
let mut reader = open_archive(bytes);
for entry_result in reader.entries()? {
let mut entry =
entry_result.map_err(|err| invalid(path, &format!("unreadable entry: {err}")))?;
let Some(file) = classify_entry_path(&entry.path_bytes_lossy_path()) else {
continue;
};
let Some(plan) = planned_by_path.get(file.as_path()).copied() else {
continue;
};
let contents = read_entry_bounded(path, &mut entry, &plan.relative, &mut budget)
.map_err(|err| over_cap_after_writing(&plan.relative, &written, err))?;
write_file(&context, plan, &contents)
.map_err(|err| install_failed(&plan.relative, &written, err))?;
written.push(plan.relative.clone());
}
Ok(())
}
fn over_cap_after_writing(
relative: &Utf8Path,
written: &[Utf8PathBuf],
err: crate::error::BootstrapError,
) -> crate::error::BootstrapError {
if written.is_empty() {
return err;
}
let kind = err.kind();
crate::error::BootstrapError::new(
kind,
err.into_report().wrap_err(format!(
"failed to install {relative}; files already written: {written:?}"
)),
)
}
fn read_entry_bounded(
path: &Utf8Path,
entry: &mut impl Read,
relative: &Utf8Path,
budget: &mut u64,
) -> BootstrapResult<Vec<u8>> {
let limit = ENTRY_DECOMPRESSED_CAP.min(*budget);
let mut contents = Vec::new();
entry
.take(limit.saturating_add(1))
.read_to_end(&mut contents)
.map_err(|err| invalid(path, &format!("cannot read {relative}: {err}")))?;
let read = contents.len() as u64;
if read > limit {
return Err(invalid(
path,
&format!(
"{relative} decompresses past the limit; each file is capped at \
{ENTRY_DECOMPRESSED_CAP} bytes and the archive at {ARCHIVE_DECOMPRESSED_CAP}"
),
));
}
*budget -= read;
Ok(contents)
}
struct WriteContext {
tree: InstallTree,
owner: Owner,
}
fn write_file(context: &WriteContext, plan: &PlannedFile, bytes: &[u8]) -> Result<(), Report> {
let tree = &context.tree;
tree.require_real_parents(&plan.relative)?;
let destination = tree.path_of(&plan.relative);
match tree.inspect_destination(&plan.relative, bytes) {
Destination::Identical(existing) => repair_existing(context, plan, &destination, &existing),
other => {
log_rewrite(&destination, &other);
write_afresh(context, plan, bytes)
}
}
}
fn log_rewrite(destination: &Utf8Path, found: &Destination) {
if let Some(reason) = found.rewrite_reason() {
tracing::debug!(%destination, reason, "writing the file afresh");
}
}
fn repair_existing(
context: &WriteContext,
plan: &PlannedFile,
destination: &Utf8Path,
existing: &File,
) -> Result<(), Report> {
set_mode(existing, destination, plan.mode)?;
apply_owner(existing, destination, context.owner)
}
fn write_afresh(context: &WriteContext, plan: &PlannedFile, bytes: &[u8]) -> Result<(), Report> {
let tree = &context.tree;
tree.create_parents(&plan.relative)?;
let (temp, temp_relative) = tree.create_temp_beside(&plan.relative)?;
let placed = fill_temp(context, plan, bytes, &temp)
.and_then(|()| tree.place(&temp_relative, &plan.relative));
if placed.is_err() {
tree.discard_temp(&temp_relative);
}
placed
}
fn fill_temp(
context: &WriteContext,
plan: &PlannedFile,
bytes: &[u8],
temp: &File,
) -> Result<(), Report> {
let destination = context.tree.path_of(&plan.relative);
let mut handle = temp;
std::io::Write::write_all(&mut handle, bytes)
.map_err(|err| eyre!("cannot write {destination}: {err}"))?;
set_mode(temp, &destination, plan.mode)?;
apply_owner(temp, &destination, context.owner)
}
#[cfg(unix)]
fn set_mode(file: &File, destination: &Utf8Path, mode: u32) -> Result<(), Report> {
use cap_std::fs::{Permissions, PermissionsExt};
file.set_permissions(Permissions::from_mode(mode))
.map_err(|err| eyre!("cannot set mode {mode:o} on {destination}: {err}"))
}
#[cfg(not(unix))]
fn set_mode(_file: &File, _destination: &Utf8Path, _mode: u32) -> Result<(), Report> { Ok(()) }
#[derive(Debug, Clone, Copy)]
struct Owner {
#[cfg(unix)]
uid: u32,
#[cfg(unix)]
gid: u32,
}
#[cfg(unix)]
fn tree_owner(install_dir: &Utf8Path) -> BootstrapResult<Owner> {
use std::{fs, os::unix::fs::MetadataExt};
use crate::{error::BootstrapErrorKind, extensions::extension_error};
let metadata = fs::metadata(install_dir).map_err(|err| {
extension_error(
BootstrapErrorKind::ExtensionInstallFailed,
eyre!("cannot stat installation directory {install_dir}: {err}"),
)
})?;
Ok(Owner {
uid: metadata.uid(),
gid: metadata.gid(),
})
}
#[cfg(not(unix))]
fn tree_owner(_install_dir: &Utf8Path) -> BootstrapResult<Owner> { Ok(Owner {}) }
#[cfg(unix)]
fn apply_owner(file: &File, destination: &Utf8Path, owner: Owner) -> Result<(), Report> {
use cap_std::fs::MetadataExt;
let metadata = file
.metadata()
.map_err(|err| eyre!("cannot stat {destination}: {err}"))?;
if metadata.uid() == owner.uid && metadata.gid() == owner.gid {
return Ok(());
}
std::os::unix::fs::fchown(file, Some(owner.uid), Some(owner.gid)).map_err(|err| {
eyre!(
"cannot chown {destination} to {}:{}: {err}",
owner.uid,
owner.gid
)
})
}
#[cfg(not(unix))]
fn apply_owner(_file: &File, _destination: &Utf8Path, _owner: Owner) -> Result<(), Report> {
Ok(())
}