1use crate::{
2 case_map::CaseMap,
3 control::Control,
4 errors::{APTError, MissingKeyError},
5};
6use rayon::prelude::*;
7use std::ops::Index;
8
9pub struct Package {
10 pub(crate) map: CaseMap,
11 pub package: String,
12 pub source: Option<String>,
13 pub version: String,
14 pub section: Option<String>,
15 pub priority: Option<String>,
16 pub architecture: String,
17 pub is_essential: Option<bool>,
18 pub depends: Option<Vec<String>>,
19 pub pre_depends: Option<Vec<String>>,
20 pub recommends: Option<Vec<String>>,
21 pub suggests: Option<Vec<String>>,
22 pub replaces: Option<Vec<String>>,
23 pub enhances: Option<Vec<String>>,
24 pub breaks: Option<Vec<String>>,
25 pub conflicts: Option<Vec<String>>,
26 pub installed_size: Option<i64>,
27 pub maintainer: Option<String>,
28 pub description: Option<String>,
29 pub homepage: Option<String>,
30 pub built_using: Option<String>,
31 pub package_type: Option<String>,
32 pub tags: Option<Vec<String>>,
33 pub filename: String,
34 pub size: i64,
35 pub md5sum: Option<String>,
36 pub sha1sum: Option<String>,
37 pub sha256sum: Option<String>,
38 pub sha512sum: Option<String>,
39 pub description_md5sum: Option<String>,
40}
41
42impl Package {
43 pub fn from(data: &str) -> Result<Package, APTError> {
44 let control = match Control::from(data) {
45 Ok(control) => control,
46 Err(err) => return Err(err),
47 };
48
49 let map = control.map;
50
51 let filename = match map.get("Filename") {
52 Some(filename) => filename.to_owned(),
53 None => {
54 return Err(APTError::MissingKeyError(MissingKeyError::new(
55 "Filename", data,
56 )))
57 }
58 };
59
60 let size = match map.get("Size") {
61 Some(size) => size.parse::<i64>().unwrap_or(-1),
62 None => {
63 return Err(APTError::MissingKeyError(MissingKeyError::new(
64 "Size", data,
65 )))
66 }
67 };
68
69 Ok(Package {
70 map: map.clone(),
71 package: control.package,
72 source: control.source,
73 version: control.version,
74 section: control.section,
75 priority: control.priority,
76 architecture: control.architecture,
77 is_essential: control.is_essential,
78 depends: control.depends,
79 pre_depends: control.pre_depends,
80 recommends: control.recommends,
81 suggests: control.suggests,
82 replaces: control.replaces,
83 enhances: control.enhances,
84 breaks: control.breaks,
85 conflicts: control.conflicts,
86 installed_size: control.installed_size,
87 maintainer: control.maintainer,
88 description: control.description,
89 homepage: control.homepage,
90 built_using: control.built_using,
91 package_type: control.package_type,
92 tags: control.tags,
93 filename,
94 size,
95 md5sum: map.get("MD5Sum").cloned(),
96 sha1sum: map.get("SHA1").cloned(),
97 sha256sum: map.get("SHA256").cloned(),
98 sha512sum: map.get("SHA512").cloned(),
99 description_md5sum: map.get("Description-md5").cloned(),
100 })
101 }
102
103 pub fn get(&self, key: &str) -> Option<&str> {
104 self.map.get(key).map(|x| &**x)
105 }
106}
107
108pub struct Packages {
109 pub(crate) packages: Vec<Package>,
110 pub errors: Vec<APTError>,
111}
112
113impl Packages {
114 pub fn from(data: &str) -> Packages {
115 let binding = data.replace("\r\n", "\n").replace('\0', "");
116 let iter = binding.trim().split("\n\n").par_bridge().into_par_iter();
117
118 let values = iter
119 .map(|package| Package::from(&package))
120 .collect::<Vec<Result<Package, APTError>>>();
121
122 let mut packages = Vec::new();
123 let mut errors = Vec::new();
124
125 for value in values {
126 match value {
127 Ok(package) => packages.push(package),
128 Err(err) => errors.push(err),
129 }
130 }
131
132 Packages { packages, errors }
133 }
134
135 pub fn len(&self) -> usize {
136 self.packages.len()
137 }
138}
139
140impl Iterator for Packages {
141 type Item = Package;
142
143 fn next(&mut self) -> Option<Self::Item> {
144 self.packages.pop()
145 }
146}
147
148impl Index<usize> for Packages {
149 type Output = Package;
150
151 fn index(&self, index: usize) -> &Self::Output {
152 &self.packages[index]
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::Packages;
159 use std::fs::read_to_string;
160
161 #[test]
162 fn packages_chariz() {
163 let file = "./test/chariz.packages";
164 let data = match read_to_string(file) {
165 Ok(data) => data,
166 Err(err) => panic!("Failed to read file: {}", err),
167 };
168
169 let packages = Packages::from(&data);
170 if !packages.errors.is_empty() {
171 panic!("Failed to parse packages: {:?}", packages.errors);
172 }
173
174 let control = &packages[0];
175 assert_eq!(packages.len(), 415);
176
177 assert_eq!(control.package, "arpoison");
178 assert_eq!(control.source, None);
179 assert_eq!(control.version, "0.7");
180 assert_eq!(control.section, Some("System".to_owned()));
181 assert_eq!(control.priority, None);
182 assert_eq!(control.architecture, "iphoneos-arm");
183 assert_eq!(control.is_essential, None);
184
185 assert_eq!(control.depends, Some(vec!["libnet9".to_owned()]));
186 assert_eq!(control.pre_depends, None);
187 assert_eq!(control.recommends, None);
188 assert_eq!(control.suggests, None);
189 assert_eq!(control.replaces, None);
190 assert_eq!(control.enhances, None);
191 assert_eq!(control.breaks, None);
192 assert_eq!(control.conflicts, None);
193
194 assert_eq!(control.installed_size, Some(88));
195 assert_eq!(
196 control.maintainer,
197 Some("MidnightChips <midnightchips@gmail.com>".to_owned())
198 );
199 assert_eq!(
200 control.description,
201 Some("Generates user-defined ARP packets".to_owned())
202 );
203 assert_eq!(
204 control.homepage,
205 Some("http://www.arpoison.net/".to_owned())
206 );
207 assert_eq!(control.built_using, None);
208 assert_eq!(control.package_type, None);
209 assert_eq!(
210 control.tags,
211 Some(vec![
212 "role::developer".to_owned(),
213 "compatible_min::ios14.0".to_owned(),
214 ])
215 );
216
217 assert_eq!(control.filename, "debs/arpoison_0.7_iphoneos-arm.deb");
218 assert_eq!(control.size, 9618);
219 assert_eq!(
220 control.md5sum,
221 Some("e0be09b9f6d1c17371701d0ed6f625bf".to_owned())
222 );
223 assert_eq!(control.sha1sum, None);
224 assert_eq!(
225 control.sha256sum,
226 Some("9f9f615c50e917e0ce629966899ed28ba78fa637c5de5476aac34f630ab18dd5".to_owned())
227 );
228 assert_eq!(control.sha512sum, None);
229 assert_eq!(control.description_md5sum, None);
230
231 assert_eq!(
232 control.get("Depiction"),
233 Some("https://chariz.com/get/arpoison")
234 );
235
236 assert_eq!(
237 control.get("SileoDepiction"),
238 Some("https://repo.chariz.com/api/sileo/package/arpoison/depiction.json")
239 );
240
241 assert_eq!(
242 control.get("Author"),
243 Some("MidnightChips <midnightchips@gmail.com>")
244 );
245 }
246
247 #[test]
248 fn packages_jammy() {
249 let file = "./test/jammy.packages";
250 let data = match read_to_string(file) {
251 Ok(data) => data,
252 Err(err) => panic!("Failed to read file: {}", err),
253 };
254
255 let packages = Packages::from(&data);
256 if !packages.errors.is_empty() {
257 panic!("Failed to parse packages: {:?}", packages.errors);
258 }
259
260 let control = &packages[0];
261 assert_eq!(packages.len(), 6132);
262
263 assert_eq!(control.package, "accountsservice");
264 assert_eq!(control.source, None);
265 assert_eq!(control.version, "0.6.55-3ubuntu2");
266 assert_eq!(control.section, Some("gnome".to_owned()));
267 assert_eq!(control.priority, Some("optional".to_owned()));
268 assert_eq!(control.architecture, "amd64");
269 assert_eq!(control.is_essential, None);
270
271 assert_eq!(
272 control.depends,
273 Some(vec![
274 "dbus (>= 1.9.18)".to_owned(),
275 "libaccountsservice0 (= 0.6.55-3ubuntu2)".to_owned(),
276 "libc6 (>= 2.34)".to_owned(),
277 "libglib2.0-0 (>= 2.44)".to_owned(),
278 "libpolkit-gobject-1-0 (>= 0.99)".to_owned(),
279 ])
280 );
281 assert_eq!(control.pre_depends, None);
282 assert_eq!(
283 control.recommends,
284 Some(vec!["default-logind | logind".to_owned()])
285 );
286 assert_eq!(
287 control.suggests,
288 Some(vec!["gnome-control-center".to_owned()])
289 );
290 assert_eq!(control.replaces, None);
291 assert_eq!(control.enhances, None);
292 assert_eq!(control.breaks, None);
293 assert_eq!(control.conflicts, None);
294
295 assert_eq!(control.installed_size, Some(484));
296 assert_eq!(
297 control.maintainer,
298 Some("Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>".to_owned())
299 );
300 assert_eq!(
301 control.description,
302 Some("query and manipulate user account information".to_owned())
303 );
304 assert_eq!(
305 control.homepage,
306 Some("https://www.freedesktop.org/wiki/Software/AccountsService/".to_owned())
307 );
308 assert_eq!(control.built_using, None);
309 assert_eq!(control.package_type, None);
310 assert_eq!(control.tags, None);
311
312 assert_eq!(
313 control.filename,
314 "pool/main/a/accountsservice/accountsservice_0.6.55-3ubuntu2_amd64.deb"
315 );
316 assert_eq!(control.size, 66304);
317 assert_eq!(
318 control.md5sum,
319 Some("d1dc884f3b039c09d9aaa317d6614582".to_owned())
320 );
321 assert_eq!(
322 control.sha1sum,
323 Some("f0c2c870146d05b8d53cd805527e942ca793ce38".to_owned())
324 );
325 assert_eq!(
326 control.sha256sum,
327 Some("9823e2e330e3ca986440eb5117574c29c1247efc4e8e23cd3b936013dff493b1".to_owned())
328 );
329 assert_eq!(control.sha512sum, Some("9d816378feaa1cb1135212b416321059b86ee622eccfd3e395b863e5b2ea976244c2b2c016b44f5bf6a30f18cd04406c0193f0da13ca296aac0212975f763bd7".to_owned()));
330 assert_eq!(
331 control.description_md5sum,
332 Some("8aeed0a03c7cd494f0c4b8d977483d7e".to_owned())
333 );
334
335 assert_eq!(control.get("Origin"), Some("Ubuntu"));
336
337 assert_eq!(
338 control.get("Original-Maintainer"),
339 Some("Debian freedesktop.org maintainers <pkg-freedesktop-maintainers@lists.alioth.debian.org>")
340 );
341
342 assert_eq!(
343 control.get("Bugs"),
344 Some("https://bugs.launchpad.net/ubuntu/+filebug")
345 );
346
347 assert_eq!(
348 control.get("Task"),
349 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")
350 );
351 }
352}