apt-parser 1.0.6

A parser for the APT package manager's package lists
Documentation
use crate::{
	case_map::CaseMap,
	control::Control,
	errors::{APTError, MissingKeyError},
};
use rayon::prelude::*;
use std::ops::Index;

pub struct Package {
	pub(crate) map: CaseMap,
	pub package: String,
	pub source: Option<String>,
	pub version: String,
	pub section: Option<String>,
	pub priority: Option<String>,
	pub architecture: String,
	pub is_essential: Option<bool>,
	pub depends: Option<Vec<String>>,
	pub pre_depends: Option<Vec<String>>,
	pub recommends: Option<Vec<String>>,
	pub suggests: Option<Vec<String>>,
	pub replaces: Option<Vec<String>>,
	pub enhances: Option<Vec<String>>,
	pub breaks: Option<Vec<String>>,
	pub conflicts: Option<Vec<String>>,
	pub installed_size: Option<i64>,
	pub maintainer: Option<String>,
	pub description: Option<String>,
	pub homepage: Option<String>,
	pub built_using: Option<String>,
	pub package_type: Option<String>,
	pub tags: Option<Vec<String>>,
	pub filename: String,
	pub size: i64,
	pub md5sum: Option<String>,
	pub sha1sum: Option<String>,
	pub sha256sum: Option<String>,
	pub sha512sum: Option<String>,
	pub description_md5sum: Option<String>,
}

impl Package {
	pub fn from(data: &str) -> Result<Package, APTError> {
		let control = match Control::from(data) {
			Ok(control) => control,
			Err(err) => return Err(err),
		};

		let map = control.map;

		let filename = match map.get("Filename") {
			Some(filename) => filename.to_owned(),
			None => {
				return Err(APTError::MissingKeyError(MissingKeyError::new(
					"Filename", data,
				)))
			}
		};

		let size = match map.get("Size") {
			Some(size) => size.parse::<i64>().unwrap_or(-1),
			None => {
				return Err(APTError::MissingKeyError(MissingKeyError::new(
					"Size", data,
				)))
			}
		};

		Ok(Package {
			map: map.clone(),
			package: control.package,
			source: control.source,
			version: control.version,
			section: control.section,
			priority: control.priority,
			architecture: control.architecture,
			is_essential: control.is_essential,
			depends: control.depends,
			pre_depends: control.pre_depends,
			recommends: control.recommends,
			suggests: control.suggests,
			replaces: control.replaces,
			enhances: control.enhances,
			breaks: control.breaks,
			conflicts: control.conflicts,
			installed_size: control.installed_size,
			maintainer: control.maintainer,
			description: control.description,
			homepage: control.homepage,
			built_using: control.built_using,
			package_type: control.package_type,
			tags: control.tags,
			filename,
			size,
			md5sum: map.get("MD5Sum").cloned(),
			sha1sum: map.get("SHA1").cloned(),
			sha256sum: map.get("SHA256").cloned(),
			sha512sum: map.get("SHA512").cloned(),
			description_md5sum: map.get("Description-md5").cloned(),
		})
	}

	pub fn get(&self, key: &str) -> Option<&str> {
		self.map.get(key).map(|x| &**x)
	}
}

pub struct Packages {
	pub(crate) packages: Vec<Package>,
	pub errors: Vec<APTError>,
}

impl Packages {
	pub fn from(data: &str) -> Packages {
		let binding = data.replace("\r\n", "\n").replace('\0', "");
		let iter = binding.trim().split("\n\n").par_bridge().into_par_iter();

		let values = iter
			.map(|package| Package::from(&package))
			.collect::<Vec<Result<Package, APTError>>>();

		let mut packages = Vec::new();
		let mut errors = Vec::new();

		for value in values {
			match value {
				Ok(package) => packages.push(package),
				Err(err) => errors.push(err),
			}
		}

		Packages { packages, errors }
	}

	pub fn len(&self) -> usize {
		self.packages.len()
	}
}

impl Iterator for Packages {
	type Item = Package;

	fn next(&mut self) -> Option<Self::Item> {
		self.packages.pop()
	}
}

impl Index<usize> for Packages {
	type Output = Package;

	fn index(&self, index: usize) -> &Self::Output {
		&self.packages[index]
	}
}

#[cfg(test)]
mod tests {
	use super::Packages;
	use std::fs::read_to_string;

