1pub mod parser;
2use std::{
3 fs::{self, read_dir},
4 io,
5 path::{Path, PathBuf},
6};
7
8use ahash::{HashMap, HashSet};
9use debversion::Version;
10use oma_pm_operation_type::{InstallOperation, OmaOperation};
11use serde::Deserialize;
12use snafu::{ResultExt, Snafu, Whatever};
13use spdlog::warn;
14
15use crate::parser::{VersionToken, parse_version_expr};
16
17#[derive(Deserialize, Debug)]
18pub struct TopicUpdateManifest {
19 #[serde(flatten)]
20 pub entries: HashMap<String, TopicUpdateEntry>,
21}
22
23#[inline]
24const fn must_match_all_default() -> bool {
25 true
26}
27
28#[derive(Deserialize, Debug)]
29#[serde(tag = "type")]
30pub enum TopicUpdateEntry {
31 #[serde(rename = "conventional")]
32 Conventional {
33 security: bool,
34 #[serde(default)]
35 packages: HashMap<String, Option<String>>,
36 #[serde(default, rename = "packages-v2")]
37 packages_v2: HashMap<String, Option<String>>,
38 name: HashMap<String, String>,
39 caution: Option<HashMap<String, String>>,
40 #[serde(default = "must_match_all_default")]
41 must_match_all: bool,
42 },
43 #[serde(rename = "cumulative")]
44 Cumulative {
45 name: HashMap<String, String>,
46 caution: Option<HashMap<String, String>>,
47 topics: Vec<String>,
48 #[serde(default)]
49 security: bool,
50 },
51}
52
53#[derive(Debug)]
54pub enum TopicUpdateEntryRef<'a> {
55 Conventional {
56 security: bool,
57 packages: &'a HashMap<String, Option<String>>,
58 packages_v2: &'a HashMap<String, Option<String>>,
59 name: &'a HashMap<String, String>,
60 caution: Option<&'a HashMap<String, String>>,
61 },
62 Cumulative {
63 name: &'a HashMap<String, String>,
64 caution: Option<&'a HashMap<String, String>>,
65 topics: &'a [String],
66 count_packages_changed: usize,
67 security: bool,
68 },
69}
70
71impl TopicUpdateEntryRef<'_> {
72 pub fn is_security(&self) -> bool {
73 match self {
74 TopicUpdateEntryRef::Conventional { security, .. } => *security,
75 TopicUpdateEntryRef::Cumulative { security, .. } => *security,
76 }
77 }
78
79 pub fn count_packages(&self) -> usize {
80 match self {
81 TopicUpdateEntryRef::Conventional {
82 packages,
83 packages_v2,
84 ..
85 } => {
86 if !packages_v2.is_empty() {
87 packages_v2.len()
88 } else {
89 packages.len()
90 }
91 }
92 TopicUpdateEntryRef::Cumulative {
93 count_packages_changed,
94 ..
95 } => *count_packages_changed,
96 }
97 }
98}
99
100impl<'a> From<&'a TopicUpdateEntry> for TopicUpdateEntryRef<'a> {
101 fn from(value: &'a TopicUpdateEntry) -> Self {
102 match value {
103 TopicUpdateEntry::Conventional {
104 security,
105 packages,
106 name,
107 caution,
108 packages_v2,
109 ..
110 } => TopicUpdateEntryRef::Conventional {
111 security: *security,
112 packages,
113 packages_v2,
114 name,
115 caution: caution.as_ref(),
116 },
117 TopicUpdateEntry::Cumulative {
118 name,
119 caution,
120 topics,
121 security,
122 } => TopicUpdateEntryRef::Cumulative {
123 name,
124 caution: caution.as_ref(),
125 topics,
126 count_packages_changed: 0,
127 security: *security,
128 },
129 }
130 }
131}
132
133#[derive(Debug, Snafu)]
134pub enum TumError {
135 #[snafu(display("Failed to read apt list dir"))]
136 ReadAptListDir { source: io::Error },
137 #[snafu(display("Failed to read dir entry"))]
138 ReadDirEntry { source: io::Error },
139 #[snafu(display("Failed to read file: {}", path.display()))]
140 ReadFile { path: PathBuf, source: io::Error },
141}
142
143pub fn get_tum(list_dir: impl AsRef<Path>) -> Result<Vec<TopicUpdateManifest>, TumError> {
144 let mut entries = vec![];
145
146 for i in read_dir(list_dir).context(ReadAptListDirSnafu)? {
147 let i = i.context(ReadDirEntrySnafu)?;
148
149 if i.path()
150 .file_name()
151 .is_some_and(|x| x.to_string_lossy().ends_with("updates.json"))
152 {
153 let f = fs::read(i.path()).context(ReadFileSnafu {
154 path: i.path().to_path_buf(),
155 })?;
156
157 let entry = match parse_single_tum(&f) {
158 Ok(entry) => entry,
159 Err(e) => {
160 warn!("Parse {} got error: {}", i.path().display(), e);
161 continue;
162 }
163 };
164
165 entries.push(entry);
166 }
167 }
168
169 Ok(entries)
170}
171
172pub fn parse_single_tum(bytes: &[u8]) -> Result<TopicUpdateManifest, serde_json::Error> {
173 serde_json::from_slice(bytes)
174}
175
176pub fn get_matches_tum<'a>(
177 tum: &'a [TopicUpdateManifest],
178 op: &OmaOperation,
179) -> HashMap<&'a str, TopicUpdateEntryRef<'a>> {
180 let mut matches = HashMap::with_hasher(ahash::RandomState::new());
181
182 let install_map = &op
183 .install
184 .iter()
185 .filter(|x| *x.op() != InstallOperation::Downgrade)
186 .map(|x| (x.name_without_arch(), (x.old_version(), x.new_version())))
187 .collect::<HashMap<_, _>>();
188
189 let remove_map = &op.remove.iter().map(|x| x.name()).collect::<HashSet<_>>();
190
191 for i in tum {
192 'a: for (name, entry) in &i.entries {
193 if let TopicUpdateEntry::Conventional {
194 must_match_all,
195 packages,
196 packages_v2,
197 ..
198 } = entry
199 {
200 if !packages_v2.is_empty() {
201 'b: for (index, (pkg_name, version)) in packages_v2.iter().enumerate() {
203 let install_pkg_on_topic =
204 match install_pkg_on_topic_v2(install_map, pkg_name, version) {
205 Ok(b) => b,
206 Err(e) => {
207 warn!("{e}");
208 if *must_match_all {
209 continue 'a;
210 } else {
211 continue 'b;
212 }
213 }
214 };
215
216 if !must_match_all
217 && (install_pkg_on_topic
218 || remove_pkg_on_topic(remove_map, pkg_name, version))
219 {
220 break 'b;
221 } else if !install_pkg_on_topic
222 && !remove_pkg_on_topic(remove_map, pkg_name, version)
223 {
224 if *must_match_all || index == packages_v2.len() - 1 {
225 continue 'a;
226 } else {
227 continue 'b;
228 }
229 }
230 }
231 } else if !packages.is_empty() {
232 'b: for (index, (pkg_name, version)) in packages.iter().enumerate() {
234 let install_pkg_on_topic =
235 match install_pkg_on_topic(install_map, pkg_name, version) {
236 Ok(b) => b,
237 Err(e) => {
238 warn!("{e}");
239 if *must_match_all {
240 continue 'a;
241 } else {
242 continue 'b;
243 }
244 }
245 };
246
247 if !must_match_all
248 && (install_pkg_on_topic
249 || remove_pkg_on_topic(remove_map, pkg_name, version))
250 {
251 break 'b;
252 } else if !install_pkg_on_topic
253 && !remove_pkg_on_topic(remove_map, pkg_name, version)
254 {
255 if *must_match_all || index == packages.len() - 1 {
256 continue 'a;
257 } else {
258 continue 'b;
259 }
260 }
261 }
262 }
263
264 matches.insert(name.as_str(), TopicUpdateEntryRef::from(entry));
265 }
266 }
267 }
268
269 for i in tum {
270 for (name, entry) in &i.entries {
271 if let TopicUpdateEntry::Cumulative { topics, .. } = entry
272 && topics.iter().all(|x| matches.contains_key(x.as_str()))
273 {
274 let mut count_packages_changed_tmp = 0;
275
276 for t in topics {
277 let t = matches.remove(t.as_str()).unwrap();
278
279 let TopicUpdateEntryRef::Conventional {
280 packages,
281 packages_v2,
282 ..
283 } = t
284 else {
285 unreachable!()
286 };
287
288 if !packages_v2.is_empty() {
289 count_packages_changed_tmp += packages_v2.len();
290 } else {
291 count_packages_changed_tmp += packages.len();
292 }
293 }
294
295 let mut entry = TopicUpdateEntryRef::from(entry);
296
297 let TopicUpdateEntryRef::Cumulative {
298 count_packages_changed,
299 ..
300 } = &mut entry
301 else {
302 unreachable!()
303 };
304
305 *count_packages_changed = count_packages_changed_tmp;
306 matches.insert(name.as_str(), entry);
307 }
308 }
309 }
310
311 matches
312}
313
314pub fn collection_all_matches_security_tum_pkgs<'a>(
315 matches_tum: &HashMap<&str, TopicUpdateEntryRef<'a>>,
316) -> HashMap<&'a str, &'a Option<String>> {
317 let mut res = HashMap::with_hasher(ahash::RandomState::new());
318 for v in matches_tum.values() {
319 let TopicUpdateEntryRef::Conventional {
320 security,
321 packages,
322 packages_v2,
323 ..
324 } = v
325 else {
326 continue;
327 };
328
329 if !*security {
330 continue;
331 }
332
333 if !packages_v2.is_empty() {
334 res.extend(
335 packages_v2
336 .iter()
337 .map(|(pkg, version)| (pkg.as_str(), version)),
338 );
339 } else {
340 res.extend(
341 packages
342 .iter()
343 .map(|(pkg, version)| (pkg.as_str(), version)),
344 );
345 }
346 }
347
348 res
349}
350
351fn install_pkg_on_topic(
352 install_map: &HashMap<&str, (Option<&str>, &str)>,
353 pkg_name: &str,
354 tum_version: &Option<String>,
355) -> Result<bool, Whatever> {
356 let Some((_, new_version)) = install_map.get(pkg_name) else {
357 return Ok(false);
358 };
359
360 let Some(tum_version) = tum_version else {
361 return Ok(false);
362 };
363
364 compare_version(new_version, tum_version, VersionToken::Eq)
365}
366
367fn compare_version(
368 install_ver: &str,
369 tum_version: &str,
370 op: VersionToken,
371) -> Result<bool, Whatever> {
372 if let Some((prefix, suffix)) = install_ver.rsplit_once("~pre")
373 && is_topic_preversion(suffix)
374 {
375 return compare_version_inner(prefix, tum_version, op);
376 }
377
378 compare_version_inner(install_ver, tum_version, op)
379}
380
381fn compare_version_inner(
382 another_ver: &str,
383 tum_version: &str,
384 op: VersionToken<'_>,
385) -> Result<bool, Whatever> {
386 let another_ver: Version = another_ver.parse().with_whatever_context(|e| {
387 format!("Parse string '{another_ver}' to debversion got error: {e}")
388 })?;
389
390 let tum_version: Version = tum_version.parse().with_whatever_context(|e| {
391 format!("Parse string '{tum_version}' to debversion got error: {e}")
392 })?;
393
394 Ok(match op {
395 VersionToken::Eq | VersionToken::EqEq => tum_version == another_ver,
396 VersionToken::NotEq => tum_version != another_ver,
397 VersionToken::GtEq => another_ver >= tum_version,
398 VersionToken::LtEq => another_ver <= tum_version,
399 VersionToken::Gt => another_ver > tum_version,
400 VersionToken::Lt => another_ver < tum_version,
401 _ => unreachable!(),
402 })
403}
404
405fn install_pkg_on_topic_v2(
406 install_map: &HashMap<&str, (Option<&str>, &str)>,
407 pkg_name: &str,
408 tum_version: &Option<String>,
409) -> Result<bool, Whatever> {
410 let Some((Some(old_version), _)) = install_map.get(pkg_name) else {
411 return Ok(false);
412 };
413
414 let Some(tum_version_expr) = tum_version else {
415 return Ok(false);
416 };
417
418 let tokens = parse_version_expr(tum_version_expr).with_whatever_context(|e| {
419 format!("Parse version expr '{tum_version_expr}' got error: {e}")
420 })?;
421
422 is_right_version(tokens, old_version)
423}
424
425fn is_right_version(tokens: Vec<VersionToken<'_>>, install_ver: &str) -> Result<bool, Whatever> {
426 let tokens: Vec<_> = tokens
427 .into_iter()
428 .map(|x| {
429 if let VersionToken::VersionNumber("$VER") = x {
430 VersionToken::VersionNumber(install_ver)
431 } else {
432 x
433 }
434 })
435 .collect();
436
437 let mut stack = vec![];
438 let mut index = 0;
439
440 while index < tokens.len() {
441 match tokens[index] {
442 VersionToken::VersionNumber(install_ver) => {
443 let VersionToken::VersionNumber(tum_version) = tokens[index + 1] else {
444 unreachable!()
445 };
446
447 let b = compare_version(install_ver, tum_version, tokens[index + 2])?;
448
449 stack.push(b);
450 index += 3;
451 }
452 VersionToken::Or => {
453 let b1 = stack.pop().unwrap();
454 let b2 = stack.pop().unwrap();
455 stack.push(b1 || b2);
456 index += 1;
457 }
458 VersionToken::And => {
459 let b1 = stack.pop().unwrap();
460 let b2 = stack.pop().unwrap();
461 stack.push(b1 && b2);
462 index += 1;
463 }
464 _ => unreachable!(),
465 }
466 }
467
468 assert!(stack.len() == 1);
469
470 Ok(stack[0])
471}
472
473fn is_topic_preversion(suffix: &str) -> bool {
474 if suffix.len() < 16 {
475 return false;
476 }
477
478 for (idx, c) in suffix.chars().enumerate() {
479 if idx == 8 && c != 'T' {
480 return false;
481 } else if idx == 15 {
482 if c != 'Z' {
483 return false;
484 }
485 break;
486 } else if !c.is_ascii_digit() && idx != 8 {
487 return false;
488 }
489 }
490
491 true
492}
493
494fn remove_pkg_on_topic(
495 remove_map: &HashSet<&str>,
496 pkg_name: &str,
497 version: &Option<String>,
498) -> bool {
499 version.is_none() && remove_map.contains(pkg_name)
500}
501
502#[test]
503fn test_is_topic_preversion() {
504 let suffix = "20241213T090405Z";
505 let res = is_topic_preversion(suffix);
506 assert!(res);
507}
508
509#[test]
510fn test_is_right_version() {
511 let input_expr = "(=1.2.3 || =4.5.6) && <7.8.9";
512 let ver1 = "4.5.6";
513 let ver2 = "1.2.3";
514 let tokens = parse_version_expr(input_expr).unwrap();
515 assert!(is_right_version(tokens.clone(), ver1).unwrap());
516 assert!(is_right_version(tokens, ver2).unwrap());
517
518 let input_expr = "<=7.8.9";
519 let ver1 = "1.2.3";
520 let ver2 = "7.8.9";
521 let tokens = parse_version_expr(input_expr).unwrap();
522 assert!(is_right_version(tokens.clone(), ver1).unwrap());
523 assert!(is_right_version(tokens, ver2).unwrap());
524
525 let input_expr = ">=7.8.9";
526 let ver1 = "11.0.0";
527 let ver2 = "7.8.9";
528 let tokens = parse_version_expr(input_expr).unwrap();
529 assert!(is_right_version(tokens.clone(), ver1).unwrap());
530 assert!(is_right_version(tokens, ver2).unwrap());
531
532 let input_expr = "=7.8.9";
533 let ver1 = "7.8.9";
534 let ver2 = "1.2.3";
535 let tokens = parse_version_expr(input_expr).unwrap();
536 assert!(is_right_version(tokens.clone(), ver1).unwrap());
537 assert!(!is_right_version(tokens, ver2).unwrap());
538}