1#![deny(missing_docs)]
2use deb822_lossless::{FromDeb822, FromDeb822Paragraph, ToDeb822, ToDeb822Paragraph};
47use signature::Signature;
48use std::{collections::HashSet, ops::Deref, str::FromStr};
49use url::Url;
50use std::result::Result;
51use error::RepositoryError;
52
53pub mod error;
54pub mod signature;
55
56#[derive(PartialEq, Eq, Hash, Debug, Clone)]
59pub enum RepositoryType {
60 Binary,
62 Source
64}
65
66impl FromStr for RepositoryType {
67 type Err = RepositoryError;
68
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 match s {
71 "deb" => Ok(RepositoryType::Binary),
72 "deb-src" => Ok(RepositoryType::Source),
73 _ => Err(RepositoryError::InvalidType)
74 }
75 }
76}
77
78impl From<&RepositoryType> for String {
79 fn from(value: &RepositoryType) -> Self {
80 match value {
81 RepositoryType::Binary => "deb".to_owned(),
82 RepositoryType::Source => "deb-src".to_owned(),
83 }
84 }
85}
86
87impl ToString for RepositoryType {
88 fn to_string(&self) -> String {
89 self.into()
90 }
91}
92
93#[derive(Debug, Clone, PartialEq)]
94pub enum YesNoForce {
96 Yes,
98 No,
100 Force
102}
103
104
105impl FromStr for YesNoForce {
106 type Err = RepositoryError;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 match s {
110 "yes" => Ok(Self::Yes),
111 "no" => Ok(Self::No),
112 "force" => Ok(Self::Force),
113 _ => Err(RepositoryError::InvalidType)
114 }
115 }
116}
117
118impl From<&YesNoForce> for String {
119 fn from(value: &YesNoForce) -> Self {
120 match value {
121 YesNoForce::Yes => "yes".to_owned(),
122 YesNoForce::No => "no".to_owned(),
123 YesNoForce::Force => "force".to_owned()
124 }
125 }
126}
127
128impl ToString for &YesNoForce {
129 fn to_string(&self) -> String {
130 self.to_owned().into()
131 }
132}
133
134fn deserialize_types(text: &str) -> Result<HashSet<RepositoryType>, RepositoryError> {
135 text.split_whitespace()
136 .map(|t| RepositoryType::from_str(t))
137 .collect::<Result<HashSet<RepositoryType>, RepositoryError>>()
138}
139
140fn serialize_types(files: &HashSet<RepositoryType>) -> String {
141 files.into_iter().map(|rt| rt.to_string()).collect::<Vec<String>>().join("\n")
142}
143
144fn deserialize_uris(text: &str) -> Result<Vec<Url>, String> { text.split_whitespace()
146 .map(|u| Url::from_str(u))
147 .collect::<Result<Vec<Url>, _>>()
148 .map_err(|e| e.to_string()) }
150
151fn serialize_uris(uris: &[Url]) -> String {
152 uris.into_iter().map(|u| u.as_str()).collect::<Vec<&str>>().join(" ")
153}
154
155fn deserialize_string_chain(text: &str) -> Result<Vec<String>, String> { Ok(text.split_whitespace()
157 .map(|x| x.to_string())
158 .collect())
159}
160
161fn deserialize_yesno(text: &str) -> Result<bool, String> { match text {
163 "yes" => Ok(true),
164 "no" => Ok(false),
165 _ => Err("Invalid value for yes/no field".to_owned())
166 }
167}
168
169fn serializer_yesno(value: &bool) -> String {
170 if *value {
171 "yes".to_owned()
172 } else {
173 "no".to_owned()
174 }
175}
176
177fn serialize_string_chain(chain: &[String]) -> String {
178 chain.join(" ")
179}
180
181#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Debug, Default)]
217pub struct Repository {
218 #[deb822(field = "Enabled", deserialize_with = deserialize_yesno, serialize_with = serializer_yesno)] enabled: Option<bool>,
221
222 #[deb822(field = "Types", deserialize_with = deserialize_types, serialize_with = serialize_types)]
224 types: HashSet<RepositoryType>, #[deb822(field = "URIs", deserialize_with = deserialize_uris, serialize_with = serialize_uris)]
227 uris: Vec<Url>, #[deb822(field = "Suites", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
230 suites: Vec<String>,
231 #[deb822(field = "Components", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
233 components: Vec<String>,
234
235 #[deb822(field = "Architectures", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
237 architectures: Vec<String>,
238 #[deb822(field = "Languages", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
240 languages: Option<Vec<String>>, #[deb822(field = "Targets", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
243 targets: Option<Vec<String>>,
244 #[deb822(field = "PDiffs", deserialize_with = deserialize_yesno)]
246 pdiffs: Option<bool>,
247 #[deb822(field = "By-Hash")]
249 by_hash: Option<YesNoForce>,
250 #[deb822(field = "Allow-Insecure")]
252 allow_insecure: Option<bool>, #[deb822(field = "Allow-Weak")]
255 allow_weak: Option<bool>, #[deb822(field = "Allow-Downgrade-To-Insecure")]
258 allow_downgrade_to_insecure: Option<bool>, #[deb822(field = "Trusted")]
261 trusted: Option<bool>,
262 #[deb822(field = "Signed-By")]
265 signature: Option<Signature>,
266
267 #[deb822(field = "X-Repolib-Name")]
269 x_repolib_name: Option<String>, #[deb822(field = "Description")]
273 description: Option<String>
274
275 }
277
278impl Repository {
279 pub fn suites(&self) -> &[String] {
281 self.suites.as_slice()
282 }
283
284}
285
286#[derive(Debug)]
288pub struct Repositories(Vec<Repository>);
289
290impl Repositories {
291 pub fn empty() -> Self {
293 Repositories(Vec::new())
294 }
295
296 pub fn new<Container>(container: Container) -> Self
298 where
299 Container: Into<Vec<Repository>>
300 {
301 Repositories(container.into())
302 }
303}
304
305impl std::str::FromStr for Repositories {
306 type Err = String;
307
308 fn from_str(s: &str) -> Result<Self, Self::Err> {
309 let deb822: deb822_lossless::Deb822 = s
310 .parse()
311 .map_err(|e: deb822_lossless::ParseError| e.to_string())?;
312
313 let repos = deb822.paragraphs().map(|p| Repository::from_paragraph(&p)).collect::<Result<Vec<Repository>, Self::Err>>()?;
314 Ok(Repositories(repos))
315 }
316}
317
318impl ToString for Repositories {
319 fn to_string(&self) -> String {
320 self.0.iter()
321 .map(|r| { let p: deb822_lossless::lossy::Paragraph = r.to_paragraph(); p.to_string() })
322 .collect::<Vec<_>>()
323 .join("\n")
324 }
325}
326
327impl Deref for Repositories {
328 type Target = Vec<Repository>;
329
330 fn deref(&self) -> &Self::Target {
331 &self.0
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use std::{collections::HashSet, str::FromStr};
338
339 use indoc::indoc;
340 use url::Url;
341
342 use crate::{signature::Signature, Repositories, Repository, RepositoryType};
343
344 #[test]
345 fn test_not_machine_readable() {
346 let s = indoc!(r#"
347 deb [arch=arm64 signed-by=/usr/share/keyrings/docker.gpg] http://ports.ubuntu.com/ noble stable
348 "#);
349 let ret = s.parse::<Repositories>();
350 assert!(ret.is_err());
351 assert_eq!(ret.unwrap_err(), "expected ':', got Some(NEWLINE)\n".to_owned());
353 }
354
355 #[test]
356 fn test_parse_w_keyblock() {
357 let s = indoc!(r#"
358 Types: deb
359 URIs: http://ports.ubuntu.com/
360 Suites: noble
361 Components: stable
362 Architectures: arm64
363 Signed-By:
364 -----BEGIN PGP PUBLIC KEY BLOCK-----
365 .
366 mDMEY865UxYJKwYBBAHaRw8BAQdAd7Z0srwuhlB6JKFkcf4HU4SSS/xcRfwEQWzr
367 crf6AEq0SURlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEyL2Jvb2t3b3JtKSA8
368 ZGViaWFuLXJlbGVhc2VAbGlzdHMuZGViaWFuLm9yZz6IlgQTFggAPhYhBE1k/sEZ
369 wgKQZ9bnkfjSWFuHg9SBBQJjzrlTAhsDBQkPCZwABQsJCAcCBhUKCQgLAgQWAgMB
370 Ah4BAheAAAoJEPjSWFuHg9SBSgwBAP9qpeO5z1s5m4D4z3TcqDo1wez6DNya27QW
371 WoG/4oBsAQCEN8Z00DXagPHbwrvsY2t9BCsT+PgnSn9biobwX7bDDg==
372 =5NZE
373 -----END PGP PUBLIC KEY BLOCK-----
374 "#);
375
376 let repos = s.parse::<Repositories>().expect("Shall be parsed flawlessly");
377 assert!(repos[0].types.contains(&super::RepositoryType::Binary));
378 assert!(matches!(repos[0].signature, Some(Signature::KeyBlock(_))));
379 }
380
381 #[test]
382 fn test_parse_w_keypath() {
383 let s = indoc!(r#"
384 Types: deb
385 URIs: http://ports.ubuntu.com/
386 Suites: noble
387 Components: stable
388 Architectures: arm64
389 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
390 "#);
391
392 let reps = s.parse::<Repositories>().expect("Shall be parsed flawlessly");
393 assert!(reps[0].types.contains(&super::RepositoryType::Binary));
394 assert!(matches!(reps[0].signature, Some(Signature::KeyPath(_))));
395 }
396
397 #[test]
398 fn test_serialize() {
399 let repos = Repositories::new([
401 Repository {
402 enabled: Some(true), types: HashSet::from([RepositoryType::Binary]),
404 architectures: vec!["arm64".to_owned()],
405 uris: vec![Url::from_str("https://deb.debian.org/debian").unwrap()],
406 suites: vec!["jammy".to_owned()],
407 components: vec!["main". to_owned()],
408 signature: None,
409 x_repolib_name: None,
410 languages: None,
411 targets: None,
412 pdiffs: None,
413 ..Default::default()
414 }
415 ]);
416 let text = repos.to_string();
417 assert_eq!(text, indoc! {r#"
418 Enabled: yes
419 Types: deb
420 URIs: https://deb.debian.org/debian
421 Suites: jammy
422 Components: main
423 Architectures: arm64
424 "#});
425 }
426}