use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Write;
use std::mem::size_of;
use std::error::Error;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use zstd::stream::write::Encoder as ZstdEncoder;
use crate::hpkg_common::*;
use crate::repository::{RepositoryHeaderV2, PackageInfo};
const ATTR_PACKAGE: u16 = 54;
const ATTR_PACKAGE_SUMMARY: u16 = 16;
const ATTR_PACKAGE_DESCRIPTION: u16 = 17;
const ATTR_PACKAGE_VENDOR: u16 = 18;
const ATTR_PACKAGE_PACKAGER: u16 = 19;
const ATTR_PACKAGE_FLAGS: u16 = 20;
const ATTR_PACKAGE_ARCHITECTURE: u16 = 21;
const ATTR_PACKAGE_VERSION_MAJOR: u16 = 22;
const ATTR_PACKAGE_VERSION_MINOR: u16 = 23;
const ATTR_PACKAGE_VERSION_MICRO: u16 = 24;
const ATTR_PACKAGE_VERSION_REVISION: u16 = 25;
const ATTR_PACKAGE_COPYRIGHT: u16 = 26;
const ATTR_PACKAGE_LICENSE: u16 = 27;
const ATTR_PACKAGE_PROVIDES: u16 = 28;
const ATTR_PACKAGE_REQUIRES: u16 = 29;
const ATTR_PACKAGE_CHECKSUM: u16 = 35;
const ATTR_PACKAGE_URL: u16 = 38;
const ATTR_PACKAGE_SOURCE_URL: u16 = 39;
const ATTR_PACKAGE_INSTALL_PATH: u16 = 40;
pub struct RepositoryWriter {
path: PathBuf,
compression: u16,
chunk_size: u32,
name: Option<String>,
identifier: Option<String>,
base_url: Option<String>,
vendor: Option<String>,
summary: Option<String>,
priority: Option<u8>,
architecture: Option<u64>,
license_names: Vec<String>,
license_texts: Vec<String>,
packages: Vec<PackageInfo>,
}
impl RepositoryWriter {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
RepositoryWriter {
path: path.as_ref().to_path_buf(),
compression: B_HPKG_COMPRESSION_NONE,
chunk_size: 65536,
name: None,
identifier: None,
base_url: None,
vendor: None,
summary: None,
priority: None,
architecture: None,
license_names: Vec::new(),
license_texts: Vec::new(),
packages: Vec::new(),
}
}
pub fn set_name(&mut self, name: &str) -> &mut Self {
self.name = Some(name.to_string());
self
}
pub fn set_identifier(&mut self, id: &str) -> &mut Self {
self.identifier = Some(id.to_string());
self
}
pub fn set_base_url(&mut self, url: &str) -> &mut Self {
self.base_url = Some(url.to_string());
self
}
pub fn set_vendor(&mut self, vendor: &str) -> &mut Self {
self.vendor = Some(vendor.to_string());
self
}
pub fn set_summary(&mut self, summary: &str) -> &mut Self {
self.summary = Some(summary.to_string());
self
}
pub fn set_priority(&mut self, priority: u8) -> &mut Self {
self.priority = Some(priority);
self
}
pub fn set_architecture(&mut self, arch: u64) -> &mut Self {
self.architecture = Some(arch);
self
}
pub fn add_license(&mut self, name: &str, text: &str) -> &mut Self {
self.license_names.push(name.to_string());
self.license_texts.push(text.to_string());
self
}
pub fn set_compression(&mut self, compression: u16) -> &mut Self {
self.compression = compression;
self
}
pub fn add_package(&mut self, pkg: PackageInfo) -> &mut Self {
self.packages.push(pkg);
self
}
pub fn finish(&mut self) -> Result<(), Box<dyn Error>> {
let info_buf = self.serialize_info();
let info_length = info_buf.len() as u32;
let pkg_strings: Vec<u8> = Vec::new();
let mut pkg_tree: Vec<u8> = Vec::new();
let pkg_strings_count: u64 = 0;
self.serialize_package_list(&mut pkg_tree)?;
let pkg_strings_length = pkg_strings.len() as u64;
let package_length = (pkg_strings_length + pkg_tree.len() as u64) as u64;
let mut heap = Vec::new();
heap.extend_from_slice(&info_buf);
heap.extend_from_slice(&pkg_strings);
heap.extend_from_slice(&pkg_tree);
let heap_size_uncompressed = heap.len() as u64;
let chunk_count = (heap_size_uncompressed + self.chunk_size as u64 - 1) / self.chunk_size as u64;
let mut compressed_chunks: Vec<Vec<u8>> = Vec::new();
let mut chunk_size_table: Vec<u16> = Vec::new();
for i in 0..chunk_count {
let start = (i * self.chunk_size as u64) as usize;
let end = std::cmp::min(start + self.chunk_size as usize, heap.len());
let chunk_data = &heap[start..end];
let compressed = match self.compression {
B_HPKG_COMPRESSION_NONE => chunk_data.to_vec(),
B_HPKG_COMPRESSION_ZLIB => {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(chunk_data)?;
let c = encoder.finish()?;
if c.len() < chunk_data.len() { c } else { chunk_data.to_vec() }
}
B_HPKG_COMPRESSION_ZSTD => {
let mut encoder = ZstdEncoder::new(Vec::new(), 3)?;
encoder.write_all(chunk_data)?;
let c = encoder.finish()?;
if c.len() < chunk_data.len() { c } else { chunk_data.to_vec() }
}
_ => return Err(From::from(format!("Unknown compression: {}", self.compression))),
};
compressed_chunks.push(compressed);
}
let mut heap_size_compressed: u64 = 0;
if self.compression == B_HPKG_COMPRESSION_NONE {
heap_size_compressed = heap_size_uncompressed;
} else {
for (i, chunk) in compressed_chunks.iter().enumerate() {
heap_size_compressed += chunk.len() as u64;
if i < compressed_chunks.len() - 1 {
chunk_size_table.push((chunk.len() as u16).wrapping_sub(1));
}
}
heap_size_compressed += (chunk_size_table.len() * 2) as u64;
}
let header_size = size_of::<RepositoryHeaderV2>() as u16;
let header = RepositoryHeaderV2 {
magic: u32::from_ne_bytes([b'h', b'p', b'k', b'r']),
header_size: header_size.to_be(),
version: 2u16.to_be(),
total_size: (header_size as u64 + heap_size_compressed).to_be(),
minor_version: 0u16.to_be(),
heap_compression: self.compression.to_be(),
heap_chunk_size: self.chunk_size.to_be(),
heap_size_compressed: heap_size_compressed.to_be(),
heap_size_uncompressed: heap_size_uncompressed.to_be(),
info_length: info_length.to_be(),
reserved1: 0u32.to_be(),
package_length: package_length.to_be(),
package_strings_length: pkg_strings_length.to_be(),
package_strings_count: pkg_strings_count.to_be(),
};
let mut f = File::create(&self.path)?;
let header_bytes = unsafe {
std::slice::from_raw_parts(
&header as *const RepositoryHeaderV2 as *const u8,
size_of::<RepositoryHeaderV2>(),
)
};
f.write_all(header_bytes)?;
for chunk in &compressed_chunks {
f.write_all(chunk)?;
}
if self.compression != B_HPKG_COMPRESSION_NONE {
for &cookie in &chunk_size_table {
f.write_all(&cookie.to_be_bytes())?;
}
}
f.sync_all()?;
Ok(())
}
fn serialize_info(&self) -> Vec<u8> {
const B_STRING_TYPE: u32 = 0x43535452; const B_INT8_TYPE: u32 = 0x42595445;
let mut fields: Vec<BMessageField> = Vec::new();
let string_fields: &[(&str, Option<&str>)] = &[
("name", self.name.as_deref()),
("identifier", self.identifier.as_deref()),
("baseurl", self.base_url.as_deref()),
("vendor", self.vendor.as_deref()),
("summary", self.summary.as_deref()),
];
for &(name, value) in string_fields {
if let Some(v) = value {
let encoded = encode_bmessage_string(v);
add_bmessage_field(&mut fields, name, B_STRING_TYPE, false, encoded);
}
}
for (i, name) in self.license_names.iter().enumerate() {
add_bmessage_field(&mut fields, "licenseName", B_STRING_TYPE, false, encode_bmessage_string(name));
if let Some(text) = self.license_texts.get(i) {
add_bmessage_field(&mut fields, "licenseText", B_STRING_TYPE, false, encode_bmessage_string(text));
}
}
if let Some(p) = self.priority {
add_bmessage_field(&mut fields, "priority", B_INT8_TYPE, true, vec![p]);
}
if let Some(a) = self.architecture {
add_bmessage_field(&mut fields, "architecture", B_INT8_TYPE, true, vec![a as u8]);
}
build_bmessage_buffer(&fields)
}
}
fn encode_bmessage_string(value: &str) -> Vec<u8> {
let encoded = value.as_bytes();
let str_len = encoded.len() as u32 + 1;
let mut buf = Vec::new();
buf.extend_from_slice(&str_len.to_le_bytes());
buf.extend_from_slice(encoded);
buf.push(0);
buf
}
fn add_bmessage_field(fields: &mut Vec<BMessageField>, name: &str,
type_code: u32, fixed_size: bool, value: Vec<u8>)
{
if let Some(f) = fields.iter_mut().find(|f| f.name == name && f.type_code == type_code) {
f.values.push(value);
} else {
fields.push(BMessageField {
name: name.to_string(),
type_code,
fixed_size,
values: vec![value],
});
}
}
struct BMessageField {
name: String,
type_code: u32,
fixed_size: bool,
values: Vec<Vec<u8>>,
}
fn build_bmessage_buffer(fields: &[BMessageField]) -> Vec<u8> {
let mut data: Vec<u8> = Vec::new();
let mut field_headers: Vec<[u8; 24]> = Vec::new();
for field in fields {
let name_offset = data.len() as u32;
let name_bytes = field.name.as_bytes();
let name_len = name_bytes.len() as u16 + 1;
data.extend_from_slice(name_bytes);
data.push(0);
for value in &field.values {
data.extend_from_slice(value);
}
let values_start = name_offset + name_len as u32;
let data_size = data.len() as u32 - values_start;
let count = field.values.len() as u32;
let flags: u16 = if field.fixed_size { 3 } else { 1 };
let mut fh = [0u8; 24];
fh[0..2].copy_from_slice(&flags.to_le_bytes());
fh[2..4].copy_from_slice(&name_len.to_le_bytes());
fh[4..8].copy_from_slice(&field.type_code.to_le_bytes());
fh[8..12].copy_from_slice(&count.to_le_bytes());
fh[12..16].copy_from_slice(&data_size.to_le_bytes());
fh[16..20].copy_from_slice(&name_offset.to_le_bytes());
field_headers.push(fh);
}
const HASH_TABLE_SIZE: u32 = 5;
let mut hash_table = vec![-1i32; HASH_TABLE_SIZE as usize];
for (i, field) in fields.iter().enumerate() {
let hash = (hash_name(&field.name) % HASH_TABLE_SIZE) as usize;
let next = hash_table[hash];
hash_table[hash] = i as i32;
field_headers[i][20..24].copy_from_slice(&next.to_le_bytes());
}
while data.len() % 4 != 0 {
data.push(0);
}
let field_count = fields.len() as u32;
let data_size = data.len() as u32;
let mut buf = Vec::with_capacity(68 + field_headers.len() * 24 + data.len());
buf.extend_from_slice(b"HMF1");
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&(-1i32).to_le_bytes());
buf.extend_from_slice(&(-1i32).to_le_bytes());
buf.extend_from_slice(&(-1i32).to_le_bytes());
buf.extend_from_slice(&(-1i32).to_le_bytes());
buf.extend_from_slice(&(-1i32).to_le_bytes());
buf.extend_from_slice(&(-1i32).to_le_bytes());
buf.extend_from_slice(&data_size.to_le_bytes());
buf.extend_from_slice(&field_count.to_le_bytes());
buf.extend_from_slice(&HASH_TABLE_SIZE.to_le_bytes());
for &entry in &hash_table {
buf.extend_from_slice(&entry.to_le_bytes());
}
for fh in &field_headers {
buf.extend_from_slice(fh);
}
buf.extend_from_slice(&data);
buf
}
fn hash_name(name: &str) -> u32 {
let mut hash: u32 = 0;
for &byte in name.as_bytes() {
hash = (hash << 7) ^ (hash >> 24);
hash ^= byte as u32;
}
hash ^= hash << 12;
hash
}
impl RepositoryWriter {
fn serialize_package_list(&self, buf: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
for pkg in &self.packages {
let pkg_name = pkg.name.as_deref().unwrap_or("");
let has_children = pkg.summary.is_some()
|| pkg.description.is_some()
|| pkg.vendor.is_some()
|| pkg.packager.is_some()
|| pkg.flags != 0
|| pkg.architecture.is_some()
|| pkg.version_major.is_some()
|| pkg.version_minor.is_some()
|| pkg.version_micro.is_some()
|| pkg.version_revision.is_some()
|| !pkg.copyrights.is_empty()
|| !pkg.licenses.is_empty()
|| !pkg.provides.is_empty()
|| !pkg.requires.is_empty()
|| pkg.checksum.is_some()
|| pkg.url.is_some()
|| pkg.source_url.is_some()
|| pkg.install_path.is_some();
write_attr_tag(buf, ATTR_PACKAGE, HPKG_ATTR_TYPE_STRING, HPKG_ATTR_ENCODING_STRING_INLINE, has_children);
write_string_inline(buf, pkg_name);
if has_children {
self.serialize_package_attributes(buf, pkg)?;
write_unsigned_leb128(buf, 0);
}
}
Ok(())
}
fn serialize_package_attributes(&self, buf: &mut Vec<u8>, pkg: &PackageInfo) -> Result<(), Box<dyn Error>> {
let string_attrs: &[(u16, Option<&str>)] = &[
(ATTR_PACKAGE_SUMMARY, pkg.summary.as_deref()),
(ATTR_PACKAGE_DESCRIPTION, pkg.description.as_deref()),
(ATTR_PACKAGE_VENDOR, pkg.vendor.as_deref()),
(ATTR_PACKAGE_PACKAGER, pkg.packager.as_deref()),
(ATTR_PACKAGE_VERSION_MAJOR, pkg.version_major.as_deref()),
(ATTR_PACKAGE_VERSION_MINOR, pkg.version_minor.as_deref()),
(ATTR_PACKAGE_VERSION_MICRO, pkg.version_micro.as_deref()),
(ATTR_PACKAGE_CHECKSUM, pkg.checksum.as_deref()),
(ATTR_PACKAGE_URL, pkg.url.as_deref()),
(ATTR_PACKAGE_SOURCE_URL, pkg.source_url.as_deref()),
(ATTR_PACKAGE_INSTALL_PATH, pkg.install_path.as_deref()),
];
for &(id, value) in string_attrs {
if let Some(v) = value {
write_attr_tag(buf, id, HPKG_ATTR_TYPE_STRING, HPKG_ATTR_ENCODING_STRING_INLINE, false);
write_string_inline(buf, v);
}
}
if pkg.flags != 0 {
write_attr_tag(buf, ATTR_PACKAGE_FLAGS, HPKG_ATTR_TYPE_UINT, uint_encoding(pkg.flags as u64), false);
write_int_value(buf, pkg.flags as u64, uint_encoding(pkg.flags as u64));
}
if let Some(arch) = &pkg.architecture {
if let Ok(v) = string_to_arch(arch) {
write_attr_tag(buf, ATTR_PACKAGE_ARCHITECTURE, HPKG_ATTR_TYPE_UINT, uint_encoding(v), false);
write_int_value(buf, v, uint_encoding(v));
}
}
if let Some(rev) = pkg.version_revision {
write_attr_tag(buf, ATTR_PACKAGE_VERSION_REVISION, HPKG_ATTR_TYPE_UINT, uint_encoding(rev), false);
write_int_value(buf, rev, uint_encoding(rev));
}
let list_attrs: &[(u16, &[String])] = &[
(ATTR_PACKAGE_COPYRIGHT, &pkg.copyrights),
(ATTR_PACKAGE_LICENSE, &pkg.licenses),
(ATTR_PACKAGE_PROVIDES, &pkg.provides),
(ATTR_PACKAGE_REQUIRES, &pkg.requires),
];
for &(id, list) in list_attrs {
for item in list {
write_attr_tag(buf, id, HPKG_ATTR_TYPE_STRING, HPKG_ATTR_ENCODING_STRING_INLINE, false);
write_string_inline(buf, item);
}
}
Ok(())
}
}
fn string_to_arch(s: &str) -> Result<u64, Box<dyn Error>> {
match s {
"any" => Ok(ARCH_ANY),
"x86" => Ok(ARCH_X86),
"x86_gcc2" => Ok(ARCH_X86_GCC2),
"source" => Ok(ARCH_SOURCE),
"x86_64" => Ok(ARCH_X86_64),
"ppc" => Ok(ARCH_PPC),
"arm" => Ok(ARCH_ARM),
"m68k" => Ok(ARCH_M68K),
"sparc" => Ok(ARCH_SPARC),
"arm64" => Ok(ARCH_ARM64),
"riscv64" => Ok(ARCH_RISCV64),
_ => {
if let Some(num) = s.strip_prefix("arch_") {
num.parse::<u64>().map_err(|_| From::from(format!("Unknown architecture: {}", s)))
} else {
Err(From::from(format!("Unknown architecture: {}", s)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repository::Repository;
#[test]
fn test_writer_round_trip_uncompressed() {
let tmp = std::env::temp_dir().join("test_repo_uncompressed.hpkr");
let _ = std::fs::remove_file(&tmp);
{
let mut writer = RepositoryWriter::new(&tmp);
writer.set_name("TestRepo")
.set_summary("A test repository")
.set_vendor("Test Vendor")
.set_architecture(ARCH_X86_64)
.set_priority(1)
.set_compression(B_HPKG_COMPRESSION_NONE);
let mut pkg = PackageInfo::new();
pkg.name = Some("hello".to_string());
pkg.summary = Some("A hello package".to_string());
pkg.vendor = Some("Test".to_string());
pkg.architecture = Some("x86_64".to_string());
pkg.version_major = Some("1".to_string());
pkg.version_minor = Some("0".to_string());
pkg.version_revision = Some(1);
pkg.licenses.push("MIT".to_string());
pkg.provides.push("hello = 1.0".to_string());
writer.add_package(pkg);
let mut pkg2 = PackageInfo::new();
pkg2.name = Some("world".to_string());
pkg2.summary = Some("A world package".to_string());
pkg2.architecture = Some("x86_64".to_string());
writer.add_package(pkg2);
writer.finish().expect("write should succeed");
}
let repo = Repository::load(&tmp).expect("should load written repository");
assert_eq!(repo.info.name.as_deref(), Some("TestRepo"));
assert_eq!(repo.info.summary.as_deref(), Some("A test repository"));
assert_eq!(repo.info.vendor.as_deref(), Some("Test Vendor"));
assert_eq!(repo.info.priority, Some(1));
assert_eq!(repo.packages.len(), 2);
let pkg = &repo.packages[0];
assert_eq!(pkg.name.as_deref(), Some("hello"));
assert_eq!(pkg.summary.as_deref(), Some("A hello package"));
assert_eq!(pkg.vendor.as_deref(), Some("Test"));
assert_eq!(pkg.architecture.as_deref(), Some("x86_64"));
assert_eq!(pkg.version_major.as_deref(), Some("1"));
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn test_writer_round_trip_zlib() {
let tmp = std::env::temp_dir().join("test_repo_zlib.hpkr");
let _ = std::fs::remove_file(&tmp);
{
let mut writer = RepositoryWriter::new(&tmp);
writer.set_name("ZlibRepo")
.set_summary("Zlib compressed repo test")
.set_vendor("Test")
.set_architecture(ARCH_X86_64)
.set_compression(B_HPKG_COMPRESSION_ZLIB);
let mut pkg = PackageInfo::new();
pkg.name = Some("test_pkg".to_string());
pkg.summary = Some("A zlib test package".to_string());
writer.add_package(pkg);
writer.finish().expect("write should succeed");
}
let repo = Repository::load(&tmp).expect("should load zlib repo");
assert_eq!(repo.info.name.as_deref(), Some("ZlibRepo"));
assert_eq!(repo.packages.len(), 1);
assert_eq!(repo.packages[0].name.as_deref(), Some("test_pkg"));
let _ = std::fs::remove_file(&tmp);
}
}