hpkg 0.0.7

A native Rust crate to parse Haiku's binary package and repo formats
Documentation
/*
 * Copyright, 2017-2023, 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::error::Error;
use std::path::{Path,PathBuf};
use std::fs::{read_to_string};
use std::fs;
use std::ffi::OsStr;
use std::fmt::{Display,Formatter};

use crate::recipe::Recipe;

pub struct PortsTree {
    path: String,
	categories: Vec<String>,
	ports: Vec<Recipe>,
}

impl Display for PortsTree {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		for port in self.ports.iter() {
			write!(f, "{}\n", port)?;
		}
        Ok(())
    }
}

/// PortsTree represents a tree of ports for Haiku (HaikuPorts source tree)
impl PortsTree {
	/// Create a new ports tree for the provided path.  Don't forget to rescan()
    pub fn new(path: String) -> PortsTree {
        PortsTree {
            path: path,
			categories: Vec::new(),
			ports: Vec::new(),
        }
    }
	/// Returns a PathBuf representing the base of the configured ports tree
	pub fn base_path(&mut self) -> Result<PathBuf, Box<dyn Error>> {
        let ports_base = Path::new(&self.path);
        if !ports_base.is_dir() {
            return Err(From::from(format!("Ports tree path unavailable")));
        }
		Ok(ports_base.to_path_buf())
	}
	/// Returns the ABI version of the ports tree itself
	pub fn version(&mut self) -> Result<u8, Box<dyn Error>> {
		let base = self.base_path()?;
		let repoformat_fh = base.join("FormatVersions");
		if !repoformat_fh.is_file() {
			return Err(From::from(format!("Ports tree RepositoryFormat unavailable")));
		}
		for line in read_to_string(repoformat_fh)?.lines() {
			if line.starts_with("RecipeFormatVersion=") {
				let v: Vec<&str> = line.split('=').collect();
				return Ok(v[1].parse::<u8>()?)
			}
		}
		Err(From::from(format!("Ports tree RepositoryFormat invalid")))
	}
	/// Rescans the ports tree and re-collects data for it
    pub fn rescan(&mut self, primary_arch: String, secondary_arch: Option<String>) -> Result<usize, Box<dyn Error>> {
		let base = self.base_path()?;
		let version = self.version()?;
		assert!(version == 1, "Unknown ports tree version");

		// empty our Vecs
		self.categories.clear();
		self.ports.clear();

		let _primary_architecture = primary_arch.clone();
		let secondary_architecture = secondary_arch.unwrap_or("".to_string());

		// collect categories
		for entry in fs::read_dir(base)? {
			let entry = entry?;
			let category_path = entry.path();

			// If it's not a directory, skip
			if !category_path.is_dir() {
				continue
			};
			// If it starts with . or is none, skip
			let category_name = category_path.file_name().and_then(OsStr::to_str);
			if category_name.is_none() || category_name.unwrap().starts_with(".") {
				continue
			};
			let category_name = category_name.unwrap();

			// If it is a reserved name, skip
			if ["packages", "repository", "stub"].contains(&category_name) {
				continue
			};
			self.categories.push(category_name.to_string());

			for port_entry in fs::read_dir(&category_path)? {
				let port_entry = port_entry?;
				let port_path = port_entry.path();
				if !port_path.is_dir() {
					continue
				}
				for recipe_entry in fs::read_dir(port_path)? {
					let recipe_entry = recipe_entry?;
					let recipe_path = recipe_entry.path();
					let extension = recipe_path.extension();
					if !recipe_path.is_file() || extension.is_none() || extension.unwrap() != "recipe" {
						continue
					}

					println!("Scanning {}", &recipe_path.display());
					let mut recipe = Recipe::new_from_file(&recipe_path, category_name.to_string())?;
					let port_version = &recipe.version.clone();
					recipe.set_vars("secondaryArchSuffix".to_string(), secondary_architecture.clone());
					recipe.set_vars("portVersion".to_string(), port_version.to_string());
					self.ports.push(recipe);
				}
			}
		}

		Ok(self.ports.len())
    }
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	/// Test creating a new empty portstree definition
	fn test_portstree_new() {
		let _portstree = PortsTree::new("samples/ports".to_string());
	}
	#[test]
	/// Test reading portstree version
	fn test_portstree_version() {
		let mut portstree = PortsTree::new("sample/ports".to_string());
		let version = portstree.version();
		assert!(version.is_ok(), "Ports tree version compute failure!");
		assert!(version.unwrap() == 1, "Ports tree version mismatch!");
	}
	#[test]
	/// Test scanning a ports tree
	fn test_portstree_scan() {
		let mut portstree = PortsTree::new("sample/ports".to_string());
		assert!(portstree.rescan("x86_64".to_string(), Some("x86_gcc2".to_string())).is_ok(),
			"Ports tree scan failed!");
	}
}