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(_
, _
))
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
::default();
319 parsed
.architectures
= whitespace_split_to_vec(
322 .ok_or_else(|| format_err
!("'Architectures' field missing."))?
,
324 parsed
.components
= whitespace_split_to_vec(
327 .ok_or_else(|| format_err
!("'Components' field missing."))?
,
330 parsed
.changelogs
= value
.changelogs
;
331 parsed
.codename
= value
.codename
;
333 parsed
.date
= value
.date
.as_deref().map(parse_date
);
334 parsed
.valid_until
= value
337 .map(|val
| parse_date(&val
.to_string()));
339 parsed
.description
= value
.description
;
340 parsed
.label
= value
.label
;
341 parsed
.origin
= value
.origin
;
342 parsed
.suite
= value
.suite
;
343 parsed
.version
= value
.version
;
345 parsed
.aquire_by_hash
= match value
.extra_fields
.get("Acquire-By-Hash") {
346 Some(val
) => *val
== "yes",
350 // Fixup bullseye-security release files which have invalid components
351 if parsed
.label
.as_deref() == Some("Debian-Security")
352 && parsed
.codename
.as_deref() == Some("bullseye-security")
354 parsed
.components
= parsed
358 if let Some(stripped
) = comp
.strip_prefix("updates/") {
367 let mut references_map
: HashMap
<String
, HashMap
<String
, FileReference
>> = HashMap
::new();
369 if let Some(md5
) = value
.md5_sum
{
370 for line
in md5
.lines() {
371 let (mut file_ref
, checksum
) =
372 parse_file_reference(line
, 16, parsed
.components
.as_ref())?
;
374 let checksum
= checksum
376 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
378 file_ref
.checksums
.md5
= Some(checksum
);
380 merge_references(&mut references_map
, file_ref
)?
;
384 if let Some(sha1
) = value
.sha1
{
385 for line
in sha1
.lines() {
386 let (mut file_ref
, checksum
) =
387 parse_file_reference(line
, 20, parsed
.components
.as_ref())?
;
388 let checksum
= checksum
390 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
392 file_ref
.checksums
.sha1
= Some(checksum
);
393 merge_references(&mut references_map
, file_ref
)?
;
397 if let Some(sha256
) = value
.sha256
{
398 for line
in sha256
.lines() {
399 let (mut file_ref
, checksum
) =
400 parse_file_reference(line
, 32, parsed
.components
.as_ref())?
;
401 let checksum
= checksum
403 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
405 file_ref
.checksums
.sha256
= Some(checksum
);
406 merge_references(&mut references_map
, file_ref
)?
;
410 if let Some(sha512
) = value
.sha512
{
411 for line
in sha512
.lines() {
412 let (mut file_ref
, checksum
) =
413 parse_file_reference(line
, 64, parsed
.components
.as_ref())?
;
414 let checksum
= checksum
416 .map_err(|_err
| format_err
!("unexpected checksum length"))?
;
418 file_ref
.checksums
.sha512
= Some(checksum
);
419 merge_references(&mut references_map
, file_ref
)?
;
426 .fold(HashMap
::new(), |mut map
, (base
, inner_map
)| {
427 map
.insert(base
, inner_map
.into_values().collect());
431 if let Some(insecure
) = parsed
435 .find(|file
| !file
.checksums
.is_secure())
438 "found file reference without strong checksum: {}",
447 impl TryFrom
<String
> for ReleaseFile
{
450 fn try_from(value
: String
) -> Result
<Self, Self::Error
> {
451 value
.as_bytes().try_into()
455 impl TryFrom
<&[u8]> for ReleaseFile
{
458 fn try_from(value
: &[u8]) -> Result
<Self, Self::Error
> {
459 let deserialized
= ReleaseFileRaw
::deserialize(Deserializer
::new(value
))?
;
460 deserialized
.try_into()
464 fn whitespace_split_to_vec(list_str
: &str) -> Vec
<String
> {
466 .split_ascii_whitespace()
467 .map(|arch
| arch
.to_owned())
471 fn parse_file_reference(
474 components
: &[String
],
475 ) -> Result
<(FileReference
, Vec
<u8>), Error
> {
476 let mut split
= line
.split_ascii_whitespace();
479 .ok_or_else(|| format_err
!("No 'checksum' field in the file reference line."))?
;
480 if checksum
.len() > csum_len
* 2 {
482 "invalid checksum length: '{}', expected {} bytes",
488 let checksum
= hex
::decode(checksum
)?
;
492 .ok_or_else(|| format_err
!("No 'size' field in file reference line."))?
497 .ok_or_else(|| format_err
!("No 'path' field in file reference line."))?
500 let (component
, file_type
) = components
502 .find_map(|component
| {
503 if !file
.starts_with(&format
!("{component}/")) {
508 FileReferenceType
::parse(component
, &file
)
509 .map(|file_type
| (component
.clone(), file_type
)),
512 .unwrap_or_else(|| Ok(("UNKNOWN".to_string(), FileReferenceType
::Unknown
)))?
;
518 checksums
: CheckSums
::default(),
526 fn parse_date(_date_str
: &str) -> u64 {
531 fn parse_binary_dir(file_name
: &str, arch
: &str, path
: &str) -> Result
<FileReferenceType
, Error
> {
532 if let Some((dir
, _rest
)) = file_name
.split_once('
/'
) {
533 if dir
== "Packages.diff" {
535 Ok(FileReferenceType
::PDiff
)
537 Ok(FileReferenceType
::Unknown
)
539 } else if file_name
== "Release" {
540 Ok(FileReferenceType
::PseudoRelease(Some(arch
.to_owned())))
542 let comp
= match file_name
.strip_prefix("Packages") {
544 bail
!("found unexpected non-Packages reference to '{path}'")
546 Some(ext
) => FileReferenceType
::match_compression(ext
)?
,
548 //println!("compression: {comp:?}");
549 Ok(FileReferenceType
::Packages(arch
.to_owned(), comp
))
554 base_map
: &mut HashMap
<String
, HashMap
<String
, FileReference
>>,
555 file_ref
: FileReference
,
556 ) -> Result
<(), Error
> {
557 let base
= file_ref
.basename()?
;
559 match base_map
.get_mut(&base
) {
561 let mut map
= HashMap
::new();
562 map
.insert(file_ref
.path
.clone(), file_ref
);
563 base_map
.insert(base
, map
);
566 match entries
.get_mut(&file_ref
.path
) {
568 if entry
.size
!= file_ref
.size
{
570 "Multiple entries for '{}' with size mismatch: {} / {}",
577 entry
.checksums
.merge(&file_ref
.checksums
).map_err(|err
| {
578 format_err
!("Multiple checksums for '{}' - {err}", entry
.path
)
582 entries
.insert(file_ref
.path
.clone(), file_ref
);
592 pub fn test_deb_release_file() {
593 let input
= include_str
!(concat
!(
594 env
!("CARGO_MANIFEST_DIR"),
595 "/tests/deb822/release/deb.debian.org_debian_dists_bullseye_Release"
598 let deserialized
= ReleaseFileRaw
::deserialize(Deserializer
::new(input
.as_bytes())).unwrap();
599 //println!("{:?}", deserialized);
601 let parsed
: ReleaseFile
= deserialized
.try_into().unwrap();
602 //println!("{:?}", parsed);
604 assert_eq
!(parsed
.files
.len(), 315);
608 pub fn test_deb_release_file_insecure() {
609 let input
= include_str
!(concat
!(
610 env
!("CARGO_MANIFEST_DIR"),
611 "/tests/deb822/release/deb.debian.org_debian_dists_bullseye_Release_insecure"
614 let deserialized
= ReleaseFileRaw
::deserialize(Deserializer
::new(input
.as_bytes())).unwrap();
615 //println!("{:?}", deserialized);
617 let parsed
: Result
<ReleaseFile
, Error
> = deserialized
.try_into();
618 assert
!(parsed
.is_err());
620 println
!("{:?}", parsed
);