1use super::RepositoryError;
17use super::RepositoryType;
18use super::Signature;
19use super::YesNoForce;
20use itertools::Itertools;
21use regex::Regex;
22use std::borrow::Cow;
23use std::collections::HashSet;
24use std::fmt::Display;
25use std::ops::Deref;
26use std::ops::Not;
27use std::path::PathBuf;
28use std::str::FromStr;
29use std::sync::LazyLock;
30use url::Url;
31
32#[derive(Clone, PartialEq, Debug)]
65pub struct LegacyRepository {
66 enabled: bool,
68 pub typ: RepositoryType,
70 pub uri: Url,
72 pub suite: String,
74 pub components: Vec<String>,
77
78 pub architectures: Vec<String>, pub languages: Vec<String>, pub targets: Vec<String>, pub pdiffs: Option<bool>, pub by_hash: Option<YesNoForce>, pub allow_insecure: bool, pub allow_weak: bool, pub allow_downgrade_to_insecure: bool, pub trusted: Option<bool>, pub signature: Option<Signature>, }
100
101impl Default for LegacyRepository {
102 fn default() -> Self {
103 Self {
104 enabled: true,
105 typ: RepositoryType::Binary,
106 uri: "http://nowhere.com".parse().unwrap(),
107 suite: "none".to_string(),
108 components: vec![],
109 architectures: vec![],
110 languages: vec![],
111 targets: vec![],
112 pdiffs: None,
113 by_hash: None,
114 allow_insecure: false,
115 allow_weak: false,
116 allow_downgrade_to_insecure: false,
117 trusted: None,
118 signature: None,
119 }
120 }
121}
122
123impl LegacyRepository {
124 fn assign_option_field(&mut self, key: &str, value: &str) -> Result<(), RepositoryError> {
126 match key {
127 "arch" => self.architectures = value.split(',').map(|s| s.to_string()).collect(),
128 "lang" => self.languages = value.split(',').map(|s| s.to_string()).collect(),
129 "target" => self.targets = value.split(',').map(|s| s.to_string()).collect(),
130 "pdiffs" => self.pdiffs = Some(super::deserialize_yesno(value)?),
131 "by-hash" => self.by_hash = Some(YesNoForce::from_str(value)?),
132 "allow-insecure" => self.allow_insecure = super::deserialize_yesno(value)?, "allow-weak" => self.allow_weak = super::deserialize_yesno(value)?, "allow-downgrade-to-insecure" => {
135 self.allow_downgrade_to_insecure = super::deserialize_yesno(value)?
136 } "trusted" => self.trusted = Some(super::deserialize_yesno(value)?), "signed-by" => self.signature = Some(Signature::KeyPath(PathBuf::from(value))),
139 any => return Err(RepositoryError::UnrecognizedFieldName(any.to_string())),
140 };
141 Ok(())
142 }
143}
144
145#[derive(Debug, Clone, PartialEq)]
147pub struct LegacyRepositories(Vec<LegacyRepository>);
148
149impl LegacyRepositories {
150 pub fn empty() -> Self {
152 Self(Vec::new())
153 }
154
155 pub fn new<Container>(container: Container) -> Self
157 where
158 Container: Into<Vec<LegacyRepository>>,
159 {
160 Self(container.into())
161 }
162
163 pub fn repositories(&self) -> impl Iterator<Item = &LegacyRepository> {
165 self.0.iter()
167 }
168
169 pub fn push(&mut self, repo: LegacyRepository) {
171 self.0.push(repo);
172 }
173
174 pub fn retain<F>(&mut self, f: F)
176 where
177 F: FnMut(&LegacyRepository) -> bool,
178 {
179 self.0.retain(f);
180 }
181
182 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, LegacyRepository> {
184 self.0.iter_mut()
185 }
186
187 pub fn extend<I>(&mut self, iter: I)
189 where
190 I: IntoIterator<Item = LegacyRepository>,
191 {
192 self.0.extend(iter);
193 }
194
195 pub fn is_empty(&self) -> bool {
197 self.0.is_empty()
198 }
199}
200
201static RE: LazyLock<Regex> = LazyLock::new(|| {
202 Regex::new(
203 r"(?xm)^
204 (?P<type>deb|deb-src)\s+ # Catch repository type
205 (\[(?P<options>[^]]*)]\s+)? # Catch options
206 (?P<uri>\S+)\s+ # Catch repository URI
207 (?P<suite>\S+)\s+ # Catch suite/distribution
208 (?P<components>(?:(?P<component>\w+)\s?)+) # Catch components (multiple)
209 $",
210 )
211 .expect("Tested correct regular expression shall not fail!")
212});
213
214impl FromStr for LegacyRepositories {
217 type Err = RepositoryError;
218
219 fn from_str(text: &str) -> Result<Self, Self::Err> {
220 let elements = RE
221 .captures_iter(text)
222 .map(|caps| {
223 let mut repository = LegacyRepository::default();
224 repository.typ = RepositoryType::from_str(&caps["type"])?;
225 let options = caps.name("options").map(|o| o.as_str()).unwrap_or("");
226 options
227 .trim_matches(|c| c == '[' || c == ']')
228 .split_whitespace()
229 .map(|o| {
230 o.splitn(2, '=')
231 .collect_tuple::<(&str, &str)>()
232 .ok_or(RepositoryError::InvalidFormat)
233 })
234 .collect::<Result<Vec<_>, _>>()?
235 .into_iter()
236 .try_for_each(|(k, v)| repository.assign_option_field(k, v))?;
237 repository.uri = Url::from_str(&caps["uri"])?;
238 repository.suite = caps["suite"].to_owned();
239 repository
240 .components
241 .extend(caps["components"].split_whitespace().map(|c| c.to_owned()));
242 <Result<LegacyRepository, Self::Err>>::Ok(repository)
243 })
244 .collect::<Result<Vec<_>, _>>()?;
245 Ok(Self(elements))
246 }
247}
248
249impl Deref for LegacyRepositories {
250 type Target = Vec<LegacyRepository>;
251
252 fn deref(&self) -> &Self::Target {
253 &self.0
254 }
255}
256
257impl From<&LegacyRepository> for super::Repository {
258 fn from(original: &LegacyRepository) -> Self {
259 Self {
260 enabled: Some(original.enabled), types: HashSet::from([original.typ.clone()]),
262 uris: vec![original.uri.clone()],
263 suites: vec![original.suite.clone()],
264 components: original.components.clone().into(),
265 architectures: (!original.architectures.is_empty())
266 .then_some(original.architectures.clone()),
267 languages: (!original.languages.is_empty()).then_some(original.languages.clone()),
268 targets: (!original.targets.is_empty()).then_some(original.targets.clone()),
269 pdiffs: original.pdiffs,
270 by_hash: original.by_hash,
271 allow_insecure: original.allow_insecure.then_some(true),
272 allow_weak: original.allow_weak.then_some(true),
273 allow_downgrade_to_insecure: original.allow_downgrade_to_insecure.then_some(true),
274 trusted: original.trusted,
275 signature: original.signature.clone(),
276 x_repolib_name: None,
277 description: None,
278 }
279 }
280}
281
282impl From<LegacyRepository> for super::Repository {
283 fn from(original: LegacyRepository) -> Self {
284 Self {
285 enabled: Some(original.enabled), types: HashSet::from([original.typ]),
287 uris: vec![original.uri],
288 suites: vec![original.suite],
289 components: original.components.into(),
290 architectures: (!original.architectures.is_empty()).then_some(original.architectures),
291 languages: (!original.languages.is_empty()).then_some(original.languages),
292 targets: (!original.targets.is_empty()).then_some(original.targets),
293 pdiffs: original.pdiffs,
294 by_hash: original.by_hash,
295 allow_insecure: original.allow_insecure.then_some(true),
296 allow_weak: original.allow_weak.then_some(true),
297 allow_downgrade_to_insecure: original.allow_downgrade_to_insecure.then_some(true),
298 trusted: original.trusted,
299 signature: original.signature,
300 x_repolib_name: None,
301 description: None,
302 }
303 }
304}
305
306impl From<&LegacyRepositories> for super::Repositories {
307 fn from(original: &LegacyRepositories) -> Self {
308 Self(original.iter().map(|v| v.into()).collect())
309 }
310}
311
312impl From<LegacyRepositories> for super::Repositories {
313 fn from(original: LegacyRepositories) -> Self {
314 Self(original.0.into_iter().map(|v| v.into()).collect())
315 }
316}
317
318impl From<&super::Repository> for LegacyRepositories {
319 fn from(repo: &super::Repository) -> Self {
322 let mut repos = Vec::new();
323
324 for typ in &repo.types {
325 for uri in &repo.uris {
326 for suite in &repo.suites {
327 repos.push(LegacyRepository {
328 enabled: repo.enabled.unwrap_or(true),
329 typ: typ.clone(),
330 uri: uri.clone(),
331 suite: suite.clone(),
332 components: repo.components.clone().unwrap_or_default(),
333 architectures: repo.architectures.clone().unwrap_or_default(),
334 languages: repo.languages.clone().unwrap_or_default(),
335 targets: repo.targets.clone().unwrap_or_default(),
336 pdiffs: repo.pdiffs,
337 by_hash: repo.by_hash,
338 allow_insecure: repo.allow_insecure.unwrap_or(false),
339 allow_weak: repo.allow_weak.unwrap_or(false),
340 allow_downgrade_to_insecure: repo
341 .allow_downgrade_to_insecure
342 .unwrap_or(false),
343 trusted: repo.trusted,
344 signature: repo.signature.clone(),
345 });
346 }
347 }
348 }
349
350 LegacyRepositories(repos)
351 }
352}
353
354fn option_output<O: AsRef<str> + Display>(name: &str, option: &[O]) -> Cow<'static, str> {
355 if option.is_empty() {
356 Cow::Borrowed("")
357 } else {
358 Cow::Owned(format!("{name}={}", option.iter().join(",")))
359 }
360}
361
362impl Display for LegacyRepository {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 write!(f, "{}", self.typ)?;
365 let options = vec![
368 option_output("arch", &self.architectures),
369 option_output("lang", &self.languages),
370 option_output("target", &self.targets),
371 self.pdiffs
372 .map(|p| Cow::Owned(format!("pdiff={}", if p { "yes" } else { "no" })))
373 .unwrap_or(Cow::Borrowed("")),
374 self.by_hash
375 .map(|p| Cow::Owned(format!("by-hash={p}")))
376 .unwrap_or(Cow::Borrowed("")),
377 if self.allow_insecure {
378 Cow::Owned("allow-insecure=yes".to_string())
379 } else {
380 Cow::Borrowed("")
381 },
382 if self.allow_weak {
383 Cow::Owned("allow-weak=yes".to_string())
384 } else {
385 Cow::Borrowed("")
386 },
387 if self.allow_downgrade_to_insecure {
388 Cow::Owned("allow-downgrade-to-insecure=yes".to_string())
389 } else {
390 Cow::Borrowed("")
391 },
392 self.trusted
393 .map(|t| Cow::Owned(format!("trusted={}", if t { "yes" } else { "no" })))
394 .unwrap_or(Cow::Borrowed("")),
395 self.signature
396 .as_ref()
397 .map(|s| {
398 if let Signature::KeyPath(ref p) = s {
399 Cow::Owned(format!("signed-by={}", p.display()))
400 } else {
401 panic!("Short format not supported!") }
403 })
404 .unwrap_or(Cow::Borrowed("")),
405 ];
406 let options = options.iter().filter(|s| !s.is_empty()).join(" ");
407 options.is_empty().not().then(|| write!(f, " [{options}]"));
408 write!(f, " {}", self.uri)?;
409 write!(f, " {}", self.suite)?;
410 write!(f, " {}", self.components.join(" "))
411 }
412}
413
414impl Display for LegacyRepositories {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 for (i, repo) in self.0.iter().enumerate() {
417 if i > 0 {
418 writeln!(f)?;
419 }
420 write!(f, "{}", repo)?;
421 }
422 Ok(())
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use crate::Repository;
429
430 use super::*;
431 use indoc::indoc;
432
433 const LONG_SAMPLE: &str = indoc!("
434 deb [arch=arm64 signed-by=/usr/share/keyrings/rcn-ee-archive-keyring.gpg] http://debian.beagleboard.org/arm64/ jammy main
435 ");
436 const SHORT_SAMPLE: &str = indoc!(
437 "
438 deb http://archive.ubuntu.com/ubuntu jammy main restricted
439 deb-src http://archive.ubuntu.com/ubuntu jammy main restricted
440 "
441 );
442 const COMMENTED_SAMPLE: &str = indoc!(
443 "
444 deb http://archive.ubuntu.com/ubuntu jammy main restricted
445 # deb-src http://archive.ubuntu.com/ubuntu jammy main restricted
446 "
447 );
448
449 fn golden_sample() -> Repository {
450 Repository {
452 enabled: Some(true), types: HashSet::from([RepositoryType::Binary]),
454 architectures: Some(vec!["arm64".to_owned()]),
455 uris: vec![Url::from_str("http://debian.beagleboard.org/arm64/").unwrap()],
456 suites: vec!["jammy".to_owned()],
457 components: Some(vec!["main".to_owned()]),
458 signature: Some(Signature::KeyPath(PathBuf::from(
459 "/usr/share/keyrings/rcn-ee-archive-keyring.gpg",
460 ))),
461 x_repolib_name: None,
462 languages: None,
463 targets: None,
464 pdiffs: None,
465 ..Default::default()
466 }
467 }
468
469 #[test]
470 fn test_legacy_repositories_from_str() {
471 let repositories = LegacyRepositories::from_str(LONG_SAMPLE)
472 .expect("Shall not fail for correct list entry!");
473
474 assert_eq!(repositories.len(), 1);
475 let repository = repositories.iter().nth(0).unwrap();
476
477 assert_eq!(repository.enabled, true);
478 assert_eq!(repository.typ, RepositoryType::Binary);
479 assert_eq!(repository.architectures, vec!["arm64".to_owned()]);
480 assert_eq!(
481 repository.signature,
482 Some(Signature::KeyPath(PathBuf::from(
483 "/usr/share/keyrings/rcn-ee-archive-keyring.gpg"
484 )))
485 );
486 assert_eq!(repository.typ, RepositoryType::Binary);
487 assert_eq!(
488 repository.uri,
489 "http://debian.beagleboard.org/arm64/"
490 .parse::<Url>()
491 .unwrap()
492 );
493 assert_eq!(repository.suite, "jammy".to_owned());
494 assert_eq!(repository.components, vec!["main".to_owned()]);
495 }
496
497 #[test]
498 fn test_short_legacy_repositories_from_str() {
499 let repositories = LegacyRepositories::from_str(SHORT_SAMPLE)
500 .expect("Shall not fail for correct list entry!");
501
502 assert_eq!(repositories.len(), 2);
503 let bin_repository = repositories.iter().nth(0).unwrap();
504 let src_repository = repositories.iter().nth(1).unwrap();
505
506 assert_eq!(bin_repository.typ, RepositoryType::Binary);
507 assert_eq!(src_repository.typ, RepositoryType::Source);
508 assert_eq!(bin_repository.architectures.len(), 0);
509 assert_eq!(src_repository.architectures.len(), 0);
510 assert_eq!(bin_repository.components.len(), 2);
511 assert_eq!(src_repository.components.len(), 2);
512 }
513
514 #[test]
515 #[ignore = "commented lines support not yet implemented"]
516 fn test_commented_legacy_repositories_from_str() {
517 let repositories = LegacyRepositories::from_str(COMMENTED_SAMPLE)
518 .expect("Shall not fail for correct list entry!");
519
520 assert_eq!(repositories.len(), 2);
521 let bin_repository = repositories.iter().nth(0).unwrap();
522 let src_repository = repositories.iter().nth(1).unwrap();
523
524 assert_eq!(bin_repository.enabled, true);
525 assert_eq!(bin_repository.enabled, false);
526 assert_eq!(bin_repository.typ, RepositoryType::Binary);
527 assert_eq!(src_repository.typ, RepositoryType::Source);
528 assert_eq!(bin_repository.architectures.len(), 0);
529 assert_eq!(src_repository.architectures.len(), 0);
530 assert_eq!(bin_repository.components.len(), 2);
531 assert_eq!(src_repository.components.len(), 2);
532 }
533
534 #[test]
535 fn test_conversion_from_legacy_to_deb822() {
536 let repositories = LegacyRepositories::from_str(LONG_SAMPLE)
537 .expect("Shall not fail for correct list entry!");
538
539 assert_eq!(repositories.len(), 1);
540 let legacy_repository = repositories.iter().nth(0).unwrap();
541
542 let deb822_repository = Repository::from(legacy_repository);
543 let golden_sample = golden_sample();
544
545 assert_eq!(golden_sample, deb822_repository);
546 }
547
548 #[test]
549 fn test_moving_conversion_from_legacy_to_deb822() {
550 let mut repositories = LegacyRepositories::from_str(LONG_SAMPLE)
551 .expect("Shall not fail for correct list entry!");
552
553 assert_eq!(repositories.len(), 1);
554 let legacy_repository = repositories.0.pop().unwrap(); let deb822_repository = Repository::from(legacy_repository);
557 let golden_sample = golden_sample();
558
559 assert_eq!(golden_sample, deb822_repository);
560 }
561
562 #[test]
563 fn test_display_of_simple_legacy_repository() {
564 let sample = LegacyRepository {
565 enabled: true,
566 typ: RepositoryType::Binary,
567 uri: "http://debian.beagleboard.org/arm64/".parse().unwrap(),
568 suite: "jammy".to_string(),
569 components: vec!["main".to_string()],
570 architectures: vec![],
571 languages: vec![],
572 targets: vec![],
573 pdiffs: None,
574 by_hash: None,
575 allow_insecure: false,
576 allow_weak: false,
577 allow_downgrade_to_insecure: false,
578 trusted: None,
579 signature: None,
580 };
581 let list_text = sample.to_string();
582
583 assert_eq!(
584 list_text,
585 "deb http://debian.beagleboard.org/arm64/ jammy main"
586 )
587 }
588
589 #[test]
590 fn test_display_of_legacy_repository_with_options() {
591 let sample = LegacyRepository {
592 enabled: true,
593 typ: RepositoryType::Binary,
594 uri: "http://debian.beagleboard.org/arm64/".parse().unwrap(),
595 suite: "jammy".to_string(),
596 components: vec!["main".to_string()],
597 architectures: vec!["amd64".to_string()],
598 languages: vec![],
599 targets: vec![],
600 pdiffs: None,
601 by_hash: None,
602 allow_insecure: false,
603 allow_weak: false,
604 allow_downgrade_to_insecure: false,
605 trusted: None,
606 signature: Some(Signature::KeyPath(PathBuf::from(
607 "/usr/share/keyrings/rcn-ee-archive-keyring.gpg",
608 ))), };
610 let list_text = sample.to_string();
611
612 assert_eq!(
613 list_text,
614 "deb [arch=amd64 signed-by=/usr/share/keyrings/rcn-ee-archive-keyring.gpg] http://debian.beagleboard.org/arm64/ jammy main"
615 )
616 }
617
618 #[test]
619 fn test_conversion_from_deb822_to_legacy() {
620 use std::collections::HashSet;
621
622 let repo = Repository {
623 enabled: Some(true),
624 types: HashSet::from([RepositoryType::Binary, RepositoryType::Source]),
625 uris: vec!["http://archive.ubuntu.com/ubuntu".parse().unwrap()],
626 suites: vec!["jammy".to_string()],
627 components: Some(vec!["main".to_string(), "universe".to_string()]),
628 architectures: Some(vec!["amd64".to_string()]),
629 ..Default::default()
630 };
631
632 let legacy = LegacyRepositories::from(&repo);
633 assert_eq!(legacy.len(), 2); let legacy_str = legacy.to_string();
636 assert!(legacy_str.contains("deb [arch=amd64]"));
637 assert!(legacy_str.contains("deb-src [arch=amd64]"));
638 assert!(legacy_str.contains("http://archive.ubuntu.com/ubuntu"));
639 assert!(legacy_str.contains("jammy main universe"));
640 }
641
642 #[test]
643 fn test_legacy_repositories_display() {
644 let repos = LegacyRepositories(vec![
645 LegacyRepository {
646 enabled: true,
647 typ: RepositoryType::Binary,
648 uri: "http://example.com/ubuntu".parse().unwrap(),
649 suite: "jammy".to_string(),
650 components: vec!["main".to_string()],
651 ..Default::default()
652 },
653 LegacyRepository {
654 enabled: true,
655 typ: RepositoryType::Source,
656 uri: "http://example.com/ubuntu".parse().unwrap(),
657 suite: "jammy".to_string(),
658 components: vec!["main".to_string()],
659 ..Default::default()
660 },
661 ]);
662
663 let display = repos.to_string();
664 assert_eq!(
665 display,
666 "deb http://example.com/ubuntu jammy main\ndeb-src http://example.com/ubuntu jammy main"
667 );
668 }
669
670 #[test]
671 fn test_allow_downgrade_to_insecure_parsing() {
672 let input = "deb [allow-downgrade-to-insecure=yes] http://example.com/ubuntu jammy main\n";
673 let repos = LegacyRepositories::from_str(input).unwrap();
674 assert_eq!(repos.len(), 1);
675 let repo = repos.iter().nth(0).unwrap();
676 assert!(repo.allow_downgrade_to_insecure);
677 assert!(!repo.allow_weak);
678 }
679
680 #[test]
681 fn test_allow_downgrade_to_insecure_display() {
682 let repo = LegacyRepository {
683 enabled: true,
684 typ: RepositoryType::Binary,
685 uri: "http://example.com/ubuntu".parse().unwrap(),
686 suite: "jammy".to_string(),
687 components: vec!["main".to_string()],
688 allow_downgrade_to_insecure: true,
689 ..Default::default()
690 };
691 let text = repo.to_string();
692 assert_eq!(
693 text,
694 "deb [allow-downgrade-to-insecure=yes] http://example.com/ubuntu jammy main"
695 );
696 }
697
698 #[test]
699 fn test_malformed_option_without_equals() {
700 let input = "deb [badoption] http://example.com/ubuntu jammy main\n";
701 let result = LegacyRepositories::from_str(input);
702 assert!(result.is_err());
703 }
704}