hpkg 0.0.1

A native Rust crate to parse Haiku's binary package and repo formats
Documentation
/*
 * Copyright, 2017-2020, Alexander von Gluck IV. All rights reserved.
 * Released under the terms of the MIT license.
 *
 * vim: set noai noet ts=4 sw=4:
 *
 * Authors:
 *   Alexander von Gluck IV <kallisti5@unixzen.com>
 */


use std::fmt;
use std::path::Path;
use std::fs::File;
use std::io;
use std::io::{Read,Seek,SeekFrom,BufReader};
use std::error;
use std::slice;

pub const MAX_TOC:			u64 = 64 * 1024 * 1024;
pub const MAX_ATTRIBUTES:	u64 = 1 * 1204 * 1024;

#[derive(Debug, Clone)]
#[repr(C)]
pub struct PackageHeader {
	pub magic: u32,
	pub header_size: u16,
	pub version: u16,
	pub total_size: u64,
	pub minor_version: u16,

	// Heap
	pub heap_compression: u16,
	pub heap_chunk_size: u32,
	pub heap_size_compressed: u64,
	pub heap_size_uncompressed: u64,

	// package attributes section
	pub attributes_length: u32,
	pub attributes_strings_length: u32,
	pub attributes_strings_count: u32,
	pub reserved1: u32,

	// TOC section
	pub toc_length: u64,
	pub toc_strings_length: u64,
	pub toc_strings_count: u64,
}

#[derive(Debug, Clone)]
pub struct PackageFileSection {
	pub uncompressed_length: u32,
	pub data: u8,		// TODO: Data uint8*
	pub offset: u64,
	pub current_offset: u64,
	pub strings_length: u64,
	pub strings_count: u64,
	pub strings: u8,	// TODO: char**
	pub name: String,
}

#[derive(Debug, Clone)]
pub struct Package {
	pub header: Option<PackageHeader>,

	pub name: Option<String>,
	pub summary: Option<String>,
	pub description: Option<String>,
	pub vendor: Option<String>,
	pub packager: Option<String>,
	pub basepackage: Option<i32>,
	pub checksum: Option<String>,
	pub installpath: Option<String>,
	pub filename: Option<String>,
	pub flags: u32,
	pub architecture: Option<String>,
}

fn read_struct<T, R: Read>(mut read: R) -> io::Result<T> {
	let num_bytes = ::std::mem::size_of::<T>();
	unsafe {
		let mut s = ::std::mem::zeroed();
		let buffer = slice::from_raw_parts_mut(&mut s as *mut T as *mut u8, num_bytes);
		match read.read_exact(buffer) {
			Ok(()) => Ok(s),
			Err(e) => {
				Err(e)
			}
		}
	}
}

fn parse_header<P: AsRef<Path>>(hpkg_file: P)
	-> Result<PackageHeader, Box<dyn error::Error>> {

	let mut f = File::open(hpkg_file.as_ref())?;
	f.seek(SeekFrom::Start(0))?;
	let reader = BufReader::new(f);

	let mut header = read_struct::<PackageHeader, _>(reader)?;
	let magic_bytes = header.magic.to_ne_bytes();
	if magic_bytes != [b'h', b'p', b'k', b'g'] {
		return Err(From::from(format!("Unknown magic: {:?}", magic_bytes)));
	}

	// Endian Adjustments (are there better ways to do this?)
	header.header_size = u16::from_be(header.header_size);
	header.version = u16::from_be(header.version);
	header.total_size = u64::from_be(header.total_size);
	header.minor_version = u16::from_be(header.minor_version);
	header.heap_compression = u16::from_be(header.heap_compression);
	header.heap_chunk_size = u32::from_be(header.heap_chunk_size);
	header.heap_size_compressed = u64::from_be(header.heap_size_compressed);
	header.heap_size_uncompressed = u64::from_be(header.heap_size_uncompressed);
	header.attributes_length = u32::from_be(header.attributes_length);
	header.attributes_strings_length = u32::from_be(header.attributes_strings_length);
	header.attributes_strings_count = u32::from_be(header.attributes_strings_count);
	header.reserved1 = u32::from_be(header.reserved1);
	header.toc_length = u64::from_be(header.toc_length);
	header.toc_strings_length = u64::from_be(header.toc_strings_length);
	header.toc_strings_count = u64::from_be(header.toc_strings_count);

	if header.version != 2 {
		return Err(From::from(format!("Unknown repo version: {}", header.version)));
	}

	if header.minor_version != 0 {
		return Err(From::from(format!("Unknown repo minor version: {}", header.version)));
	}

	Ok(header)
}

impl fmt::Display for Package {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		write!(f, "package. Name {:?}, Vendor {:?}, Summary {:?}, Arch {:?}",
			self.name, self.vendor, self.summary, self.architecture)
	}
}

impl Package {
	pub fn new() -> Package {
		Package {
			header: None,
			name: None,
			summary: None,
			description: None,
			vendor: None,
			packager: None,
			basepackage: None,
			checksum: None,
			installpath: None,
			filename: None,
			flags: 0,
			architecture: None,
		}
	}

	pub fn load<P: AsRef<Path>>(hpkg_file: P)
		-> Result<Package, Box<dyn error::Error>> {

		let mut f = File::open(hpkg_file.as_ref())?;
		f.seek(SeekFrom::Start(0))?;

		let mut hpkg = Package::new();
		hpkg.header = Some(self::parse_header(hpkg_file)?);

		return Ok(hpkg);
	}
}


#[test]
/// Test creating a new empty package definition
fn test_new_package() {
	let _package = Package::new();
}

#[test]
/// Test loading a valid package from disk
fn test_load_valid_package() {
	let hpkg = match Package::load("sample/ctags_source-5.8-5-source.hpkg") {
		Ok(o) => o,
		Err(e) => {
			println!("ERROR: {}", e);
			assert!(false);
			return;
		},
	};
	assert!(hpkg.header.is_some());
}

#[test]
/// Test loading an invalid package from disk
fn test_load_invalid_package() {
	assert!(Package::load("sample/source-5.8-5-source.hpkg").is_err());
}

#[test]
/// Test total size compared to header
fn test_total_size() {
	let metadata = match std::fs::metadata("sample/ctags_source-5.8-5-source.hpkg") {
		Ok(o) => o,
		Err(e) => {
			println!("ERROR: {}", e);
			assert!(false);
			return;
		},
	};
	let hpkg = match Package::load("sample/ctags_source-5.8-5-source.hpkg") {
		Ok(o) => o,
		Err(e) => {
			println!("ERROR: {}", e);
			assert!(false);
			return;
		},
	};
	let header = match hpkg.header {
		Some(o) => o,
		None => {
			println!("ERROR: Invalid Header!");
			assert!(false);
			return;
		},
	};
	assert_eq!(metadata.len(), header.total_size);
}

#[test]
/// Test displaying package information
fn test_dump_package_info() {
	let hpkg = match Package::load("sample/ctags_source-5.8-5-source.hpkg") {
		Ok(o) => o,
		Err(e) => {
			println!("ERROR: {}", e);
			assert!(false);
			return;
		},
	};
	println!("{:?}", hpkg);
}