]> git.proxmox.com Git - proxmox-apt.git/blob - src/deb822/sources_file.rs
deb822: source index support
[proxmox-apt.git] / src / deb822 / sources_file.rs
1 use std::collections::HashMap;
2
3 use anyhow::{bail, Error, format_err};
4 use rfc822_like::de::Deserializer;
5 use serde::Deserialize;
6 use serde_json::Value;
7
8 use super::CheckSums;
9 //Uploaders
10 //
11 //Homepage
12 //
13 //Version Control System (VCS) fields
14 //
15 //Testsuite
16 //
17 //Dgit
18 //
19 //Standards-Version (mandatory)
20 //
21 //Build-Depends et al
22 //
23 //Package-List (recommended)
24 //
25 //Checksums-Sha1 and Checksums-Sha256 (mandatory)
26 //
27 //Files (mandatory)
28
29
30
31 #[derive(Debug, Deserialize)]
32 #[serde(rename_all = "PascalCase")]
33 pub struct SourcesFileRaw {
34 pub format: String,
35 pub package: String,
36 pub binary: Option<Vec<String>>,
37 pub version: String,
38 pub section: Option<String>,
39 pub priority: Option<String>,
40 pub maintainer: String,
41 pub uploaders: Option<String>,
42 pub architecture: Option<String>,
43 pub directory: String,
44 pub files: String,
45 #[serde(rename = "Checksums-Sha256")]
46 pub sha256: Option<String>,
47 #[serde(rename = "Checksums-Sha512")]
48 pub sha512: Option<String>,
49 #[serde(flatten)]
50 pub extra_fields: HashMap<String, Value>,
51 }
52
53 #[derive(Debug, PartialEq, Eq)]
54 pub struct SourcePackageEntry {
55 pub format: String,
56 pub package: String,
57 pub binary: Option<Vec<String>>,
58 pub version: String,
59 pub architecture: Option<String>,
60 pub section: Option<String>,
61 pub priority: Option<String>,
62 pub maintainer: String,
63 pub uploaders: Option<String>,
64 pub directory: String,
65 pub files: HashMap<String, SourcePackageFileReference>,
66 }
67
68 #[derive(Debug, PartialEq, Eq)]
69 pub struct SourcePackageFileReference {
70 pub file: String,
71 pub size: usize,
72 pub checksums: CheckSums,
73 }
74
75 impl SourcePackageEntry {
76 pub fn size(&self) -> usize {
77 self.files.values().map(|f| f.size).sum()
78 }
79 }
80
81 #[derive(Debug, Default, PartialEq, Eq)]
82 /// A parsed representation of a Release file
83 pub struct SourcesFile {
84 pub source_packages: Vec<SourcePackageEntry>,
85 }
86
87 impl TryFrom<SourcesFileRaw> for SourcePackageEntry {
88 type Error = Error;
89
90 fn try_from(value: SourcesFileRaw) -> Result<Self, Self::Error> {
91 let mut parsed = SourcePackageEntry {
92 package: value.package,
93 binary: value.binary,
94 version: value.version,
95 architecture: value.architecture,
96 files: HashMap::new(),
97 format: value.format,
98 section: value.section,
99 priority: value.priority,
100 maintainer: value.maintainer,
101 uploaders: value.uploaders,
102 directory: value.directory,
103 };
104
105 for file_reference in value.files.lines() {
106 let (file_name, size, md5) = parse_file_reference(file_reference, 16)?;
107 let entry = parsed.files.entry(file_name.clone()).or_insert_with(|| SourcePackageFileReference { file: file_name, size, checksums: CheckSums::default()});
108 entry.checksums.md5 = Some(md5.try_into().map_err(|_|format_err!("unexpected checksum length"))?);
109 if entry.size != size {
110 bail!("Size mismatch: {} != {}", entry.size, size);
111 }
112 }
113
114 if let Some(sha256) = value.sha256 {
115 for line in sha256.lines() {
116 let (file_name, size, sha256) = parse_file_reference(line, 32)?;
117 let entry = parsed.files.entry(file_name.clone()).or_insert_with(|| SourcePackageFileReference { file: file_name, size, checksums: CheckSums::default()});
118 entry.checksums.sha256 = Some(sha256.try_into().map_err(|_|format_err!("unexpected checksum length"))?);
119 if entry.size != size {
120 bail!("Size mismatch: {} != {}", entry.size, size);
121 }
122 }
123 };
124
125 if let Some(sha512) = value.sha512 {
126 for line in sha512.lines() {
127 let (file_name, size, sha512) = parse_file_reference(line, 64)?;
128 let entry = parsed.files.entry(file_name.clone()).or_insert_with(|| SourcePackageFileReference { file: file_name, size, checksums: CheckSums::default()});
129 entry.checksums.sha512 = Some(sha512.try_into().map_err(|_|format_err!("unexpected checksum length"))?);
130 if entry.size != size {
131 bail!("Size mismatch: {} != {}", entry.size, size);
132 }
133 }
134 };
135
136 for (file_name, reference) in &parsed.files {
137 if !reference.checksums.is_secure() {
138 bail!(
139 "no strong checksum found for source entry '{}'",
140 file_name
141 );
142 }
143 }
144
145 Ok(parsed)
146 }
147 }
148
149 impl TryFrom<String> for SourcesFile {
150 type Error = Error;
151
152 fn try_from(value: String) -> Result<Self, Self::Error> {
153 value.as_bytes().try_into()
154 }
155 }
156
157 impl TryFrom<&[u8]> for SourcesFile {
158 type Error = Error;
159
160 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
161 let deserialized = <Vec<SourcesFileRaw>>::deserialize(Deserializer::new(value))?;
162 deserialized.try_into()
163 }
164 }
165
166 impl TryFrom<Vec<SourcesFileRaw>> for SourcesFile {
167 type Error = Error;
168
169 fn try_from(value: Vec<SourcesFileRaw>) -> Result<Self, Self::Error> {
170 let mut source_packages = Vec::with_capacity(value.len());
171 for entry in value {
172 let entry: SourcePackageEntry = entry.try_into()?;
173 source_packages.push(entry);
174 }
175
176 Ok(Self { source_packages })
177 }
178 }
179
180 fn parse_file_reference(
181 line: &str,
182 csum_len: usize,
183 ) -> Result<(String, usize, Vec<u8>), Error> {
184 let mut split = line.split_ascii_whitespace();
185
186 let checksum = split
187 .next()
188 .ok_or_else(|| format_err!("Missing 'checksum' field."))?;
189 if checksum.len() > csum_len * 2 {
190 bail!(
191 "invalid checksum length: '{}', expected {} bytes",
192 checksum,
193 csum_len
194 );
195 }
196
197 let checksum = hex::decode(checksum)?;
198
199 let size = split
200 .next()
201 .ok_or_else(|| format_err!("Missing 'size' field."))?
202 .parse::<usize>()?;
203
204 let file = split
205 .next()
206 .ok_or_else(|| format_err!("Missing 'file name' field."))?
207 .to_string();
208
209 Ok((file, size, checksum))
210 }
211
212 #[test]
213 pub fn test_deb_packages_file() {
214 // NOTE: test is over an excerpt from packages starting with 0-9, a, b & z using:
215 // http://snapshot.debian.org/archive/debian/20221017T212657Z/dists/bullseye/main/source/Sources.xz
216 let input = include_str!(concat!(
217 env!("CARGO_MANIFEST_DIR"),
218 "/tests/deb822/sources/deb.debian.org_debian_dists_bullseye_main_source_Sources"
219 ));
220
221 let deserialized =
222 <Vec<SourcesFileRaw>>::deserialize(Deserializer::new(input.as_bytes())).unwrap();
223 assert_eq!(deserialized.len(), 1558);
224
225 let parsed: SourcesFile = deserialized.try_into().unwrap();
226
227 assert_eq!(parsed.source_packages.len(), 1558);
228
229 let found = parsed.source_packages.iter().find(|source| source.package == "base-files").expect("test file contains 'base-files' entry");
230 assert_eq!(found.package, "base-files");
231 assert_eq!(found.format, "3.0 (native)");
232 assert_eq!(found.architecture.as_deref(), Some("any"));
233 assert_eq!(found.directory, "pool/main/b/base-files");
234 assert_eq!(found.section.as_deref(), Some("admin"));
235 assert_eq!(found.version, "11.1+deb11u5");
236
237 let binary_packages = found.binary.as_ref().expect("base-files source package builds base-files binary package");
238 assert_eq!(binary_packages.len(), 1);
239 assert_eq!(binary_packages[0], "base-files");
240
241 let references = &found.files;
242 assert_eq!(references.len(), 2);
243
244 let dsc_file = "base-files_11.1+deb11u5.dsc";
245 let dsc = references.get(dsc_file).expect("base-files source package contains 'dsc' reference");
246 assert_eq!(dsc.file, dsc_file);
247 assert_eq!(dsc.size, 1110);
248 assert_eq!(dsc.checksums.md5.expect("dsc has md5 checksum"), hex::decode("741c34ac0151262a03de8d5a07bc4271").unwrap()[..]);
249 assert_eq!(dsc.checksums.sha256.expect("dsc has sha256 checksum"), hex::decode("c41a7f00d57759f27e6068240d1ea7ad80a9a752e4fb43850f7e86e967422bd3").unwrap()[..]);
250
251 let tar_file = "base-files_11.1+deb11u5.tar.xz";
252 let tar = references.get(tar_file).expect("base-files source package contains 'tar' reference");
253 assert_eq!(tar.file, tar_file);
254 assert_eq!(tar.size, 65612);
255 assert_eq!(tar.checksums.md5.expect("tar has md5 checksum"), hex::decode("995df33642118b566a4026410e1c6aac").unwrap()[..]);
256 assert_eq!(tar.checksums.sha256.expect("tar has sha256 checksum"), hex::decode("31c9e5745845a73f3d5c8a7868c379d77aaca42b81194679d7ab40cc28e3a0e9").unwrap()[..]);
257 }