1use std::collections::HashMap;
6
7use anyhow::anyhow;
8use nom::{
9 branch::alt,
10 bytes::complete::tag,
11 character::complete::{alphanumeric0, char as p_char, i32 as p_i32, none_of, one_of, space1},
12 combinator::{all_consuming, opt},
13 error::Error as NError,
14 multi::{many0, many1, separated_list1},
15 sequence::{delimited, preceded, terminated, tuple},
16 Err as NErr, IResult,
17};
18
19use crate::defs::Error;
20
21#[derive(Debug, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum RepoLoc {
25 LocalFile(String),
27
28 Url {
30 url: String,
32
33 suite: String,
35
36 component: String,
38
39 arch: Option<String>,
41 },
42}
43
44#[derive(Debug)]
46enum RepoAttr {
47 Origin(String),
49
50 Release(HashMap<char, String>),
52}
53
54#[derive(Debug)]
56pub struct Repo {
57 prio: i32,
59
60 loc: RepoLoc,
62
63 origin: Option<String>,
65
66 release: HashMap<char, String>,
68}
69
70impl Repo {
71 #[inline]
73 #[must_use]
74 pub const fn prio(&self) -> i32 {
75 self.prio
76 }
77
78 #[inline]
80 #[must_use]
81 pub const fn loc(&self) -> &RepoLoc {
82 &self.loc
83 }
84
85 #[inline]
87 #[must_use]
88 pub fn origin(&self) -> Option<&str> {
89 self.origin.as_deref()
90 }
91
92 #[inline]
94 #[must_use]
95 pub const fn release(&self) -> &HashMap<char, String> {
96 &self.release
97 }
98
99 fn apply_attr(self, attr: RepoAttr) -> Self {
101 match attr {
102 RepoAttr::Origin(origin) => Self {
103 origin: Some(origin),
104 ..self
105 },
106 RepoAttr::Release(attrs) => Self {
107 release: attrs,
108 ..self
109 },
110 }
111 }
112}
113
114#[derive(Debug)]
116pub struct PinnedPackage {
117 name: String,
119
120 version: String,
122
123 prio: i32,
125}
126
127impl PinnedPackage {
128 #[inline]
130 #[must_use]
131 pub const fn prio(&self) -> i32 {
132 self.prio
133 }
134
135 #[inline]
137 #[must_use]
138 pub fn name(&self) -> &str {
139 &self.name
140 }
141
142 #[inline]
144 #[must_use]
145 pub fn version(&self) -> &str {
146 &self.version
147 }
148}
149
150#[derive(Debug)]
152pub struct PackageVersion {
153 installed: bool,
155
156 version: String,
158
159 prio: i32,
161
162 locations: Vec<RepoLoc>,
164}
165
166impl PackageVersion {
167 #[inline]
169 #[must_use]
170 pub const fn installed(&self) -> bool {
171 self.installed
172 }
173
174 #[inline]
176 #[must_use]
177 pub fn version(&self) -> &str {
178 &self.version
179 }
180
181 #[inline]
183 #[must_use]
184 pub const fn prio(&self) -> i32 {
185 self.prio
186 }
187
188 #[inline]
190 #[must_use]
191 pub fn locations(&self) -> &[RepoLoc] {
192 &self.locations
193 }
194}
195
196#[derive(Debug)]
198pub struct PackagePolicy {
199 name: String,
201
202 installed: Option<String>,
204
205 candidate: Option<String>,
207
208 versions: Vec<PackageVersion>,
210}
211
212impl PackagePolicy {
213 #[inline]
215 #[must_use]
216 pub fn name(&self) -> &str {
217 &self.name
218 }
219
220 #[inline]
222 #[must_use]
223 pub fn candidate(&self) -> Option<&str> {
224 self.candidate.as_deref()
225 }
226
227 #[inline]
229 #[must_use]
230 pub fn installed(&self) -> Option<&str> {
231 self.installed.as_deref()
232 }
233
234 #[inline]
236 #[must_use]
237 pub fn versions(&self) -> &[PackageVersion] {
238 &self.versions
239 }
240}
241
242#[derive(Debug)]
244pub struct Policy {
245 repos: Option<Vec<Repo>>,
247
248 pinned: Option<Vec<PinnedPackage>>,
250
251 packages: Option<Vec<PackagePolicy>>,
253}
254
255impl Policy {
256 #[inline]
258 #[must_use]
259 pub fn repos(&self) -> Option<&[Repo]> {
260 self.repos.as_deref()
261 }
262
263 #[inline]
265 #[must_use]
266 pub fn pinned(&self) -> Option<&[PinnedPackage]> {
267 self.pinned.as_deref()
268 }
269
270 #[inline]
272 #[must_use]
273 pub fn packages(&self) -> Option<&[PackagePolicy]> {
274 self.packages.as_deref()
275 }
276}
277
278fn p_loc_file(input: &str) -> IResult<&str, RepoLoc> {
280 let (r_input, path) = preceded(p_char('/'), many1(none_of(" \t\n")))(input)?;
281 Ok((
282 r_input,
283 RepoLoc::LocalFile(format!(
284 "/{path}",
285 path = path.into_iter().collect::<String>()
286 )),
287 ))
288}
289
290fn p_loc_url(input: &str) -> IResult<&str, RepoLoc> {
292 let (r_input, (url, suite, component, arch_opt)) = terminated(
293 tuple((
294 many1(none_of(" \t\n")),
295 preceded(tag(" "), many1(none_of(" \t\n/"))),
296 preceded(tag("/"), many0(none_of(" \t\n"))),
297 opt(preceded(
298 tag(" "),
299 tuple((one_of("abcdefghijklmnopqrstuvwxyz"), alphanumeric0)),
300 )),
301 )),
302 tuple((tag(" "), tag("Packages"))),
303 )(input)?;
304 Ok((
305 r_input,
306 RepoLoc::Url {
307 url: url.into_iter().collect(),
308 suite: suite.into_iter().collect(),
309 component: component.into_iter().collect(),
310 arch: arch_opt.map(|(arch_first, arch_rest)| format!("{arch_first}{arch_rest}")),
311 },
312 ))
313}
314
315fn p_loc(input: &str) -> IResult<&str, RepoLoc> {
317 alt((p_loc_url, p_loc_file))(input)
318}
319
320fn p_repo_line_start(input: &str) -> IResult<&str, Repo> {
322 let (r_input, (prio, loc)) =
323 delimited(space1, tuple((p_i32, preceded(tag(" "), p_loc))), tag("\n"))(input)?;
324 Ok((
325 r_input,
326 Repo {
327 prio,
328 loc,
329 origin: None,
330 release: HashMap::new(),
331 },
332 ))
333}
334
335fn p_repo_line_attr_release_attr(input: &str) -> IResult<&str, (char, String)> {
337 let (r_input, (key, value)) = tuple((
338 one_of("abcdefghijklmnopqrstuvwxyz"),
339 preceded(tag("="), many0(none_of("\n,"))),
340 ))(input)?;
341 Ok((r_input, (key, value.into_iter().collect())))
342}
343
344fn p_repo_line_attr_release(input: &str) -> IResult<&str, RepoAttr> {
346 let (r_input, attrs) = delimited(
347 tag(" release "),
348 separated_list1(tag(","), p_repo_line_attr_release_attr),
349 tag("\n"),
350 )(input)?;
351 Ok((r_input, RepoAttr::Release(attrs.into_iter().collect())))
352}
353
354fn p_repo_line_attr_origin(input: &str) -> IResult<&str, RepoAttr> {
356 let (r_input, origin) = delimited(tag(" origin"), many1(none_of("\n")), tag("\n"))(input)?;
357 Ok((r_input, RepoAttr::Origin(origin.into_iter().collect())))
358}
359
360fn p_repo_line_attr(input: &str) -> IResult<&str, RepoAttr> {
362 alt((p_repo_line_attr_release, p_repo_line_attr_origin))(input)
363}
364
365fn p_repo(input: &str) -> IResult<&str, Repo> {
367 let (r_input, (start, attrs)) = tuple((p_repo_line_start, many0(p_repo_line_attr)))(input)?;
368 Ok((r_input, attrs.into_iter().fold(start, Repo::apply_attr)))
369}
370
371fn p_pinned_pkg(input: &str) -> IResult<&str, PinnedPackage> {
373 let (r_input, (name, version, prio)) = tuple((
374 preceded(tag(" "), many1(none_of(" \t\n"))),
375 preceded(tag(" -> "), many1(none_of(" \t\n"))),
376 delimited(tag(" with priority "), p_i32, tag("\n")),
377 ))(input)?;
378 Ok((
379 r_input,
380 PinnedPackage {
381 name: name.into_iter().collect(),
382 version: version.into_iter().collect(),
383 prio,
384 },
385 ))
386}
387
388fn p_pkg_version(input: &str) -> IResult<&str, PackageVersion> {
390 let (r_input, (installed, version, prio, locations)) = tuple((
391 preceded(tag(" "), alt((tag(" "), tag("***")))),
392 preceded(tag(" "), many1(none_of(" \t\n"))),
393 delimited(tag(" "), p_i32, tag("\n")),
394 many1(delimited(
395 tuple((space1, p_i32, tag(" "))),
396 p_loc,
397 tag("\n"),
398 )),
399 ))(input)?;
400 Ok((
401 r_input,
402 PackageVersion {
403 installed: installed == "***",
404 version: version.into_iter().collect(),
405 prio,
406 locations,
407 },
408 ))
409}
410
411fn p_pkg(input: &str) -> IResult<&str, PackagePolicy> {
413 let (r_input, (name, installed_or_not_v, candidate_or_not_v, versions)) = tuple((
414 terminated(many1(none_of(" \t\n:")), tag(":\n")),
415 delimited(tag(" Installed: "), many1(none_of(" \t\r\n")), tag("\n")),
416 delimited(tag(" Candidate: "), many1(none_of(" \t\r\n")), tag("\n")),
417 preceded(tag(" Version table:\n"), many0(p_pkg_version)),
418 ))(input)?;
419 let installed_or_not: String = installed_or_not_v.into_iter().collect();
420 let candidate_or_not: String = candidate_or_not_v.into_iter().collect();
421 Ok((
422 r_input,
423 PackagePolicy {
424 name: name.into_iter().collect(),
425 installed: (installed_or_not != "(none)").then_some(installed_or_not),
426 candidate: (candidate_or_not != "(none)").then_some(candidate_or_not),
427 versions,
428 },
429 ))
430}
431
432fn p_policy(input: &str) -> IResult<&str, Policy> {
434 let (r_input, (repos, pinned, packages)) = all_consuming(tuple((
435 opt(preceded(tag("Package files:\n"), many1(p_repo))),
436 opt(preceded(tag("Pinned packages:\n"), many0(p_pinned_pkg))),
437 opt(many1(p_pkg)),
438 )))(input)?;
439 Ok((
440 r_input,
441 Policy {
442 repos,
443 pinned,
444 packages,
445 },
446 ))
447}
448
449fn nom_err_to_error(err: NErr<NError<&str>>) -> Error {
451 Error::ParsePolicy(anyhow!("{err}", err = err.map_input(ToOwned::to_owned)))
452}
453
454#[inline]
460pub fn from_str(value: &str) -> Result<Policy, Error> {
461 match p_policy(value) {
462 Ok((_, res)) => Ok(res),
463 Err(err) => Err(nom_err_to_error(err)),
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 #![allow(clippy::panic_in_result_fn)]
470 #![allow(clippy::print_stdout)]
471 #![allow(clippy::unwrap_used)]
472
473 use std::fs;
474
475 use anyhow::{anyhow, Context as _, Result};
476 use rstest::rstest;
477
478 use super::{PackageVersion, RepoLoc};
479
480 const P_LOC_FILE_DPKG_STATUS: &str = "/var/lib/dpkg/status";
481 const P_LOC_FILE_DPKG_STATUS_EXTRA: &str = "/var/lib/dpkg/status Packages";
482 const P_LOC_URL_LOCALHOST: &str = "http://127.0.0.1:9999/debian sid/main amd64 Packages";
483 const P_LOC_URL_SIMPLE: &str = "http://deb.debian.org/debian bullseye/non-free amd64 Packages";
484 const P_LOC_URL_WEIRD: &str = "https://mega.nz/linux/repo/Debian_testing ./ Packages";
485
486 #[rstest]
487 #[case::trivial("small-localhost")]
488 #[case::usual("bullseye")]
489 #[case::complex("large")]
490 #[case::packages("base-files-lintian")]
491 fn test_policy_good(#[case] basename: &str) -> Result<()> {
492 let filename = format!("test-data/policy-{basename}.txt");
493 let contents =
494 fs::read_to_string(&filename).with_context(|| format!("Could not read {filename}"))?;
495 super::from_str(&contents).with_context(|| format!("Could not parse {filename}"))?;
496 Ok(())
497 }
498
499 #[rstest]
500 #[case::file(P_LOC_FILE_DPKG_STATUS)]
501 fn test_loc_file_good(#[case] filename: &str) -> Result<()> {
502 match super::p_loc_file(filename) {
503 Ok(("", RepoLoc::LocalFile(path))) if path == filename => Ok(()),
504 huh => Err(anyhow!("{huh:?}")),
505 }
506 }
507
508 #[rstest]
509 #[case::extra(P_LOC_FILE_DPKG_STATUS_EXTRA)]
510 #[case::url_localhost(P_LOC_URL_LOCALHOST)]
511 #[case::url_simple(P_LOC_URL_SIMPLE)]
512 #[case::url_weird(P_LOC_URL_WEIRD)]
513 fn test_loc_file_bad(#[case] filename: &str) -> Result<()> {
514 match super::p_loc_file(filename) {
515 Ok((r_input, RepoLoc::LocalFile(path))) if format!("{path}{r_input}") == filename => {
516 Ok(())
517 }
518 Ok(huh) => Err(anyhow!("{huh:?}")),
519 Err(_) => Ok(()),
520 }
521 }
522
523 #[rstest]
524 #[case::localhost(P_LOC_URL_LOCALHOST)]
525 #[case::simple(P_LOC_URL_SIMPLE)]
526 #[case::weird(P_LOC_URL_WEIRD)]
527 fn test_loc_url_good(#[case] line: &str) -> Result<()> {
528 match super::p_loc_url(line) {
529 Ok(("", RepoLoc::Url { .. })) => Ok(()),
530 huh => Err(anyhow!("{huh:?}")),
531 }
532 }
533
534 #[rstest]
535 #[case::file(P_LOC_FILE_DPKG_STATUS)]
536 #[case::empty("")]
537 fn test_loc_url_bad(#[case] line: &str) -> Result<()> {
538 super::p_loc_url(line).map_or_else(|_| Ok(()), |huh| Err(anyhow!("{huh:?}")))
539 }
540
541 #[rstest]
542 #[case(false, "3.0a-2", -1_i32)]
543 #[case(true, "3.3a-5", 990_i32)]
544 #[case(false, "13ubuntu8", -1_i32)]
545 fn test_pkg_version_good(
546 #[case] exp_installed: bool,
547 #[case] exp_version: &str,
548 #[case] exp_prio: i32,
549 ) -> Result<()> {
550 let line = format!(
551 " {exp_installed} {exp_version} {exp_prio}\n {exp_prio} {loc}\n",
552 exp_installed = if exp_installed { "***" } else { " " },
553 loc = P_LOC_FILE_DPKG_STATUS
554 );
555 match super::p_pkg_version(&line) {
556 Ok((
557 "",
558 PackageVersion {
559 installed,
560 version,
561 prio,
562 locations,
563 },
564 )) if installed == exp_installed
565 && version == exp_version
566 && prio == exp_prio
567 && locations == [RepoLoc::LocalFile(P_LOC_FILE_DPKG_STATUS.to_owned())] =>
568 {
569 Ok(())
570 }
571 huh => Err(anyhow!("{huh:?}")),
572 }
573 }
574}