	#[test]
	fn packages_chariz() {
		let file = "./test/chariz.packages";
		let data = match read_to_string(file) {
			Ok(data) => data,
			Err(err) => panic!("Failed to read file: {}", err),
		};

		let packages = Packages::from(&data);
		if !packages.errors.is_empty() {
			panic!("Failed to parse packages: {:?}", packages.errors);
		}

		let control = &packages[0];
		assert_eq!(packages.len(), 415);

		assert_eq!(control.package, "arpoison");
		assert_eq!(control.source, None);
		assert_eq!(control.version, "0.7");
		assert_eq!(control.section, Some("System".to_owned()));
		assert_eq!(control.priority, None);
		assert_eq!(control.architecture, "iphoneos-arm");
		assert_eq!(control.is_essential, None);

		assert_eq!(control.depends, Some(vec!["libnet9".to_owned()]));
		assert_eq!(control.pre_depends, None);
		assert_eq!(control.recommends, None);
		assert_eq!(control.suggests, None);
		assert_eq!(control.replaces, None);
		assert_eq!(control.enhances, None);
		assert_eq!(control.breaks, None);
		assert_eq!(control.conflicts, None);

		assert_eq!(control.installed_size, Some(88));
		assert_eq!(
			control.maintainer,
			Some("MidnightChips <midnightchips@gmail.com>".to_owned())
		);
		assert_eq!(
			control.description,
			Some("Generates user-defined ARP packets".to_owned())
		);
		assert_eq!(
			control.homepage,
			Some("http://www.arpoison.net/".to_owned())
		);
		assert_eq!(control.built_using, None);
		assert_eq!(control.package_type, None);
		assert_eq!(
			control.tags,
			Some(vec![
				"role::developer".to_owned(),
				"compatible_min::ios14.0".to_owned(),
			])
		);

		assert_eq!(control.filename, "debs/arpoison_0.7_iphoneos-arm.deb");
		assert_eq!(control.size, 9618);
		assert_eq!(
			control.md5sum,
			Some("e0be09b9f6d1c17371701d0ed6f625bf".to_owned())
		);
		assert_eq!(control.sha1sum, None);
		assert_eq!(
			control.sha256sum,
			Some("9f9f615c50e917e0ce629966899ed28ba78fa637c5de5476aac34f630ab18dd5".to_owned())
		);
		assert_eq!(control.sha512sum, None);
		assert_eq!(control.description_md5sum, None);

		assert_eq!(
			control.get("Depiction"),
			Some("https://chariz.com/get/arpoison")
		);

		assert_eq!(
			control.get("SileoDepiction"),
			Some("https://repo.chariz.com/api/sileo/package/arpoison/depiction.json")
		);

		assert_eq!(
			control.get("Author"),
			Some("MidnightChips <midnightchips@gmail.com>")
		);
	}

	#[test]
	fn packages_jammy() {
		let file = "./test/jammy.packages";
		let data = match read_to_string(file) {
			Ok(data) => data,
			Err(err) => panic!("Failed to read file: {}", err),
		};

		let packages = Packages::from(&data);
		if !packages.errors.is_empty() {
			panic!("Failed to parse packages: {:?}", packages.errors);
		}

		let control = &packages[0];
		assert_eq!(packages.len(), 6132);

		assert_eq!(control.package, "accountsservice");
		assert_eq!(control.source, None);
		assert_eq!(control.version, "0.6.55-3ubuntu2");
		assert_eq!(control.section, Some("gnome".to_owned()));
		assert_eq!(control.priority, Some("optional".to_owned()));
		assert_eq!(control.architecture, "amd64");
		assert_eq!(control.is_essential, None);

		assert_eq!(
			control.depends,
			Some(vec![
				"dbus (>= 1.9.18)".to_owned(),
				"libaccountsservice0 (= 0.6.55-3ubuntu2)".to_owned(),
				"libc6 (>= 2.34)".to_owned(),
				"libglib2.0-0 (>= 2.44)".to_owned(),
				"libpolkit-gobject-1-0 (>= 0.99)".to_owned(),
			])
		);
		assert_eq!(control.pre_depends, None);
		assert_eq!(
			control.recommends,
			Some(vec!["default-logind | logind".to_owned()])
		);
		assert_eq!(
			control.suggests,
			Some(vec!["gnome-control-center".to_owned()])
		);
		assert_eq!(control.replaces, None);
		assert_eq!(control.enhances, None);
		assert_eq!(control.breaks, None);
		assert_eq!(control.conflicts, None);

		assert_eq!(control.installed_size, Some(484));
		assert_eq!(
			control.maintainer,
			Some("Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>".to_owned())
		);
		assert_eq!(
			control.description,
			Some("query and manipulate user account information".to_owned())
		);
		assert_eq!(
			control.homepage,
			Some("https://www.freedesktop.org/wiki/Software/AccountsService/".to_owned())
		);
		assert_eq!(control.built_using, None);
		assert_eq!(control.package_type, None);
		assert_eq!(control.tags, None);

		assert_eq!(
			control.filename,
			"pool/main/a/accountsservice/accountsservice_0.6.55-3ubuntu2_amd64.deb"
		);
		assert_eq!(control.size, 66304);
		assert_eq!(
			control.md5sum,
			Some("d1dc884f3b039c09d9aaa317d6614582".to_owned())
		);
		assert_eq!(
			control.sha1sum,
			Some("f0c2c870146d05b8d53cd805527e942ca793ce38".to_owned())
		);
		assert_eq!(
			control.sha256sum,
			Some("9823e2e330e3ca986440eb5117574c29c1247efc4e8e23cd3b936013dff493b1".to_owned())
		);
		assert_eq!(control.sha512sum, Some("9d816378feaa1cb1135212b416321059b86ee622eccfd3e395b863e5b2ea976244c2b2c016b44f5bf6a30f18cd04406c0193f0da13ca296aac0212975f763bd7".to_owned()));
		assert_eq!(
			control.description_md5sum,
			Some("8aeed0a03c7cd494f0c4b8d977483d7e".to_owned())
		);

		assert_eq!(control.get("Origin"), Some("Ubuntu"));

		assert_eq!(
			control.get("Original-Maintainer"),
			Some("Debian freedesktop.org maintainers <pkg-freedesktop-maintainers@lists.alioth.debian.org>")
		);

		assert_eq!(
			control.get("Bugs"),
			Some("https://bugs.launchpad.net/ubuntu/+filebug")
		);

		assert_eq!(
			control.get("Task"),
			Some("ubuntu-desktop-minimal, ubuntu-desktop, ubuntu-desktop-raspi, kubuntu-desktop, xubuntu-core, xubuntu-desktop, lubuntu-desktop, ubuntustudio-desktop-core, ubuntustudio-desktop, ubuntukylin-desktop, ubuntu-mate-core, ubuntu-mate-desktop, ubuntu-budgie-desktop, ubuntu-budgie-desktop-raspi")
		);
	}
}