1use chrono::{DateTime, FixedOffset, ParseError, Utc};
2use deb822_fast::{FromDeb822, FromDeb822Paragraph, Paragraph};
3use oma_apt_sources_lists::Signature;
4use oma_repo_verify::verify_release_by_sysroot;
5use once_cell::sync::OnceCell;
6use std::{
7 borrow::Cow,
8 fs,
9 io::{self, ErrorKind},
10 num::ParseIntError,
11 path::Path,
12 str::FromStr,
13};
14use thiserror::Error;
15use tracing::{debug, trace};
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct ChecksumItem {
19 pub name: String,
20 pub size: u64,
21 pub checksum: String,
22}
23
24#[derive(Debug, thiserror::Error)]
25pub enum InReleaseError {
26 #[error("Mirror is not signed by trusted keyring.")]
27 NotTrusted,
28 #[error(transparent)]
29 VerifyError(#[from] oma_repo_verify::VerifyError),
30 #[error("Bad InRelease Data")]
31 BadInReleaseData,
32 #[error("Bad valid until")]
33 BadInReleaseValidUntil,
34 #[error("Earlier signature")]
35 EarlierSignature,
36 #[error("Expired signature")]
37 ExpiredSignature,
38 #[error("Bad InRelease")]
39 InReleaseSyntaxError,
40 #[error("Unsupported file type in path")]
41 UnsupportedFileType,
42 #[error(transparent)]
43 ParseIntError(ParseIntError),
44 #[error("InRelease is broken")]
45 BrokenInRelease,
46 #[error("Failed to read release.gpg file: {1}")]
47 ReadGPGFileName(std::io::Error, String),
48}
49
50pub type InReleaseParserResult<T> = Result<T, InReleaseError>;
51
52#[derive(Clone, Copy)]
53pub enum InReleaseChecksum {
54 Sha256,
55 Sha512,
56 Md5,
57}
58
59const COMPRESS: &[&str] = &[".gz", ".xz", ".zst", ".bz2"];
60
61pub struct Release {
62 pub source: InReleaseEntry,
63 acquire_by_hash: OnceCell<bool>,
64 checksum_type_and_list: OnceCell<(InReleaseChecksum, Vec<ChecksumItem>)>,
65}
66
67#[derive(Debug, FromDeb822)]
68pub struct InReleaseEntry {
69 #[deb822(field = "Date")]
70 pub date: Option<String>,
71 #[deb822(field = "Valid-Until")]
72 pub valid_until: Option<String>,
73 #[deb822(field = "Acquire-By-Hash")]
74 pub acquire_by_hash: Option<String>,
75 #[deb822(field = "MD5Sum")]
76 pub md5sum: Option<String>,
77 #[deb822(field = "SHA256")]
78 pub sha256: Option<String>,
79 #[deb822(field = "SHA512")]
80 pub sha512: Option<String>,
81}
82
83impl FromStr for Release {
84 type Err = InReleaseError;
85
86 fn from_str(input: &str) -> Result<Self, Self::Err> {
87 let source: Paragraph = input.parse().map_err(|_| InReleaseError::BrokenInRelease)?;
88 let source: InReleaseEntry = FromDeb822Paragraph::from_paragraph(&source)
89 .map_err(|_| InReleaseError::BrokenInRelease)?;
90
91 Ok(Self {
92 source,
93 acquire_by_hash: OnceCell::new(),
94 checksum_type_and_list: OnceCell::new(),
95 })
96 }
97}
98
99impl Release {
100 pub fn get_or_try_init_checksum_type_and_list(
101 &self,
102 ) -> Result<&(InReleaseChecksum, Vec<ChecksumItem>), InReleaseError> {
103 self.checksum_type_and_list.get_or_try_init(|| {
104 let (checksum_type, checksums) = if let Some(sha256) = &self.source.sha256 {
105 (InReleaseChecksum::Sha256, get_checksums_inner(sha256)?)
106 } else if let Some(sha512) = &self.source.sha512 {
107 (InReleaseChecksum::Sha512, get_checksums_inner(sha512)?)
108 } else if let Some(md5) = &self.source.md5sum {
109 (InReleaseChecksum::Md5, get_checksums_inner(md5)?)
110 } else {
111 return Err(InReleaseError::BrokenInRelease);
112 };
113
114 Ok((checksum_type, checksums))
115 })
116 }
117
118 pub fn checksum_type_and_list(&self) -> &(InReleaseChecksum, Vec<ChecksumItem>) {
119 self.get_or_try_init_checksum_type_and_list()
120 .expect("checksum type and list does not init")
121 }
122
123 pub fn acquire_by_hash(&self) -> bool {
124 *self.acquire_by_hash.get_or_init(|| {
125 self.source
126 .acquire_by_hash
127 .as_ref()
128 .is_some_and(|x| x.eq_ignore_ascii_case("yes"))
129 })
130 }
131
132 pub fn check_date(&self, now: &DateTime<Utc>) -> Result<(), InReleaseError> {
133 let date = self
134 .source
135 .date
136 .as_ref()
137 .ok_or(InReleaseError::BadInReleaseData)?;
138
139 let date = parse_date(date).map_err(|e| {
140 debug!("Failed to parse data: {}", e);
141 InReleaseError::BadInReleaseData
142 })?;
143
144 if now < &date {
145 return Err(InReleaseError::EarlierSignature);
146 }
147
148 Ok(())
149 }
150
151 pub fn check_valid_until(&self, now: &DateTime<Utc>) -> Result<(), InReleaseError> {
152 if let Some(valid_until_date) = &self.source.valid_until {
154 let valid_until = parse_date(valid_until_date).map_err(|e| {
155 debug!("Failed to parse the valid_until field: {}", e);
156 InReleaseError::BadInReleaseValidUntil
157 })?;
158
159 if now > &valid_until {
160 return Err(InReleaseError::ExpiredSignature);
161 }
162 }
163
164 Ok(())
165 }
166}
167
168impl FromStr for ChecksumItem {
169 type Err = InReleaseError;
170
171 fn from_str(s: &str) -> Result<Self, Self::Err> {
172 trace!("Parsing line: {s}");
173
174 let mut line = s.split_ascii_whitespace();
175
176 let checksum = line
177 .next()
178 .ok_or(InReleaseError::BrokenInRelease)?
179 .to_string();
180
181 trace!("Checksum: {checksum}");
182
183 let size = line.next().ok_or(InReleaseError::BrokenInRelease)?;
184
185 trace!("Size: {size}");
186
187 let size = size.parse::<u64>().map_err(InReleaseError::ParseIntError)?;
188
189 let name = line
190 .next()
191 .ok_or(InReleaseError::BrokenInRelease)?
192 .to_string();
193
194 if line.next().is_some() {
195 return Err(InReleaseError::BrokenInRelease);
196 }
197
198 Ok(Self {
199 name,
200 size,
201 checksum,
202 })
203 }
204}
205
206fn get_checksums_inner(checksum_str: &str) -> Result<Vec<ChecksumItem>, InReleaseError> {
207 checksum_str
208 .trim()
209 .lines()
210 .map(ChecksumItem::from_str)
211 .collect::<Result<Vec<_>, InReleaseError>>()
212}
213
214pub fn verify_inrelease<'a>(
215 inrelease: &'a str,
216 signed_by: Option<&Signature>,
217 rootfs: impl AsRef<Path>,
218 file: impl AsRef<Path>,
219 trusted: bool,
220) -> Result<Cow<'a, str>, InReleaseError> {
221 if inrelease.starts_with("-----BEGIN PGP SIGNED MESSAGE-----") {
222 Ok(Cow::Owned(oma_repo_verify::verify_inrelease_by_sysroot(
223 inrelease, signed_by, rootfs, trusted,
224 )?))
225 } else {
226 if trusted {
227 return Ok(Cow::Borrowed(inrelease));
228 }
229
230 let inrelease_path = file.as_ref();
231
232 let mut file_name = inrelease_path
233 .file_name()
234 .map(|x| x.to_string_lossy().to_string())
235 .ok_or_else(|| {
236 InReleaseError::ReadGPGFileName(
237 io::Error::new(ErrorKind::InvalidInput, "Failed to get file name"),
238 inrelease_path.display().to_string(),
239 )
240 })?;
241
242 file_name.push_str(".gpg");
243
244 let pub_file = inrelease_path.with_file_name(&file_name);
245
246 debug!("Reading GPG file: {}", pub_file.display());
247 let bytes = fs::read(pub_file)
248 .map_err(|e| InReleaseError::ReadGPGFileName(e, file_name.to_string()))?;
249
250 verify_release_by_sysroot(inrelease, &bytes, signed_by, rootfs, trusted).map_err(|e| {
251 debug!("{e}");
252 InReleaseError::NotTrusted
253 })?;
254
255 Ok(Cow::Borrowed(inrelease))
256 }
257}
258
259pub(crate) fn split_ext_and_filename(x: &str) -> (Cow<'_, str>, String) {
260 let path = Path::new(x);
261 let ext = path.extension().unwrap_or_default().to_string_lossy();
262 let name = path.with_extension("");
263 let name = name.to_string_lossy().to_string();
264
265 (ext, name)
266}
267
268pub(crate) fn file_is_compress(name: &str) -> bool {
269 for i in COMPRESS {
270 if name.ends_with(i) {
271 return true;
272 }
273 }
274
275 false
276}
277
278#[derive(Debug, Error)]
279enum ParseDateError {
280 #[error(transparent)]
281 ParseError(#[from] ParseError),
282 #[error("Could not parse date: {0}")]
283 BadDate(ParseIntError),
284}
285
286fn parse_date(date: &str) -> Result<DateTime<FixedOffset>, ParseDateError> {
287 match DateTime::parse_from_rfc2822(date) {
288 Ok(res) => Ok(res),
289 Err(e) => {
290 debug!("Failed to parse {}: {e}, trying to use date hack ...", date);
291 let hack_date = date_hack(date).map_err(ParseDateError::BadDate)?;
292 Ok(DateTime::parse_from_rfc2822(&hack_date)?)
293 }
294 }
295}
296
297fn date_hack(date: &str) -> Result<String, ParseIntError> {
311 let mut split_time = date
312 .split_ascii_whitespace()
313 .map(|x| x.to_string())
314 .collect::<Vec<_>>();
315
316 for c in split_time.iter_mut() {
317 if c.is_empty() || !c.contains(':') {
318 continue;
319 }
320
321 let mut time_split = c.split(':').map(|x| x.to_string()).collect::<Vec<_>>();
322
323 for k in time_split.iter_mut() {
325 match k.parse::<u64>()? {
326 0..=9 if k.len() == 1 => {
327 *k = "0".to_string() + k;
328 }
329 _ => continue,
330 }
331 }
332
333 *c = time_split.join(":");
334 }
335
336 let date = split_time.join(" ");
337
338 Ok(date.replace("UTC", "+0000"))
339}
340
341#[test]
342fn test_date_hack() {
343 let a = "Thu, 02 May 2024 9:58:03 UTC";
344 let hack = date_hack(&a).unwrap();
345 assert_eq!(hack, "Thu, 02 May 2024 09:58:03 +0000");
346 let b = DateTime::parse_from_rfc2822(&hack);
347 assert!(b.is_ok());
348
349 let a = "Thu, 02 May 2024 09:58:03 +0000";
350 let hack = date_hack(&a).unwrap();
351 assert_eq!(hack, "Thu, 02 May 2024 09:58:03 +0000");
352 let b = DateTime::parse_from_rfc2822(&hack);
353 assert!(b.is_ok());
354
355 let a = "Thu, 02 May 2024 0:58:03 +0000";
356 let hack = date_hack(&a).unwrap();
357 assert_eq!(hack, "Thu, 02 May 2024 00:58:03 +0000");
358 let b = DateTime::parse_from_rfc2822(&hack);
359 assert!(b.is_ok());
360}
361
362#[test]
363fn test_split_name_and_ext() {
364 let example1 = "main/dep11/icons-128x128.tar.gz";
365 let res = split_ext_and_filename(&example1);
366 assert_eq!(
367 res,
368 ("gz".into(), "main/dep11/icons-128x128.tar".to_string())
369 );
370
371 let example2 = "main/i18n/Translation-bg.xz";
372 let res = split_ext_and_filename(&example2);
373 assert_eq!(res, ("xz".into(), "main/i18n/Translation-bg".to_string()));
374
375 let example2 = "main/i18n/Translation-bg";
376 let res = split_ext_and_filename(&example2);
377 assert_eq!(res, ("".into(), "main/i18n/Translation-bg".to_string()));
378}
379
380#[test]
381fn test_checksum_parse() {
382 let entry = "87c803ffdc2655fd4df8779707ae7713b8e1e2dba44fea4a68b4783b7d8aa6c9 392728 Contents-amd64";
383 assert_eq!(
384 ChecksumItem::from_str(entry).unwrap(),
385 ChecksumItem {
386 name: "Contents-amd64".to_string(),
387 size: 392728,
388 checksum: "87c803ffdc2655fd4df8779707ae7713b8e1e2dba44fea4a68b4783b7d8aa6c9"
389 .to_string()
390 }
391 );
392}