use std::fs::{self, File};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use anyhow::Context;
use pkgar_core::{
dryoc::classic::crypto_sign::crypto_sign_detached, Entry, Header, Mode, PackageSrc,
};
use pkgar_keys::PublicKeyFile;
use crate::ext::{copy_and_hash, EntryExt};
use crate::package::PackageFile;
use crate::transaction::Transaction;
use crate::{Error, READ_WRITE_HASH_BUF_SIZE};
fn folder_entries<P, Q>(base: P, path: Q, entries: &mut Vec<Entry>) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let base = base.as_ref();
let path = path.as_ref();
let mut read_dir = Vec::new();
for entry_res in fs::read_dir(path)? {
read_dir.push(entry_res?);
}
read_dir.sort_by_key(|path| path.file_name());
for entry in read_dir {
let metadata = entry.metadata()?;
let entry_path = entry.path();
if metadata.is_dir() {
folder_entries(base, entry_path, entries)?;
} else {
let relative = entry_path
.strip_prefix(base)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let mut path_bytes = [0; 256];
let relative_bytes = relative.as_os_str().as_bytes();
if relative_bytes.len() >= path_bytes.len() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"relative path longer than supported: {} > {}",
relative_bytes.len(),
path_bytes.len()
),
));
}
path_bytes[..relative_bytes.len()].copy_from_slice(relative_bytes);
let file_type = metadata.file_type();
let file_mode = metadata.permissions().mode();
let mut mode = file_mode & Mode::PERM.bits();
if file_type.is_file() {
mode |= Mode::FILE.bits();
} else if file_type.is_symlink() {
mode |= Mode::SYMLINK.bits();
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported entry at {:?}: {:?}", relative, metadata),
));
}
entries.push(Entry {
blake3: [0; 32],
offset: 0,
size: metadata.len(),
mode,
path: path_bytes,
});
}
}
Ok(())
}
pub fn create(
secret_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
folder: impl AsRef<Path>,
) -> anyhow::Result<()> {
let keyfile = pkgar_keys::get_skey(secret_path.as_ref())?;
let secret_key = keyfile
.secret_key()
.unwrap_or_else(|| panic!("{} was encrypted?", secret_path.as_ref().display()));
let public_key = keyfile
.public_key()
.unwrap_or_else(|| panic!("{} was encrypted?", secret_path.as_ref().display()));
let archive_path = archive_path.as_ref();
let mut archive_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(archive_path)
.map_err(|source| Error::Io {
source,
path: Some(archive_path.to_path_buf()),
})?;
let mut entries = Vec::new();
let folder = folder.as_ref();
folder_entries(folder, folder, &mut entries)
.map_err(|source| Error::Io {
source,
path: Some(folder.to_path_buf()),
})
.context("Recursing buildroot")?;
let mut header = Header {
signature: [0; 64],
public_key,
blake3: [0; 32],
count: entries.len() as u64,
};
let mut data_size: u64 = 0;
for entry in &mut entries {
entry.offset = data_size;
data_size = data_size
.checked_add(entry.size)
.ok_or(pkgar_core::Error::Overflow)
.map_err(Error::from)
.context("Overflowed processing entry offsets")
.with_context(|| {
let offset = entry.offset;
let size = entry.size;
format!(
"Offset: {}, size: {}, path: {:?}",
offset,
size,
entry.check_path().unwrap_or_else(|_| Path::new(""))
)
})?;
}
let data_offset = header.total_size()?;
archive_file
.seek(SeekFrom::Start(data_offset as u64))
.map_err(|source| Error::Io {
source,
path: Some(archive_path.to_path_buf()),
})
.with_context(|| format!("Seek to {} (data offset)", data_offset))?;
let mut header_hasher = blake3::Hasher::new();
let mut buf = vec![0; 4 * 1024 * 1024];
for entry in &mut entries {
let relative = entry.check_path()?;
let path = folder.join(relative);
let mode = entry
.mode()
.map_err(Error::from)
.with_context(|| path.display().to_string())?;
let (total, hash) = match mode.kind() {
Mode::FILE => {
let mut entry_file =
fs::OpenOptions::new()
.read(true)
.open(&path)
.map_err(|source| Error::Io {
source,
path: Some(path.to_path_buf()),
})?;
copy_and_hash(&mut entry_file, &mut archive_file, &mut buf)
.map_err(|source| Error::Io {
source,
path: Some(path.to_path_buf()),
})
.with_context(|| {
format!("Writing entry to archive: '{}'", relative.display())
})?
}
Mode::SYMLINK => {
let destination = fs::read_link(&path).map_err(|source| Error::Io {
source,
path: Some(path.to_path_buf()),
})?;
let mut data = destination.as_os_str().as_bytes();
copy_and_hash(&mut data, &mut archive_file, &mut buf)
.map_err(|source| Error::Io {
source,
path: Some(path.to_path_buf()),
})
.with_context(|| {
format!("Writing entry to archive: '{}'", relative.display())
})?
}
_ => {
return Err(Error::from(pkgar_core::Error::InvalidMode(mode.bits())))
.with_context(|| path.display().to_string());
}
};
if total != entry.size() {
return Err(Error::LengthMismatch {
actual: total,
expected: entry.size(),
})
.with_context(|| path.display().to_string());
}
entry.blake3.copy_from_slice(hash.as_bytes());
header_hasher.update_with_join::<blake3::join::RayonJoin>(bytemuck::bytes_of(entry));
}
header
.blake3
.copy_from_slice(header_hasher.finalize().as_bytes());
let mut signature = [0; 64];
crypto_sign_detached(
&mut signature,
&bytemuck::bytes_of(&header)[64..],
&secret_key,
)
.map_err(pkgar_core::Error::Dryoc)?;
header.signature.copy_from_slice(&signature);
archive_file
.seek(SeekFrom::Start(0))
.map_err(|source| Error::Io {
source,
path: Some(archive_path.to_path_buf()),
})?;
archive_file
.write_all(bytemuck::bytes_of(&header))
.map_err(|source| Error::Io {
source,
path: Some(archive_path.to_path_buf()),
})?;
for entry in &entries {
let checked_path = entry.check_path()?;
archive_file
.write_all(bytemuck::bytes_of(entry))
.map_err(|source| Error::Io {
source,
path: Some(archive_path.to_path_buf()),
})
.with_context(|| format!("Write entry {}", checked_path.display()))?;
}
Ok(())
}
pub fn extract(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
base_dir: impl AsRef<Path>,
) -> anyhow::Result<()> {
let pkey = PublicKeyFile::open(pkey_path.as_ref())?.pkey;
let mut package = PackageFile::new(archive_path, &pkey)?;
Transaction::install(&mut package, base_dir)?.commit()?;
Ok(())
}
pub fn remove(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
base_dir: impl AsRef<Path>,
) -> anyhow::Result<()> {
let pkey = PublicKeyFile::open(pkey_path.as_ref())?.pkey;
let mut package = PackageFile::new(archive_path, &pkey)?;
Transaction::remove(&mut package, base_dir)?.commit()?;
Ok(())
}
pub fn list(pkey_path: impl AsRef<Path>, archive_path: impl AsRef<Path>) -> Result<(), Error> {
let pkey = PublicKeyFile::open(pkey_path.as_ref())?.pkey;
let mut package = PackageFile::new(archive_path, &pkey)?;
for entry in package.read_entries()? {
let relative = entry.check_path()?;
println!("{}", relative.display());
}
Ok(())
}
pub fn split(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
head_path: impl AsRef<Path>,
data_path_opt: Option<impl AsRef<Path>>,
) -> anyhow::Result<()> {
let pkey_path = pkey_path.as_ref();
let archive_path = archive_path.as_ref();
let head_path = head_path.as_ref();
let pkey = PublicKeyFile::open(pkey_path)?.pkey;
let package = PackageFile::new(archive_path, &pkey)?;
let data_offset = package.header().total_size()?;
let mut src = package.src.into_inner();
if let Some(data_path) = data_path_opt {
let data_path = data_path.as_ref();
let mut data_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(data_path)
.map_err(|source| Error::Io {
source,
path: Some(data_path.to_path_buf()),
})?;
src.seek(SeekFrom::Start(data_offset))
.map_err(|source| Error::Io {
source,
path: Some(archive_path.to_path_buf()),
})?;
io::copy(&mut src, &mut data_file)
.with_context(|| format!("Archive path: {}", archive_path.display()))
.with_context(|| format!("Data path: {}", data_path.display()))?;
}
{
let mut head_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(head_path)
.map_err(|source| Error::Io {
source,
path: Some(head_path.to_path_buf()),
})?;
src.seek(SeekFrom::Start(0)).map_err(|source| Error::Io {
source,
path: Some(archive_path.to_path_buf()),
})?;
io::copy(&mut src.take(data_offset), &mut head_file)
.with_context(|| format!("Archive path: {}", archive_path.display()))
.with_context(|| format!("Head path: {}", head_path.display()))?;
}
Ok(())
}
pub fn verify(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
base_dir: impl AsRef<Path>,
) -> Result<(), Error> {
let pkey = PublicKeyFile::open(pkey_path)?.pkey;
let mut package = PackageFile::new(archive_path, &pkey)?;
let mut buf = vec![0; READ_WRITE_HASH_BUF_SIZE];
for entry in package.read_entries()? {
let expected_path = base_dir.as_ref().join(entry.check_path()?);
let expected = File::open(&expected_path).map_err(|source| Error::Io {
source,
path: Some(expected_path.to_path_buf()),
})?;
let (count, hash) =
copy_and_hash(expected, io::sink(), &mut buf).map_err(|source| Error::Io {
source,
path: Some(expected_path.to_path_buf()),
})?;
entry.verify(hash, count)?;
}
Ok(())
}