1use {
8 crate::{
9 control::{ControlParagraph, ControlParagraphReader},
10 dependency::{DependencyList, PackageDependencyFields},
11 error::{DebianError, Result},
12 io::ContentDigest,
13 package_version::PackageVersion,
14 repository::release::ChecksumType,
15 },
16 std::{
17 io::BufRead,
18 ops::{Deref, DerefMut},
19 str::FromStr,
20 },
21};
22
23#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct DebianSourceControlFileEntry<'a> {
26 pub filename: &'a str,
28
29 pub digest: ContentDigest,
31
32 pub size: u64,
34}
35
36impl<'a> DebianSourceControlFileEntry<'a> {
37 pub fn as_fetch(&self, directory: &str) -> DebianSourceControlFileFetch {
43 DebianSourceControlFileFetch {
44 path: format!("{}/{}", directory, self.filename),
45 digest: self.digest.clone(),
46 size: self.size,
47 }
48 }
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct DebianSourceControlFilePackage<'a> {
54 pub name: &'a str,
56 pub package_type: &'a str,
58 pub section: &'a str,
60 pub priority: &'a str,
62 pub extra: Vec<&'a str>,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct DebianSourceControlFileFetch {
68 pub path: String,
70
71 pub digest: ContentDigest,
73
74 pub size: u64,
76}
77
78#[derive(Default)]
86pub struct DebianSourceControlFile<'a> {
87 paragraph: ControlParagraph<'a>,
88 signatures: Option<pgp_cleartext::CleartextSignatures>,
90}
91
92impl<'a> Deref for DebianSourceControlFile<'a> {
93 type Target = ControlParagraph<'a>;
94
95 fn deref(&self) -> &Self::Target {
96 &self.paragraph
97 }
98}
99
100impl<'a> DerefMut for DebianSourceControlFile<'a> {
101 fn deref_mut(&mut self) -> &mut Self::Target {
102 &mut self.paragraph
103 }
104}
105
106impl<'a> From<ControlParagraph<'a>> for DebianSourceControlFile<'a> {
107 fn from(paragraph: ControlParagraph<'a>) -> Self {
108 Self {
109 paragraph,
110 signatures: None,
111 }
112 }
113}
114
115impl<'a> From<DebianSourceControlFile<'a>> for ControlParagraph<'a> {
116 fn from(cf: DebianSourceControlFile<'a>) -> Self {
117 cf.paragraph
118 }
119}
120
121impl<'a> DebianSourceControlFile<'a> {
122 pub fn from_reader<R: BufRead>(reader: R) -> Result<Self> {
130 let paragraphs = ControlParagraphReader::new(reader).collect::<Result<Vec<_>>>()?;
131
132 if paragraphs.len() != 1 {
133 return Err(DebianError::DebianSourceControlFileParagraphMismatch(
134 paragraphs.len(),
135 ));
136 }
137
138 let paragraph = paragraphs
139 .into_iter()
140 .next()
141 .expect("validated paragraph count above");
142
143 Ok(Self {
144 paragraph,
145 signatures: None,
146 })
147 }
148
149 pub fn from_armored_reader<R: BufRead>(reader: R) -> Result<Self> {
161 let reader = pgp_cleartext::CleartextSignatureReader::new(reader);
162 let mut reader = std::io::BufReader::new(reader);
163
164 let mut slf = Self::from_reader(&mut reader)?;
165 slf.signatures = Some(reader.into_inner().finalize());
166
167 Ok(slf)
168 }
169
170 #[must_use]
172 pub fn clone_no_signatures(&self) -> Self {
173 Self {
174 paragraph: self.paragraph.clone(),
175 signatures: None,
176 }
177 }
178
179 pub fn signatures(&self) -> Option<&pgp_cleartext::CleartextSignatures> {
181 self.signatures.as_ref()
182 }
183
184 pub fn format(&self) -> Result<&str> {
188 self.required_field_str("Format")
189 }
190
191 pub fn source(&self) -> Result<&str> {
195 self.required_field_str("Source")
196 }
197
198 pub fn binary(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
202 self.iter_field_comma_delimited("Binary")
203 }
204
205 pub fn architecture(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
209 self.iter_field_words("Architecture")
210 }
211
212 pub fn version_str(&self) -> Result<&str> {
216 self.required_field_str("Version")
217 }
218
219 pub fn version(&self) -> Result<PackageVersion> {
223 PackageVersion::parse(self.version_str()?)
224 }
225
226 pub fn maintainer(&self) -> Result<&str> {
230 self.required_field_str("Maintainer")
231 }
232
233 pub fn uploaders(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
237 self.iter_field_comma_delimited("Uploaders")
238 }
239
240 pub fn homepage(&self) -> Option<&str> {
244 self.field_str("Homepage")
245 }
246
247 pub fn testsuite(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
251 self.iter_field_comma_delimited("Testsuite")
252 }
253
254 pub fn dgit(&self) -> Option<&str> {
258 self.field_str("Dgit")
259 }
260
261 pub fn standards_version(&self) -> Result<&str> {
265 self.required_field_str("Standards-Version")
266 }
267
268 pub fn depends(&self) -> Option<Result<DependencyList>> {
270 self.field_dependency_list("Depends")
271 }
272
273 pub fn recommends(&self) -> Option<Result<DependencyList>> {
275 self.field_dependency_list("Recommends")
276 }
277
278 pub fn suggests(&self) -> Option<Result<DependencyList>> {
280 self.field_dependency_list("Suggests")
281 }
282
283 pub fn enhances(&self) -> Option<Result<DependencyList>> {
285 self.field_dependency_list("Enhances")
286 }
287
288 pub fn pre_depends(&self) -> Option<Result<DependencyList>> {
290 self.field_dependency_list("Pre-Depends")
291 }
292
293 pub fn package_dependency_fields(&self) -> Result<PackageDependencyFields> {
295 PackageDependencyFields::from_paragraph(self)
296 }
297
298 pub fn package_list(
302 &self,
303 ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFilePackage<'_>>> + '_)>> {
304 if let Some(iter) = self.iter_field_lines("Package-List") {
305 Some(Box::new(iter.map(move |v| {
306 let mut words = v.split_ascii_whitespace();
307
308 let name = words
309 .next()
310 .ok_or(DebianError::ControlPackageListMissingField("name"))?;
311 let package_type = words
312 .next()
313 .ok_or(DebianError::ControlPackageListMissingField("type"))?;
314 let section = words
315 .next()
316 .ok_or(DebianError::ControlPackageListMissingField("section"))?;
317 let priority = words
318 .next()
319 .ok_or(DebianError::ControlPackageListMissingField("priority"))?;
320 let extra = words.collect::<Vec<_>>();
321
322 Ok(DebianSourceControlFilePackage {
323 name,
324 package_type,
325 section,
326 priority,
327 extra,
328 })
329 })))
330 } else {
331 None
332 }
333 }
334
335 pub fn checksums_sha1(
339 &self,
340 ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
341 self.iter_files("Checksums-Sha1", ChecksumType::Sha1)
342 }
343
344 pub fn checksums_sha256(
348 &self,
349 ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
350 self.iter_files("Checksums-Sha256", ChecksumType::Sha256)
351 }
352
353 pub fn files(
357 &self,
358 ) -> Result<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
359 self.iter_files("Files", ChecksumType::Md5)
360 .ok_or_else(|| DebianError::ControlRequiredFieldMissing("Files".to_string()))
361 }
362
363 fn iter_files(
364 &self,
365 field: &str,
366 checksum: ChecksumType,
367 ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
368 if let Some(iter) = self.iter_field_lines(field) {
369 Some(Box::new(iter.map(move |v| {
370 let mut parts = v.split_ascii_whitespace();
373
374 let digest = parts.next().ok_or(DebianError::ReleaseMissingDigest)?;
375 let size = parts.next().ok_or(DebianError::ReleaseMissingSize)?;
376 let filename = parts.next().ok_or(DebianError::ReleaseMissingPath)?;
377
378 if parts.next().is_some() {
380 return Err(DebianError::ReleasePathWithSpaces(v.to_string()));
381 }
382
383 let digest = ContentDigest::from_hex_digest(checksum, digest)?;
384 let size = u64::from_str(size)?;
385
386 Ok(DebianSourceControlFileEntry {
387 filename,
388 digest,
389 size,
390 })
391 })))
392 } else {
393 None
394 }
395 }
396
397 pub fn file_fetches(
402 &self,
403 checksum: ChecksumType,
404 ) -> Result<Box<(dyn Iterator<Item = Result<DebianSourceControlFileFetch>> + '_)>> {
405 let entries = match checksum {
406 ChecksumType::Md5 => self.files()?,
407 ChecksumType::Sha1 => self.checksums_sha1().ok_or_else(|| {
408 DebianError::ControlRequiredFieldMissing("Checksums-Sha1".to_string())
409 })?,
410 ChecksumType::Sha256 => self.checksums_sha256().ok_or_else(|| {
411 DebianError::ControlRequiredFieldMissing("Checksums-Sha256".to_string())
412 })?,
413 };
414
415 Ok(Box::new(entries.map(move |entry| {
416 let entry = entry?;
417 let directory = self.required_field_str("Directory")?;
418
419 Ok(entry.as_fetch(directory))
420 })))
421 }
422}
423
424#[cfg(test)]
425mod test {
426 use super::*;
427
428 const ZSTD_DSC: &[u8] = include_bytes!("testdata/libzstd_1.4.8+dfsg-3.dsc");
429
430 #[test]
431 fn parse_cleartext_armored() -> Result<()> {
432 let cf = DebianSourceControlFile::from_armored_reader(std::io::Cursor::new(ZSTD_DSC))?;
433
434 cf.signatures()
435 .expect("PGP signatures should have been parsed");
436
437 assert_eq!(cf.format()?, "3.0 (quilt)");
438 assert_eq!(cf.source()?, "libzstd");
439 assert_eq!(
440 cf.binary().unwrap().collect::<Vec<_>>(),
441 vec!["libzstd-dev", "libzstd1", "zstd", "libzstd1-udeb"]
442 );
443 assert_eq!(cf.architecture().unwrap().collect::<Vec<_>>(), vec!["any"]);
444 assert_eq!(cf.version_str()?, "1.4.8+dfsg-3");
445 assert_eq!(
446 cf.maintainer()?,
447 "Debian Med Packaging Team <debian-med-packaging@lists.alioth.debian.org>"
448 );
449 assert_eq!(
450 cf.uploaders().unwrap().collect::<Vec<_>>(),
451 vec![
452 "Kevin Murray <kdmfoss@gmail.com>",
453 "Olivier Sallou <osallou@debian.org>",
454 "Alexandre Mestiashvili <mestia@debian.org>",
455 ]
456 );
457 assert_eq!(cf.homepage(), Some("https://github.com/facebook/zstd"));
458 assert_eq!(cf.standards_version()?, "4.6.0");
459 assert_eq!(
460 cf.testsuite().unwrap().collect::<Vec<_>>(),
461 vec!["autopkgtest"]
462 );
463 assert_eq!(
464 cf.package_list().unwrap().collect::<Result<Vec<_>>>()?,
465 vec![
466 DebianSourceControlFilePackage {
467 name: "libzstd-dev",
468 package_type: "deb",
469 section: "libdevel",
470 priority: "optional",
471 extra: vec!["arch=any"]
472 },
473 DebianSourceControlFilePackage {
474 name: "libzstd1",
475 package_type: "deb",
476 section: "libs",
477 priority: "optional",
478 extra: vec!["arch=any"]
479 },
480 DebianSourceControlFilePackage {
481 name: "libzstd1-udeb",
482 package_type: "udeb",
483 section: "debian-installer",
484 priority: "optional",
485 extra: vec!["arch=any"]
486 },
487 DebianSourceControlFilePackage {
488 name: "zstd",
489 package_type: "deb",
490 section: "utils",
491 priority: "optional",
492 extra: vec!["arch=any"]
493 }
494 ]
495 );
496 assert_eq!(
497 cf.checksums_sha1().unwrap().collect::<Result<Vec<_>>>()?,
498 vec![
499 DebianSourceControlFileEntry {
500 filename: "libzstd_1.4.8+dfsg.orig.tar.xz",
501 digest: ContentDigest::sha1_hex("a24e4ccf9fc356aeaaa0783316a26bd65817c354")?,
502 size: 1331996,
503 },
504 DebianSourceControlFileEntry {
505 filename: "libzstd_1.4.8+dfsg-3.debian.tar.xz",
506 digest: ContentDigest::sha1_hex("896a47a2934d0fcf9faa8397d05a12b932697d1f")?,
507 size: 12184,
508 }
509 ]
510 );
511 assert_eq!(
512 cf.checksums_sha256().unwrap().collect::<Result<Vec<_>>>()?,
513 vec![
514 DebianSourceControlFileEntry {
515 filename: "libzstd_1.4.8+dfsg.orig.tar.xz",
516 digest: ContentDigest::sha256_hex(
517 "1e8ce5c4880a6d5bd8d3186e4186607dd19b64fc98a3877fc13aeefd566d67c5"
518 )?,
519 size: 1331996,
520 },
521 DebianSourceControlFileEntry {
522 filename: "libzstd_1.4.8+dfsg-3.debian.tar.xz",
523 digest: ContentDigest::sha256_hex(
524 "fecd87a469d5a07b6deeeef53ed24b2f1a74ee097ce11528fe3b58540f05c147"
525 )?,
526 size: 12184,
527 }
528 ]
529 );
530 assert_eq!(
531 cf.files().unwrap().collect::<Result<Vec<_>>>()?,
532 vec![
533 DebianSourceControlFileEntry {
534 filename: "libzstd_1.4.8+dfsg.orig.tar.xz",
535 digest: ContentDigest::md5_hex("943bed8b8d98a50c8d8a101b12693bb4")?,
536 size: 1331996,
537 },
538 DebianSourceControlFileEntry {
539 filename: "libzstd_1.4.8+dfsg-3.debian.tar.xz",
540 digest: ContentDigest::md5_hex("4d2692830e1f481ce769e2dd24cbc9db")?,
541 size: 12184,
542 }
543 ]
544 );
545
546 Ok(())
547 }
548}