use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, bail};
use tempfile::NamedTempFile;
use super::release::ArchiveKind;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Ownership {
Replaceable,
CargoBin,
NotWritable(String),
}
pub(crate) fn classify(
exe: &Path,
cargo_bins: &[PathBuf],
probe_writable: impl FnOnce(&Path) -> std::io::Result<()>,
) -> Ownership {
if cargo_bins.iter().any(|dir| exe.starts_with(dir)) {
return Ownership::CargoBin;
}
let dir = exe.parent().unwrap_or(exe);
match probe_writable(dir) {
Ok(()) => Ownership::Replaceable,
Err(e) => Ownership::NotWritable(e.to_string()),
}
}
pub(crate) fn cargo_bin_dirs(
home: &Path,
cargo_home: Option<&Path>,
install_root: Option<&Path>,
) -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
for root in [
Some(home.join(".cargo")),
cargo_home.map(Path::to_path_buf),
install_root.map(Path::to_path_buf),
]
.into_iter()
.flatten()
{
let dir = root.join("bin");
let dir = dir.canonicalize().unwrap_or(dir);
if !dirs.contains(&dir) {
dirs.push(dir);
}
}
dirs
}
pub(crate) fn running_exe() -> anyhow::Result<PathBuf> {
let exe = std::env::current_exe().context("locate the running executable")?;
exe.canonicalize()
.with_context(|| format!("resolve {}", exe.display()))
}
pub(crate) fn probe_writable(dir: &Path) -> std::io::Result<()> {
tempfile::Builder::new()
.prefix(".decdn-update-probe-")
.tempfile_in(dir)
.map(drop)
.map_err(|e| match e.raw_os_error() {
Some(code) => std::io::Error::from_raw_os_error(code),
None => std::io::Error::from(e.kind()),
})
}
pub(crate) fn extract_binary(
archive: &[u8],
kind: ArchiveKind,
max_bytes: u64,
) -> anyhow::Result<Vec<u8>> {
let bytes = match kind {
ArchiveKind::TarGz => extract_tar_gz(archive, max_bytes)?,
ArchiveKind::Zip => extract_zip(archive, max_bytes)?,
};
if bytes.is_empty() {
bail!("`{}` in the archive is empty", kind.binary_name());
}
Ok(bytes)
}
fn extract_tar_gz(archive: &[u8], max_bytes: u64) -> anyhow::Result<Vec<u8>> {
let bin = ArchiveKind::TarGz.binary_name();
let gz = flate2::read::GzDecoder::new(archive);
let mut tar = tar::Archive::new(gz);
for entry in tar.entries().context("read the release archive")? {
let entry = entry.context("read the release archive")?;
let path = entry.path().context("read an archive entry name")?;
let name = path.to_string_lossy();
if name.strip_prefix("./").unwrap_or(&name) != bin {
continue;
}
if !entry.header().entry_type().is_file() {
bail!("`{bin}` in the archive is not a regular file");
}
let size = entry.size();
return read_binary(entry, size, bin, max_bytes);
}
bail!("the release archive holds no `{bin}` entry at its root")
}
#[cfg(any(windows, test))]
fn extract_zip(archive: &[u8], max_bytes: u64) -> anyhow::Result<Vec<u8>> {
let bin = ArchiveKind::Zip.binary_name();
let mut zip =
zip::ZipArchive::new(std::io::Cursor::new(archive)).context("read the release archive")?;
let entry = match zip.by_name(bin) {
Ok(entry) => entry,
Err(zip::result::ZipError::FileNotFound) => {
bail!("the release archive holds no `{bin}` entry at its root")
}
Err(e) => return Err(e).context("read the release archive"),
};
if !entry.is_file() {
bail!("`{bin}` in the archive is not a regular file");
}
let size = entry.size();
read_binary(entry, size, bin, max_bytes)
}
fn read_binary(
mut entry: impl Read,
size: u64,
bin: &str,
max_bytes: u64,
) -> anyhow::Result<Vec<u8>> {
if size > max_bytes {
bail!("`{bin}` in the archive is {size} bytes, larger than the {max_bytes}-byte limit");
}
let mut bytes = Vec::new();
entry
.read_to_end(&mut bytes)
.context("read the binary out of the archive")?;
Ok(bytes)
}
#[cfg(not(any(windows, test)))]
fn extract_zip(_archive: &[u8], _max_bytes: u64) -> anyhow::Result<Vec<u8>> {
bail!("zip release archives are for Windows builds only")
}
pub(crate) fn stage(dir: &Path, bytes: &[u8]) -> anyhow::Result<NamedTempFile> {
let mut staged = tempfile::Builder::new()
.prefix(".decdn-update-")
.tempfile_in(dir)
.with_context(|| format!("create a temporary file in {}", dir.display()))?;
staged
.write_all(bytes)
.and_then(|()| staged.as_file().sync_all())
.with_context(|| format!("write {}", staged.path().display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(staged.path(), std::fs::Permissions::from_mode(0o755))
.with_context(|| format!("mark {} executable", staged.path().display()))?;
}
Ok(staged)
}
pub(crate) fn swap(staged: NamedTempFile, exe: &Path) -> anyhow::Result<()> {
match swap_inner(&staged, exe) {
Ok(()) => {
drop(staged);
Ok(())
}
Err(e) => {
let staged_path = staged.path().to_path_buf();
let kept = staged.keep().map_or(staged_path, |(_, path)| path);
Err(e.context(format!(
"the verified new binary is kept at {}; move it over {} by hand",
kept.display(),
exe.display()
)))
}
}
}
#[cfg(unix)]
fn swap_inner(staged: &NamedTempFile, exe: &Path) -> anyhow::Result<()> {
std::fs::rename(staged.path(), exe)
.with_context(|| format!("rename {} over {}", staged.path().display(), exe.display()))
}
#[cfg(windows)]
fn swap_inner(staged: &NamedTempFile, exe: &Path) -> anyhow::Result<()> {
self_replace::self_replace(staged.path())
.with_context(|| format!("replace {} with {}", exe.display(), staged.path().display()))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests;