apt-parser 1.0.6

A parser for the APT package manager's package lists
Documentation
use crate::{
	case_map::CaseMap,
	errors::{APTError, MissingKeyError, ParseError},
	make_array, parse_kv,
};

pub struct Control {
	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>>,
}

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

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

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

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

		let installed_size = match map.get("Installed-Size") {
			Some(size) => Some(match size.parse::<i64>() {
				Ok(size) => size,
				Err(_) => return Err(APTError::ParseError(ParseError)),
			}),
			None => None,
		};

		Ok(Control {
			map: map.clone(),
			package: package.to_string(),
			source: map.get("Source").cloned(),
			version: version.to_string(),
			section: map.get("Section").cloned(),
			priority: map.get("Priority").cloned(),
			architecture: architecture.to_string(),
			is_essential: map.get("Essential").map(|x| x == "yes"),
			depends: make_array(map.get("Depends")),
			pre_depends: make_array(map.get("Pre-Depends")),
			recommends: make_array(map.get("Recommends")),
			suggests: make_array(map.get("Suggests")),
			replaces: make_array(map.get("Replaces")),
			enhances: make_array(map.get("Enhances")),
			breaks: make_array(map.get("Breaks")),
			conflicts: make_array(map.get("Conflicts")),
			installed_size,
			maintainer: map.get("Maintainer").cloned(),
			description: map.get("Description").cloned(),
			homepage: map.get("Homepage").cloned(),
			built_using: map.get("Built-Using").cloned(),
			package_type: map.get("Package-Type").cloned(),
			tags: make_array(map.get("Tag")),
		})
	}

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

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

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

		let control = match Control::from(&data) {
			Ok(control) => control,
			Err(err) => panic!("Failed to parse control: {}", err),
		};

		assert_eq!(control.package, "clang");
		assert_eq!(control.source, Some("llvm-defaults (0.54)".to_owned()));
		assert_eq!(control.version, "1:13.0-54");
		assert_eq!(control.section, Some("devel".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!["clang-13 (>= 13~)".to_owned()]));
		assert_eq!(control.pre_depends, None);
		assert_eq!(control.recommends, None);
		assert_eq!(control.suggests, None);
		assert_eq!(
			control.replaces,
			Some(vec![
				"clang (<< 3.2-1~exp2)".to_owned(),
				"clang-3.2".to_owned(),
				"clang-3.3".to_owned(),
				"clang-3.4 (<< 1:3.4.2-7~exp1)".to_owned(),
				"clang-3.5 (<< 1:3.5~+rc1-3~exp1)".to_owned(),
			])
		);
		assert_eq!(control.enhances, None);
		assert_eq!(
			control.breaks,
			Some(vec![
				"clang-3.2".to_owned(),
				"clang-3.3".to_owned(),
				"clang-3.4 (<< 1:3.4.2-7~exp1)".to_owned(),
				"clang-3.5 (<< 1:3.5~+rc1-3~exp1)".to_owned(),
			])
		);
		assert_eq!(control.conflicts, None);

		assert_eq!(control.installed_size, Some(24));
		assert_eq!(
			control.maintainer,
			Some("Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>".to_owned())
		);
		assert_eq!(control.description, Some("C, C++ and Objective-C compiler (LLVM based), clang binary\nClang project is a C, C++, Objective C and Objective C++ front-end for the LLVM compiler. Its goal is to offer a replacement to the GNU Compiler Collection (GCC).\n\nClang implements all of the ISO C++ 1998, 11 and 14 standards and also provides most of the support of C++17.\n\nThis is a dependency package providing the default clang compiler.".to_owned()));
		assert_eq!(control.homepage, None);
		assert_eq!(control.built_using, None);
		assert_eq!(control.package_type, None);
		assert_eq!(control.tags, None);

		assert_eq!(
			control.get("Original-Maintainer"),
			Some("LLVM Packaging Team <pkg-llvm-team@lists.alioth.debian.org>")
		);
	}

	#[test]
	fn control_com_amywhile_signalreborn() {
		let file = "./test/com.amywhile.signalreborn.control";
		let data = match read_to_string(file) {
			Ok(data) => data,
			Err(err) => panic!("Failed to read file: {}", err),
		};

		let control = match Control::from(&data) {
			Ok(control) => control,
			Err(err) => panic!("Failed to parse control: {}", err),
		};

		assert_eq!(control.package, "com.amywhile.signalreborn");
		assert_eq!(control.source, None);
		assert_eq!(control.version, "2.2.1-2");
		assert_eq!(control.section, Some("Applications".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!["firmware (>= 12.2) | org.swift.libswift".to_owned()])
		);
		assert_eq!(control.pre_depends, None);
		assert_eq!(control.recommends, None);
		assert_eq!(control.suggests, None);
		assert_eq!(
			control.replaces,
			Some(vec!["com.charliewhile.signalreborn".to_owned()])
		);
		assert_eq!(control.enhances, None);
		assert_eq!(
			control.breaks,
			Some(vec!["com.charliewhile.signalreborn".to_owned()])
		);
		assert_eq!(
			control.conflicts,
			Some(vec!["com.charliewhile.signalreborn".to_owned()])
		);

		assert_eq!(control.installed_size, Some(1536));
		assert_eq!(
			control.maintainer,
			Some("Amy While <support@anamy.gay>".to_owned())
		);
		assert_eq!(
			control.description,
			Some("Visualise your nearby cell towers".to_owned())
		);
		assert_eq!(control.homepage, None);
		assert_eq!(control.built_using, None);
		assert_eq!(control.package_type, None);
		assert_eq!(
			control.tags,
			Some(vec!["compatible_min::ios11.0".to_owned()])
		);

		assert_eq!(control.get("Name"), Some("SignalReborn"));
		assert_eq!(control.get("Author"), Some("Amy While <support@anamy.gay>"));
		assert_eq!(
			control.get("Icon"),
			Some("https://img.chariz.cloud/icon/signal/icon@3x.png")
		);
		assert_eq!(
			control.get("Depiction"),
			Some("https://chariz.com/get/signal")
		);
	}
}