oma_tum/
lib.rs

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 tracing::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                    // v2
202                    '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                    // v1
233                    '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}