1 use std
::collections
::HashMap
;
3 use anyhow
::{bail, format_err, Error}
;
4 use rfc822_like
::de
::Deserializer
;
5 use serde
::Deserialize
;
10 #[derive(Debug, Deserialize)]
11 #[serde(rename_all = "PascalCase")]
12 pub struct ReleaseFileRaw
{
13 pub architectures
: Option
<String
>,
14 pub changelogs
: Option
<String
>,
15 pub codename
: Option
<String
>,
16 pub components
: Option
<String
>,
17 pub date
: Option
<String
>,
18 pub description
: Option
<String
>,
19 pub label
: Option
<String
>,
20 pub origin
: Option
<String
>,
21 pub suite
: Option
<String
>,
22 pub version
: Option
<String
>,
24 #[serde(rename = "MD5Sum")]
25 pub md5_sum
: Option
<String
>,
26 #[serde(rename = "SHA1")]
27 pub sha1
: Option
<String
>,
28 #[serde(rename = "SHA256")]
29 pub sha256
: Option
<String
>,
30 #[serde(rename = "SHA512")]
31 pub sha512
: Option
<String
>,
34 pub extra_fields
: HashMap
<String
, Value
>,
37 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
38 pub enum CompressionType
{
45 pub type Architecture
= String
;
46 pub type Component
= String
;
48 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
49 /// Type of file reference extraced from path.
51 /// `Packages` and `Sources` will contain further reference to binary or source package files.
52 /// These are handled in `PackagesFile` and `SourcesFile` respectively.
53 pub enum FileReferenceType
{
54 /// A `Commands` index listing command to package mappings
55 Commands(Architecture
, Option
<CompressionType
>),
56 /// A `Contents` index listing contents of binary packages
57 Contents(Architecture
, Option
<CompressionType
>),
58 /// A `Contents` index listing contents of binary udeb packages
59 ContentsUdeb(Architecture
, Option
<CompressionType
>),
60 /// A DEP11 `Components` metadata file or `icons` archive
61 Dep11(Option
<CompressionType
>),
62 /// Referenced files which are not really part of the APT repository but only signed for trust-anchor reasons
66 /// A `Packages` index listing binary package metadata and references
67 Packages(Architecture
, Option
<CompressionType
>),
68 /// A compat `Release` file with no relevant content
69 PseudoRelease(Option
<Architecture
>),
70 /// A `Sources` index listing source package metadata and references
71 Sources(Option
<CompressionType
>),
72 /// A `Translation` file
73 Translation(Option
<CompressionType
>),
74 /// Unknown file reference
78 impl FileReferenceType
{
79 fn match_compression(value
: &str) -> Result
<Option
<CompressionType
>, Error
> {
84 let value
= if let Some(stripped
) = value
.strip_prefix('
.'
) {
91 "bz2" => Ok(Some(CompressionType
::Bzip2
)),
92 "gz" => Ok(Some(CompressionType
::Gzip
)),
93 "lzma" => Ok(Some(CompressionType
::Lzma
)),
94 "xz" => Ok(Some(CompressionType
::Xz
)),
95 other
=> bail
!("Unexpected file extension '{other}'."),
99 pub fn parse(component
: &str, path
: &str) -> Result
<FileReferenceType
, Error
> {
100 // everything referenced in a release file should be component-specific
102 .strip_prefix(&format
!("{component}/"))
103 .ok_or_else(|| format_err
!("Doesn't start with component '{component}'"))?
;
105 let parse_binary_dir
=
106 |file_name
: &str, arch
: &str| parse_binary_dir(file_name
, arch
, path
);
108 if let Some((dir
, rest
)) = rest
.split_once('
/'
) {
109 // reference into another subdir
112 // Sources or compat-Release
113 if let Some((dir
, _rest
)) = rest
.split_once('
/'
) {
114 if dir
== "Sources.diff" {
115 Ok(FileReferenceType
::PDiff
)
117 Ok(FileReferenceType
::Unknown
)
119 } else if rest
== "Release" {
120 Ok(FileReferenceType
::PseudoRelease(None
))
121 } else if let Some(ext
) = rest
.strip_prefix("Sources") {
122 let comp
= FileReferenceType
::match_compression(ext
)?
;
123 Ok(FileReferenceType
::Sources(comp
))
125 Ok(FileReferenceType
::Unknown
)
129 if let Some(rest
) = rest
.strip_prefix("Commands-") {
130 if let Some((arch
, ext
)) = rest
.rsplit_once('
.'
) {
131 Ok(FileReferenceType
::Commands(
133 FileReferenceType
::match_compression(ext
).ok().flatten(),
136 Ok(FileReferenceType
::Commands(rest
.to_owned(), None
))
139 Ok(FileReferenceType
::Unknown
)
143 if let Some((_path
, ext
)) = rest
.rsplit_once('
.'
) {
144 Ok(FileReferenceType
::Dep11(
145 FileReferenceType
::match_compression(ext
).ok().flatten(),
148 Ok(FileReferenceType
::Dep11(None
))
151 "debian-installer" => {
152 // another layer, then like regular repo but pointing at udebs
153 if let Some((dir
, rest
)) = rest
.split_once('
/'
) {
154 if let Some(arch
) = dir
.strip_prefix("binary-") {
155 // Packages or compat-Release
156 return parse_binary_dir(rest
, arch
);
161 Ok(FileReferenceType
::Unknown
)
164 if let Some((dir
, _rest
)) = rest
.split_once('
/'
) {
165 if dir
.starts_with("Translation") && dir
.ends_with(".diff") {
166 Ok(FileReferenceType
::PDiff
)
168 Ok(FileReferenceType
::Unknown
)
170 } else if let Some((_
, ext
)) = rest
.split_once('
.'
) {
171 Ok(FileReferenceType
::Translation(
172 FileReferenceType
::match_compression(ext
)?
,
175 Ok(FileReferenceType
::Translation(None
))
179 if let Some(arch
) = dir
.strip_prefix("binary-") {
180 // Packages or compat-Release
181 parse_binary_dir(rest
, arch
)
182 } else if let Some(_arch
) = dir
.strip_prefix("installer-") {
183 // netboot installer checksum files
184 Ok(FileReferenceType
::Ignored
)
187 Ok(FileReferenceType
::Unknown
)
191 } else if let Some(rest
) = rest
.strip_prefix("Contents-") {
192 // reference to a top-level file - Contents-*
193 let (rest
, udeb
) = if let Some(rest
) = rest
.strip_prefix("udeb-") {
198 let (arch
, comp
) = match rest
.split_once('
.'
) {
199 Some((arch
, comp_str
)) => (
201 FileReferenceType
::match_compression(comp_str
)?
,
203 None
=> (rest
.to_owned(), None
),
206 Ok(FileReferenceType
::ContentsUdeb(arch
, comp
))
208 Ok(FileReferenceType
::Contents(arch
, comp
))
211 Ok(FileReferenceType
::Unknown
)
215 pub fn compression(&self) -> Option
<CompressionType
> {
217 FileReferenceType
::Commands(_
, comp
)
218 | FileReferenceType
::Contents(_
, comp
)
219 | FileReferenceType
::ContentsUdeb(_
, comp
)
220 | FileReferenceType
::Packages(_
, comp
)
221 | FileReferenceType
::Sources(comp
)
222 | FileReferenceType
::Translation(comp
)
223 | FileReferenceType
::Dep11(comp
) => comp
,
224 FileReferenceType
::Unknown
225 | FileReferenceType
::PDiff
226 | FileReferenceType
::PseudoRelease(_
)
227 | FileReferenceType
::Ignored
=> None
,
231 pub fn architecture(&self) -> Option
<&Architecture
> {
233 FileReferenceType
::Commands(arch
, _
)
234 | FileReferenceType
::Contents(arch
, _
)
235 | FileReferenceType
::ContentsUdeb(arch
, _
)
236 | FileReferenceType
::Packages(arch
, _
) => Some(arch
),
237 FileReferenceType
::PseudoRelease(arch
) => arch
.as_ref(),
238 FileReferenceType
::Unknown
239 | FileReferenceType
::PDiff
240 | FileReferenceType
::Sources(_
)
241 | FileReferenceType
::Dep11(_
)
242 | FileReferenceType
::Translation(_
)
243 | FileReferenceType
::Ignored
=> None
,
247 pub fn is_package_index(&self) -> bool
{
248 matches
!(self, FileReferenceType
::Packages(_
, _
) | FileReferenceType
::Sources(_
))
252 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
253 pub struct FileReference
{
256 pub checksums
: CheckSums
,
257 pub component
: Component
,
258 pub file_type
: FileReferenceType
,
262 pub fn basename(&self) -> Result
<String
, Error
> {
263 match self.file_type
.compression() {
265 let (base
, _ext
) = self
268 .ok_or_else(|| format_err
!("compressed file without file extension"))?
;
271 None
=> Ok(self.path
.clone()),
276 #[derive(Debug, Default, PartialEq, Eq)]
277 /// A parsed representation of a Release file
278 pub struct ReleaseFile
{
279 /// List of architectures, e.g., `amd64` or `all`.
280 pub architectures
: Vec
<String
>,
281 // TODO No-Support-for-Architecture-all
282 /// URL for changelog queries via `apt changelog`.
283 pub changelogs
: Option
<String
>,
284 /// Release codename - single word, e.g., `bullseye`.
285 pub codename
: Option
<String
>,
286 /// List of repository areas, e.g., `main`.
287 pub components
: Vec
<String
>,
288 /// UTC timestamp of release file generation
289 pub date
: Option
<u64>,
290 /// UTC timestamp of release file expiration
291 pub valid_until
: Option
<u64>,
292 /// Repository description -
293 // TODO exact format?
294 pub description
: Option
<String
>,
295 /// Repository label - single line
296 pub label
: Option
<String
>,
297 /// Repository origin - single line
298 pub origin
: Option
<String
>,
299 /// Release suite - single word, e.g., `stable`.
300 pub suite
: Option
<String
>,
302 pub version
: Option
<String
>,
304 /// Whether by-hash retrieval of referenced files is possible
305 pub aquire_by_hash
: bool
,
307 /// Files referenced by this `Release` file, e.g., packages indices.
309 /// Grouped by basename, since only the compressed version needs to actually exist on the repository server.
310 pub files
: HashMap
<String
, Vec
<FileReference
>>,
313 impl TryFrom
<ReleaseFileRaw
> for ReleaseFile
{
316 fn try_from(value
: ReleaseFileRaw
) -> Result
<Self, Self::Error
> {
317 let mut parsed
= ReleaseFile
{
318 architectures
: whitespace_split_to_vec(
321 .ok_or_else(|| format_err
!("'Architectures' field missing."))?
,
323 components
: whitespace_split_to_vec(
326 .ok_or_else(|| format_err
!("'Components' field missing."))?
,
328 changelogs
: value
.changelogs
,
329 codename
: value
.codename
,
330 date
: value
.date
.as_deref().map(parse_date
),
334 .map(|val
| parse_date(&val
.to_string())),
335 description
: value
.description
,
337 origin
: value
.origin
,
339 files
: HashMap
::new(),
340 aquire_by_hash
: false,
341 version
: value
.version
,
344 if let Some(val
) = value
.extra_fields
.get("Acquire-By-Hash") {
345 parsed
.aquire_by_hash
= *val
== "yes";
347 // Fixup bullseye-security release files which have invalid components
348 if parsed
.label
.as_deref() == Some("Debian-Security")
349 && parsed
.codename
.as_deref() == Some("bullseye-security")
351 parsed
.components
= parsed
355 if let Some(stripped
) = comp
.strip_prefix("updates/") {
364 let mut references_map
: HashMap
<String
, HashMap
<String
, FileReference
>> = HashMap
::new();
366 if let Some(md5
) = value
.md5_sum
{
367 for line
in md5
.lines() {
368 let (mut file_ref
, checksum
) =
369 parse_file_reference(line
, 16, parsed
.components
.as_ref())?
;
371 let checksum
= checksum
373 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
375 file_ref
.checksums
.md5
= Some(checksum
);
377 merge_references(&mut references_map
, file_ref
)?
;
381 if let Some(sha1
) = value
.sha1
{
382 for line
in sha1
.lines() {
383 let (mut file_ref
, checksum
) =
384 parse_file_reference(line
, 20, parsed
.components
.as_ref())?
;
385 let checksum
= checksum
387 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
389 file_ref
.checksums
.sha1
= Some(checksum
);
390 merge_references(&mut references_map
, file_ref
)?
;
394 if let Some(sha256
) = value
.sha256
{
395 for line
in sha256
.lines() {
396 let (mut file_ref
, checksum
) =
397 parse_file_reference(line
, 32, parsed
.components
.as_ref())?
;
398 let checksum
= checksum
400 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
402 file_ref
.checksums
.sha256
= Some(checksum
);
403 merge_references(&mut references_map
, file_ref
)?
;
407 if let Some(sha512
) = value
.sha512
{
408 for line
in sha512
.lines() {
409 let (mut file_ref
, checksum
) =
410 parse_file_reference(line
, 64, parsed
.components
.as_ref())?
;
411 let checksum
= checksum
413 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
415 file_ref
.checksums
.sha512
= Some(checksum
);
416 merge_references(&mut references_map
, file_ref
)?
;
423 .fold(parsed
.files
, |mut map
, (base
, inner_map
)| {
424 map
.insert(base
, inner_map
.into_values().collect());
428 if let Some(insecure
) = parsed
432 .find(|file
| !file
.checksums
.is_secure())
435 "found file reference without strong checksum: {}",
444 impl TryFrom
<String
> for ReleaseFile
{
447 fn try_from(value
: String
) -> Result
<Self, Self::Error
> {
448 value
.as_bytes().try_into()
452 impl TryFrom
<&[u8]> for ReleaseFile
{
455 fn try_from(value
: &[u8]) -> Result
<Self, Self::Error
> {
456 let deserialized
= ReleaseFileRaw
::deserialize(Deserializer
::new(value
))?
;
457 deserialized
.try_into()
461 fn whitespace_split_to_vec(list_str
: &str) -> Vec
<String
> {
463 .split_ascii_whitespace()
464 .map(|arch
| arch
.to_owned())
468 fn parse_file_reference(
471 components
: &[String
],
472 ) -> Result
<(FileReference
, Vec
<u8>), Error
> {
473 let mut split
= line
.split_ascii_whitespace();
476 .ok_or_else(|| format_err
!("No 'checksum' field in the file reference line."))?
;
477 if checksum
.len() > csum_len
* 2 {
479 "invalid checksum length: '{}', expected {} bytes",
485 let checksum
= hex
::decode(checksum
)?
;
489 .ok_or_else(|| format_err
!("No 'size' field in file reference line."))?
494 .ok_or_else(|| format_err
!("No 'path' field in file reference line."))?
497 let (component
, file_type
) = components
499 .find_map(|component
| {
500 if !file
.starts_with(&format
!("{component}/")) {
505 FileReferenceType
::parse(component
, &file
)
506 .map(|file_type
| (component
.clone(), file_type
)),
509 .unwrap_or_else(|| Ok(("UNKNOWN".to_string(), FileReferenceType
::Unknown
)))?
;
515 checksums
: CheckSums
::default(),
523 fn parse_date(_date_str
: &str) -> u64 {
528 fn parse_binary_dir(file_name
: &str, arch
: &str, path
: &str) -> Result
<FileReferenceType
, Error
> {
529 if let Some((dir
, _rest
)) = file_name
.split_once('
/'
) {
530 if dir
== "Packages.diff" {
532 Ok(FileReferenceType
::PDiff
)
534 Ok(FileReferenceType
::Unknown
)
536 } else if file_name
== "Release" {
537 Ok(FileReferenceType
::PseudoRelease(Some(arch
.to_owned())))
539 let comp
= match file_name
.strip_prefix("Packages") {
541 bail
!("found unexpected non-Packages reference to '{path}'")
543 Some(ext
) => FileReferenceType
::match_compression(ext
)?
,
545 //println!("compression: {comp:?}");
546 Ok(FileReferenceType
::Packages(arch
.to_owned(), comp
))
551 base_map
: &mut HashMap
<String
, HashMap
<String
, FileReference
>>,
552 file_ref
: FileReference
,
553 ) -> Result
<(), Error
> {
554 let base
= file_ref
.basename()?
;
556 match base_map
.get_mut(&base
) {
558 let mut map
= HashMap
::new();
559 map
.insert(file_ref
.path
.clone(), file_ref
);
560 base_map
.insert(base
, map
);
563 match entries
.get_mut(&file_ref
.path
) {
565 if entry
.size
!= file_ref
.size
{
567 "Multiple entries for '{}' with size mismatch: {} / {}",
574 entry
.checksums
.merge(&file_ref
.checksums
).map_err(|err
| {
575 format_err
!("Multiple checksums for '{}' - {err}", entry
.path
)
579 entries
.insert(file_ref
.path
.clone(), file_ref
);
589 pub fn test_deb_release_file() {
590 let input
= include_str
!(concat
!(
591 env
!("CARGO_MANIFEST_DIR"),
592 "/tests/deb822/release/deb.debian.org_debian_dists_bullseye_Release"
595 let deserialized
= ReleaseFileRaw
::deserialize(Deserializer
::new(input
.as_bytes())).unwrap();
596 //println!("{:?}", deserialized);
598 let parsed
: ReleaseFile
= deserialized
.try_into().unwrap();
599 //println!("{:?}", parsed);
601 assert_eq
!(parsed
.files
.len(), 315);
605 pub fn test_deb_release_file_insecure() {
606 let input
= include_str
!(concat
!(
607 env
!("CARGO_MANIFEST_DIR"),
608 "/tests/deb822/release/deb.debian.org_debian_dists_bullseye_Release_insecure"
611 let deserialized
= ReleaseFileRaw
::deserialize(Deserializer
::new(input
.as_bytes())).unwrap();
612 //println!("{:?}", deserialized);
614 let parsed
: Result
<ReleaseFile
, Error
> = deserialized
.try_into();
615 assert
!(parsed
.is_err());
617 println
!("{:?}", parsed
);