use pna::ReadOptions;
use pna::prelude::*;
use std::{
fs::File,
io::{self, Read, Write},
path::Path,
};
pub struct FileEntryDef<'a> {
pub path: &'a str,
pub content: &'a [u8],
pub permission: u16,
}
pub fn xattr(name: &str, value: &[u8]) -> pna::ExtendedAttribute {
pna::ExtendedAttribute::new(
pna::XattrName::try_from(name).expect("xattr name fits within u32::MAX bytes"),
pna::XattrValue::try_from(value).expect("xattr value fits within u32::MAX bytes"),
)
}
pub fn create_archive_with_permissions(
archive_path: impl AsRef<Path>,
entries: &[FileEntryDef],
) -> io::Result<()> {
let file = File::create(archive_path)?;
let mut archive = pna::Archive::write_header(file)?;
for entry_def in entries {
let mut builder = pna::FileEntryBuilder::new(entry_def.path.into())?;
builder.metadata(
pna::Metadata::new()
.with_owner_uid(Some(pna::OwnerUid::from(1000)))
.with_owner_gid(Some(pna::OwnerGid::from(1000)))
.with_owner_user_name(Some(pna::OwnerUserName::new("user").unwrap()))
.with_owner_group_name(Some(pna::OwnerGroupName::new("group").unwrap()))
.with_permission_mode(Some(pna::PermissionMode::from(entry_def.permission))),
);
builder.write_all(entry_def.content)?;
let entry = builder.build()?;
archive.add_entry(entry)?;
}
archive.finalize()?;
Ok(())
}
pub fn create_solid_archive_with_permissions(
archive_path: impl AsRef<Path>,
entries: &[FileEntryDef],
) -> io::Result<()> {
let file = File::create(archive_path)?;
let mut archive = pna::Archive::write_header(file)?;
let mut solid_builder = pna::SolidEntryBuilder::new(pna::WriteOptions::store())?;
for entry_def in entries {
let mut builder = pna::FileEntryBuilder::new(entry_def.path.into())?;
builder.metadata(
pna::Metadata::new()
.with_owner_uid(Some(pna::OwnerUid::from(1000)))
.with_owner_gid(Some(pna::OwnerGid::from(1000)))
.with_owner_user_name(Some(pna::OwnerUserName::new("user").unwrap()))
.with_owner_group_name(Some(pna::OwnerGroupName::new("group").unwrap()))
.with_permission_mode(Some(pna::PermissionMode::from(entry_def.permission))),
);
builder.write_all(entry_def.content)?;
let entry = builder.build()?;
solid_builder.add_entry(entry)?;
}
let solid_entry = solid_builder.build()?;
archive.add_entry(solid_entry)?;
archive.finalize()?;
Ok(())
}
pub fn create_encrypted_solid_archive_with_permissions(
archive_path: impl AsRef<Path>,
entries: &[FileEntryDef],
password: &str,
) -> io::Result<()> {
let file = File::create(archive_path)?;
let mut archive = pna::Archive::write_header(file)?;
let write_options = pna::WriteOptions::builder()
.password(Some(password))
.encryption(pna::Encryption::AES)
.cipher_mode(pna::CipherMode::GCM)
.build();
let mut solid_builder = pna::SolidEntryBuilder::new(write_options)?;
for entry_def in entries {
let mut builder = pna::FileEntryBuilder::new_with_options(
entry_def.path.into(),
pna::WriteOptions::store(),
)?;
builder.metadata(
pna::Metadata::new()
.with_owner_uid(Some(pna::OwnerUid::from(1000)))
.with_owner_gid(Some(pna::OwnerGid::from(1000)))
.with_owner_user_name(Some(pna::OwnerUserName::new("user").unwrap()))
.with_owner_group_name(Some(pna::OwnerGroupName::new("group").unwrap()))
.with_permission_mode(Some(pna::PermissionMode::from(entry_def.permission))),
);
builder.write_all(entry_def.content)?;
let entry = builder.build()?;
solid_builder.add_entry(entry)?;
}
let solid_entry = solid_builder.build()?;
archive.add_entry(solid_entry)?;
archive.finalize()?;
Ok(())
}
pub fn create_encrypted_archive_with_permissions(
archive_path: impl AsRef<Path>,
entries: &[FileEntryDef],
password: &str,
) -> io::Result<()> {
let file = File::create(archive_path)?;
let mut archive = pna::Archive::write_header(file)?;
let write_options = pna::WriteOptions::builder()
.password(Some(password))
.encryption(pna::Encryption::AES)
.cipher_mode(pna::CipherMode::CTR)
.build();
for entry_def in entries {
let mut builder =
pna::FileEntryBuilder::new_with_options(entry_def.path.into(), write_options.clone())?;
builder.metadata(
pna::Metadata::new()
.with_owner_uid(Some(pna::OwnerUid::from(1000)))
.with_owner_gid(Some(pna::OwnerGid::from(1000)))
.with_owner_user_name(Some(pna::OwnerUserName::new("user").unwrap()))
.with_owner_group_name(Some(pna::OwnerGroupName::new("group").unwrap()))
.with_permission_mode(Some(pna::PermissionMode::from(entry_def.permission))),
);
builder.write_all(entry_def.content)?;
let entry = builder.build()?;
archive.add_entry(entry)?;
}
archive.finalize()?;
Ok(())
}
pub fn extract_single_entry(
path: impl AsRef<Path>,
name: &str,
) -> io::Result<Option<pna::NormalEntry>> {
let mut archive = pna::Archive::open(path)?;
let entries = archive
.entries()
.extract_solid_entries(&ReadOptions::builder().build());
for entry in entries {
let entry = entry?;
if entry.header().path() == name {
return Ok(Some(entry));
}
}
Ok(None)
}
pub fn for_each_entry<F>(path: impl AsRef<Path>, f: F) -> io::Result<()>
where
F: FnMut(pna::NormalEntry),
{
for_each_entry_with_password(path, None, f)
}
pub fn for_each_entry_with_password<'a, F>(
path: impl AsRef<Path>,
password: impl Into<Option<&'a str>>,
mut f: F,
) -> io::Result<()>
where
F: FnMut(pna::NormalEntry),
{
let password = password.into().map(|p| p.as_bytes());
let mut archive = pna::Archive::open(path)?;
let read_options = ReadOptions::with_password(password);
let entries = archive.entries().extract_solid_entries(&read_options);
for entry in entries {
f(entry?);
}
Ok(())
}
pub fn entry_mode(path: impl AsRef<Path>, name: &str) -> u16 {
entry_mode_with_password(path, name, None)
}
pub fn entry_mode_with_password(path: impl AsRef<Path>, name: &str, password: Option<&str>) -> u16 {
let mut mode = None;
for_each_entry_with_password(path, password, |entry| {
if entry.name() == name {
mode = Some(
entry
.metadata()
.permission_mode()
.expect("entry should have permission mode metadata")
.get()
& 0o777,
);
}
})
.unwrap();
mode.expect("target entry should exist")
}
pub fn entry_contents_with_password(
path: impl AsRef<Path>,
name: &str,
password: Option<&str>,
) -> Vec<u8> {
let password_bytes = password.map(str::as_bytes);
let mut contents = None;
for_each_entry_with_password(path, password, |entry| {
if entry.name() == name {
let mut reader = entry
.reader(pna::ReadOptions::with_password(password_bytes))
.unwrap();
let mut data = Vec::new();
reader.read_to_end(&mut data).unwrap();
contents = Some(data);
}
})
.unwrap();
contents.expect("target entry should exist")
}
pub fn read_symlink_target(entry: &pna::NormalEntry) -> String {
let mut target = Vec::new();
entry
.reader(pna::ReadOptions::with_password::<&[u8]>(None))
.unwrap()
.read_to_end(&mut target)
.unwrap();
String::from_utf8(target).unwrap()
}
pub fn create_test_archive(path: impl AsRef<Path>, entries: &[(&str, &str)]) {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let file = File::create(path).unwrap();
let mut writer = pna::Archive::write_header(file).unwrap();
for (name, contents) in entries {
writer
.add_entry({
let mut builder = pna::FileEntryBuilder::new((*name).into()).unwrap();
builder.write_all(contents.as_bytes()).unwrap();
builder.build().unwrap()
})
.unwrap();
}
writer.finalize().unwrap();
}
pub struct SymlinkEntryDef<'a> {
pub path: &'a str,
pub target: &'a str,
pub permission: Option<u16>,
pub modified: Option<pna::Duration>,
pub accessed: Option<pna::Duration>,
pub created: Option<pna::Duration>,
pub link_target_type: Option<pna::LinkTargetType>,
}
pub fn create_archive_with_symlinks(
archive_path: impl AsRef<Path>,
file_entries: &[FileEntryDef],
symlink_entries: &[SymlinkEntryDef],
) -> io::Result<()> {
let file = File::create(archive_path)?;
let mut archive = pna::Archive::write_header(file)?;
for entry_def in file_entries {
let mut builder = pna::FileEntryBuilder::new(entry_def.path.into())?;
builder.metadata(
pna::Metadata::new()
.with_owner_uid(Some(pna::OwnerUid::from(1000)))
.with_owner_gid(Some(pna::OwnerGid::from(1000)))
.with_owner_user_name(Some(pna::OwnerUserName::new("user").unwrap()))
.with_owner_group_name(Some(pna::OwnerGroupName::new("group").unwrap()))
.with_permission_mode(Some(pna::PermissionMode::from(entry_def.permission))),
);
builder.write_all(entry_def.content)?;
let entry = builder.build()?;
archive.add_entry(entry)?;
}
for symlink_def in symlink_entries {
let mut builder =
pna::SymlinkEntryBuilder::new(symlink_def.path.into(), symlink_def.target.into())?;
let mut metadata = pna::Metadata::new().with_link_target_type(symlink_def.link_target_type);
if let Some(mode) = symlink_def.permission {
metadata = metadata
.with_owner_uid(Some(pna::OwnerUid::from(1000)))
.with_owner_gid(Some(pna::OwnerGid::from(1000)))
.with_owner_user_name(Some(pna::OwnerUserName::new("user").unwrap()))
.with_owner_group_name(Some(pna::OwnerGroupName::new("group").unwrap()))
.with_permission_mode(Some(pna::PermissionMode::from(mode)));
}
if let Some(m) = symlink_def.modified {
metadata = metadata.with_modified(Some(m));
}
if let Some(a) = symlink_def.accessed {
metadata = metadata.with_accessed(Some(a));
}
if let Some(c) = symlink_def.created {
metadata = metadata.with_created(Some(c));
}
builder.metadata(metadata);
let entry = builder.build()?;
archive.add_entry(entry)?;
}
archive.finalize()?;
Ok(())
}
pub fn get_archive_entry_names(path: impl AsRef<Path>) -> Vec<String> {
let mut names = Vec::new();
for_each_entry(path, |entry| {
names.push(entry.header().path().to_string());
})
.unwrap();
names
}
pub fn corrupt_first_chunk(
path: impl AsRef<Path>,
target: [u8; 4],
recompute_crc: bool,
) -> io::Result<bool> {
let mut bytes = std::fs::read(&path)?;
let mut pos = 8; while pos + 12 <= bytes.len() {
let len = u32::from_be_bytes(bytes[pos..pos + 4].try_into().unwrap()) as usize;
let ty: [u8; 4] = bytes[pos + 4..pos + 8].try_into().unwrap();
if ty == target && len > 0 {
let data_start = pos + 8;
bytes[data_start] ^= 0xFF;
if recompute_crc {
let chunk_type = unsafe { pna::ChunkType::from_unchecked(ty) };
let chunk = pna::RawChunk::from_data(
chunk_type,
bytes[data_start..data_start + len].to_vec(),
);
let crc_pos = data_start + len;
bytes[crc_pos..crc_pos + 4].copy_from_slice(&chunk.crc().to_be_bytes());
}
std::fs::write(&path, bytes)?;
return Ok(true);
}
pos += 12 + len; }
Ok(false)
}