]>
Commit | Line | Data |
---|---|---|
b522722d FG |
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 | } |