apt_parser/
release.rs

1use crate::{
2	case_map::CaseMap,
3	errors::{APTError, MissingKeyError, ParseError},
4	parse_kv,
5};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq)]
9pub struct ReleaseHash {
10	pub filename: String,
11	pub hash: String,
12	pub size: u64,
13}
14
15pub struct Release {
16	pub(crate) map: CaseMap,
17	pub architectures: Vec<String>,
18	pub no_support_for_architecture_all: Option<bool>,
19	pub description: Option<String>,
20	pub origin: Option<String>,
21	pub label: Option<String>,
22	pub suite: Option<String>,
23	pub version: Option<String>,
24	pub codename: Option<String>,
25	pub date: Option<String>,
26	pub valid_until: Option<String>,
27	pub components: Vec<String>,
28	pub md5sum: Option<Vec<ReleaseHash>>,
29	pub sha1sum: Option<Vec<ReleaseHash>>,
30	pub sha256sum: Option<Vec<ReleaseHash>>,
31	pub sha512sum: Option<Vec<ReleaseHash>>,
32	pub not_automatic: Option<bool>,
33	pub but_automatic_upgrades: Option<bool>,
34	pub acquire_by_hash: Option<bool>,
35	pub signed_by: Option<String>,
36	pub packages_require_authorization: Option<bool>,
37}
38
39impl Release {
40	pub fn from(data: &str) -> Result<Release, APTError> {
41		let map = match parse_kv(data) {
42			Ok(map) => map,
43			Err(err) => return Err(APTError::KVError(err)),
44		};
45
46		let architectures = match map.get("Architectures") {
47			Some(architectures) => architectures
48				.split_whitespace()
49				.map(|x| x.to_string())
50				.collect(),
51			None => {
52				return Err(APTError::MissingKeyError(MissingKeyError::new(
53					"Architectures",
54					data,
55				)))
56			}
57		};
58
59		let components = match map.get("Components") {
60			Some(components) => components
61				.split_whitespace()
62				.map(|x| x.to_string())
63				.collect(),
64			None => {
65				return Err(APTError::MissingKeyError(MissingKeyError::new(
66					"Components",
67					data,
68				)))
69			}
70		};
71
72		let description = map.get("Description").map(|x| {
73			if x.ends_with('\n') {
74				x.trim_end_matches('\n').to_string()
75			} else {
76				x.to_string()
77			}
78		});
79
80		const HASHES: [&str; 4] = ["MD5Sum", "SHA1", "SHA256", "SHA512"];
81		let mut hash_map = HashMap::<String, Vec<ReleaseHash>>::new();
82
83		for (key, value) in map.iter() {
84			if !HASHES.contains(&key.as_str()) {
85				continue;
86			}
87
88			let chunks = value.split_whitespace().collect::<Vec<&str>>();
89			let chunks = chunks.chunks(3);
90
91			let mut hashes = Vec::new();
92			for chunk in chunks {
93				let chunk = chunk.to_vec();
94				if chunk.len() != 3 {
95					return Err(APTError::ParseError(ParseError));
96				}
97
98				let size = match chunk[1].parse::<u64>() {
99					Ok(size) => size,
100					Err(_) => return Err(APTError::ParseError(ParseError)),
101				};
102
103				hashes.push(ReleaseHash {
104					filename: chunk[2].to_string(),
105					hash: chunk[0].to_string(),
106					size,
107				});
108			}
109
110			hash_map.insert(key.to_string(), hashes);
111		}
112
113		Ok(Release {
114			map: map.clone(),
115			architectures,
116			no_support_for_architecture_all: map
117				.get("No-Support-for-Architecture-all")
118				.map(|x| x == "yes"),
119			description,
120			origin: map.get("Origin").cloned(),
121			label: map.get("Label").cloned(),
122			suite: map.get("Suite").cloned(),
123			version: map.get("Version").cloned(),
124			codename: map.get("Codename").cloned(),
125			date: map.get("Date").cloned(),
126			valid_until: map.get("Valid-Until").cloned(),
127			components,
128			md5sum: hash_map.get("MD5Sum").cloned(),
129			sha1sum: hash_map.get("SHA1").cloned(),
130			sha256sum: hash_map.get("SHA256").cloned(),
131			sha512sum: hash_map.get("SHA512").cloned(),
132			not_automatic: map.get("NotAutomatic").map(|x| x == "yes"),
133			but_automatic_upgrades: map.get("ButAutomaticUpgrades").map(|x| x == "yes"),
134			acquire_by_hash: map.get("Acquire-By-Hash").map(|x| x == "yes"),
135			signed_by: map.get("Signed-By").cloned(),
136			packages_require_authorization: map
137				.get("Packages-Require-Authorization")
138				.map(|x| x == "yes"),
139		})
140	}
141
142	pub fn get(&self, key: &str) -> Option<&str> {
143		self.map.get(key).map(|x| &**x)
144	}
145}
146
147#[cfg(test)]
148mod tests {
149	use super::{Release, ReleaseHash};
150	use std::fs::read_to_string;
151
152	#[test]
153	fn release_chariz() {
154		let file = "./test/chariz.release";
155		let data = match read_to_string(file) {
156			Ok(data) => data,
157			Err(err) => panic!("Failed to read file: {}", err),
158		};
159
160		let release = match Release::from(&data) {
161			Ok(release) => release,
162			Err(err) => panic!("Failed to parse release: {}", err),
163		};
164
165		assert_eq!(release.architectures, vec!["iphoneos-arm"]);
166		assert_eq!(release.no_support_for_architecture_all, None);
167		assert_eq!(
168			release.description,
169			Some(
170				"Check out what’s new and download purchases from the Chariz marketplace!"
171					.to_owned()
172			)
173		);
174		assert_eq!(release.origin, Some("Chariz".to_owned()));
175		assert_eq!(release.label, Some("Chariz".to_owned()));
176		assert_eq!(release.suite, Some("stable".to_owned()));
177		assert_eq!(release.version, Some("0.9".to_owned()));
178		assert_eq!(release.codename, Some("hbang".to_owned()));
179		assert_eq!(
180			release.date,
181			Some("Thu, 13 Jan 2022 07:15:42 +0000".to_owned())
182		);
183		assert_eq!(release.valid_until, None);
184		assert_eq!(release.components, vec!["main"]);
185
186		assert_eq!(
187			release.md5sum,
188			Some(vec![
189				ReleaseHash {
190					filename: "Packages".to_owned(),
191					hash: "e95ba4e016983b6145b3de3b535bf5e9".to_owned(),
192					size: 368031,
193				},
194				ReleaseHash {
195					filename: "Packages.bz2".to_owned(),
196					hash: "1c1be6a4f557dc99335cc03c2d2aec3c".to_owned(),
197					size: 41023,
198				},
199				ReleaseHash {
200					filename: "Packages.lzma".to_owned(),
201					hash: "eb1e7b1c68981be1fe4eeefb7a95f393".to_owned(),
202					size: 39736,
203				},
204				ReleaseHash {
205					filename: "Packages.xz".to_owned(),
206					hash: "10ad7b7937ab117be9db77b47c74eaf4".to_owned(),
207					size: 39360,
208				},
209				ReleaseHash {
210					filename: "Packages.zst".to_owned(),
211					hash: "627771b17cc4b50b130cbf5b85f22965".to_owned(),
212					size: 42508,
213				}
214			])
215		);
216
217		assert_eq!(release.sha1sum, None);
218		assert_eq!(release.sha256sum, None);
219
220		assert_eq!(
221			release.sha512sum,
222			Some(vec![
223				ReleaseHash {
224					filename: "Packages".to_owned(),
225					hash: "3b7029624379049caff7181a464841fd823c8ce6a7c41c653fcddaeb3215880c5ef5c33347726a44d76c9fed6e74dd3511f9e53e497fa275db04c907c5c44ed0".to_owned(),
226					size: 368031,
227				},
228				ReleaseHash {
229					filename: "Packages.bz2".to_owned(),
230					hash: "45637f123591db0c8c0483671ec7bbd73c87b8b7c4d03f0968f007a8bf413ed371c965224f2a5652054c0b4605b2766496c7d182a6b81107c032d8daf3eb20d4".to_owned(),
231					size: 41023,
232				},
233				ReleaseHash {
234					filename: "Packages.lzma".to_owned(),
235					hash: "5881f263d9d8dcc99eb8aea1cc95a380d02b1f6b6512b61603f2a63b446b596cd5080f67bbe04f05c6c74c69caebc2d988eedd33d7d616d9aec17253752c4ef8".to_owned(),
236					size: 39736,
237				},
238				ReleaseHash {
239					filename: "Packages.xz".to_owned(),
240					hash: "373d79126d59f28c555f4582d84836b2dd66995f6fd3d4d3c737089c1f9226ae29af5923c4ca59c848451bd7ef1b43e828c7bb96dc448482cbd3aa99a456262b".to_owned(),
241					size: 39360,
242				},
243				ReleaseHash {
244					filename: "Packages.zst".to_owned(),
245					hash: "c858de6a346a1e540f426e9b14ce8680f43dcfe3fa754dc6a0c4f1c4cfb025b82819330176b6847773b38080f370868f387e435c6aeda65ced5eaf242ce4a075".to_owned(),
246					size: 42508,
247				}
248			])
249		);
250
251		assert_eq!(release.not_automatic, None);
252		assert_eq!(release.but_automatic_upgrades, None);
253		assert_eq!(release.acquire_by_hash, None);
254		assert_eq!(release.signed_by, None);
255		assert_eq!(release.packages_require_authorization, None);
256	}
257
258	#[test]
259	fn release_jammy() {
260		let file = "./test/jammy.release";
261		let data = match read_to_string(file) {
262			Ok(data) => data,
263			Err(err) => panic!("Failed to read file: {}", err),
264		};
265
266		let release = match Release::from(&data) {
267			Ok(release) => release,
268			Err(err) => panic!("Failed to parse release: {}", err),
269		};
270
271		assert_eq!(
272			release.architectures,
273			vec!["amd64", "arm64", "armhf", "i386", "ppc64el", "riscv64", "s390x"]
274		);
275		assert_eq!(release.no_support_for_architecture_all, None);
276		assert_eq!(release.description, Some("Ubuntu Jammy 22.04".to_owned()));
277		assert_eq!(release.origin, Some("Ubuntu".to_owned()));
278		assert_eq!(release.label, Some("Ubuntu".to_owned()));
279		assert_eq!(release.suite, Some("jammy".to_owned()));
280		assert_eq!(release.version, Some("22.04".to_owned()));
281		assert_eq!(release.codename, Some("jammy".to_owned()));
282		assert_eq!(
283			release.date,
284			Some("Sat, 15 Jan 2022 22:01:06 UTC".to_owned())
285		);
286		assert_eq!(release.valid_until, None);
287		assert_eq!(
288			release.components,
289			vec!["main", "restricted", "universe", "multiverse"]
290		);
291
292		// Hashes are too long to test
293		assert_eq!(release.not_automatic, None);
294		assert_eq!(release.but_automatic_upgrades, None);
295		assert_eq!(release.acquire_by_hash, Some(true));
296		assert_eq!(release.signed_by, None);
297		assert_eq!(release.packages_require_authorization, None);
298	}
299}