1use std::{collections::HashSet, fs::read_dir, path::PathBuf, str::FromStr};
4
5use alpm_buildinfo::BuildInfo;
6use alpm_common::MetadataFile;
7use alpm_mtree::Mtree;
8use alpm_pkginfo::PackageInfo;
9use alpm_srcinfo::SourceInfo;
10use log::{debug, info};
11use rayon::iter::{IntoParallelIterator, ParallelIterator};
12use voa::{
13 commands::{openpgp_verify, read_openpgp_signatures, read_openpgp_verifiers},
14 core::{Context, Os, Purpose},
15 openpgp::VerifierLookup,
16 utils::RegularFile,
17};
18
19use crate::{
20 CacheDir,
21 Error,
22 cli::TestFileType,
23 consts::{AUR_DIR, DATABASES_DIR, DOWNLOAD_DIR, PACKAGES_DIR, PKGSRC_DIR},
24 sync::PackageRepositories,
25 ui::get_progress_bar,
26};
27
28fn openpgp_verify_file(
39 file: PathBuf,
40 signature: PathBuf,
41 lookup: &VerifierLookup,
42) -> Result<(), Error> {
43 debug!("Verifying {file:?} with {signature:?}");
44
45 let signatures = read_openpgp_signatures(&HashSet::from_iter([RegularFile::try_from(
46 signature.clone(),
47 )?]))?;
48
49 let check_results = openpgp_verify(lookup, &signatures, &RegularFile::try_from(file.clone())?)?;
50
51 for check_result in check_results {
53 if let Some(signer_info) = check_result.signer_info() {
54 debug!(
55 "Successfully verified using {} {}",
56 signer_info.certificate().fingerprint(),
57 signer_info.component_fingerprint()
58 )
59 } else {
60 return Err(Error::VoaVerificationFailed {
61 file,
62 signature,
63 context: "".to_string(),
64 });
65 }
66 }
67
68 Ok(())
69}
70
71#[derive(Clone, Debug)]
73pub struct TestRunner {
74 pub cache_dir: CacheDir,
76 pub file_type: TestFileType,
78 pub repositories: Vec<PackageRepositories>,
80}
81
82impl TestRunner {
83 pub fn run_tests(&self) -> Result<(), Error> {
86 let test_files = self.find_files_of_type()?;
87 info!(
88 "Found {} {} files for testing",
89 test_files.len(),
90 self.file_type
91 );
92
93 let certs = if matches!(self.file_type, TestFileType::Signatures) {
96 read_openpgp_verifiers(
97 Os::from_str("arch").map_err(|source| Error::Voa(voa::Error::VoaCore(source)))?,
98 Purpose::from_str("package")
99 .map_err(|source| Error::Voa(voa::Error::VoaCore(source)))?,
100 Context::Default,
101 )
102 } else {
103 Vec::new()
104 };
105
106 let lookup = VerifierLookup::new(&certs);
107
108 let progress_bar = get_progress_bar(test_files.len() as u64);
109
110 let asserts: Vec<(PathBuf, Result<(), Error>)> = test_files
112 .into_par_iter()
113 .map(|file| {
114 let result = match self.file_type {
115 TestFileType::BuildInfo => BuildInfo::from_file_with_schema(&file, None)
116 .map(|_| ())
117 .map_err(|err| err.into()),
118 TestFileType::SrcInfo => SourceInfo::from_file_with_schema(&file, None)
119 .map(|_| ())
120 .map_err(|err| err.into()),
121 TestFileType::MTree => Mtree::from_file_with_schema(&file, None)
122 .map(|_| ())
123 .map_err(|err| err.into()),
124 TestFileType::PackageInfo => PackageInfo::from_file_with_schema(&file, None)
125 .map(|_| ())
126 .map_err(|err| err.into()),
127 TestFileType::RemoteDesc => unimplemented!(),
128 TestFileType::RemoteFiles => unimplemented!(),
129 TestFileType::LocalDesc => unimplemented!(),
130 TestFileType::LocalFiles => unimplemented!(),
131 TestFileType::Signatures => {
132 let data = {
133 let mut data = file.clone();
134 data.set_extension("");
135 data
136 };
137
138 openpgp_verify_file(data, file.clone(), &lookup)
139 }
140 };
141
142 progress_bar.inc(1);
143 (file, result)
144 })
145 .collect();
146
147 progress_bar.finish_with_message("Validation run finished.");
149
150 let failures: Vec<(PathBuf, Error)> = asserts
152 .into_iter()
153 .filter_map(|(path, result)| {
154 if let Err(err) = result {
155 Some((path, err))
156 } else {
157 None
158 }
159 })
160 .collect();
161
162 if !failures.is_empty() {
163 return Err(Error::TestFailed {
164 failures: failures
165 .iter()
166 .enumerate()
167 .map(|(index, failure)| (index, failure.0.clone(), failure.1.to_string()))
168 .collect::<Vec<_>>(),
169 });
170 }
171
172 Ok(())
173 }
174
175 pub fn find_files_of_type(&self) -> Result<Vec<PathBuf>, Error> {
179 debug!("Searching for files of type {}", self.file_type);
180
181 let mut files = Vec::new();
182
183 let type_folders = match self.file_type {
185 TestFileType::BuildInfo | TestFileType::PackageInfo | TestFileType::MTree => self
188 .repositories
189 .iter()
190 .map(|repo| {
191 self.cache_dir
192 .as_ref()
193 .join(PACKAGES_DIR)
194 .join(repo.to_string())
195 })
196 .collect(),
197 TestFileType::SrcInfo => vec![
198 self.cache_dir.as_ref().join(PKGSRC_DIR),
199 self.cache_dir.as_ref().join(AUR_DIR),
200 ],
201 TestFileType::RemoteDesc | TestFileType::RemoteFiles => self
204 .repositories
205 .iter()
206 .map(|repo| {
207 self.cache_dir
208 .as_ref()
209 .join(DATABASES_DIR)
210 .join(repo.to_string())
211 })
212 .collect(),
213 TestFileType::Signatures => {
214 let dirs: Vec<PathBuf> = self
215 .repositories
216 .iter()
217 .map(|repo| {
218 self.cache_dir
219 .as_ref()
220 .join(DOWNLOAD_DIR)
221 .join(PACKAGES_DIR)
222 .join(repo.to_string())
223 })
224 .collect();
225
226 return files_in_dirs_by_extension(
228 dirs.as_slice(),
229 &TestFileType::Signatures.to_string(),
230 );
231 }
232 TestFileType::LocalDesc | TestFileType::LocalFiles => {
233 unimplemented!();
234 }
235 };
236
237 for folder in type_folders {
238 debug!("Looking for files in {folder:?}");
239 if !folder.exists() {
240 info!("The directory {folder:?} doesn't exist, skipping.");
241 continue;
242 }
243 for pkg_folder in read_dir(&folder).map_err(|source| Error::IoPath {
247 path: folder.clone(),
248 context: "reading entries in directory".to_string(),
249 source,
250 })? {
251 let pkg_folder = pkg_folder.map_err(|source| Error::IoPath {
252 path: folder.clone(),
253 context: "reading an entry of the directory".to_string(),
254 source,
255 })?;
256 let file_path = pkg_folder.path().join(self.file_type.to_string());
257 if file_path.exists() {
258 files.push(file_path);
259 }
260 }
261 }
262
263 Ok(files)
264 }
265}
266
267fn files_in_dirs_by_extension(dirs: &[PathBuf], extension: &str) -> Result<Vec<PathBuf>, Error> {
279 let mut files = Vec::new();
280
281 for dir in dirs {
282 debug!("Looking for files in {dir:?}");
283 if !dir.exists() {
284 info!("Skipping directory {dir:?} as it does not exist.");
285 continue;
286 }
287
288 for entry in read_dir(dir).map_err(|source| Error::IoPath {
289 path: dir.clone(),
290 context: "reading entries in directory".to_string(),
291 source,
292 })? {
293 let entry = entry.map_err(|source| Error::IoPath {
294 path: dir.clone(),
295 context: "reading an entry of the directory".to_string(),
296 source,
297 })?;
298
299 let file_path = entry.path();
300 if file_path.is_file()
301 && file_path
302 .extension()
303 .is_some_and(|ext| ext.to_str() == Some(extension))
304 {
305 files.push(file_path);
306 }
307 }
308 }
309
310 Ok(files)
311}
312
313#[cfg(test)]
314mod tests {
315 use std::{
316 collections::HashSet,
317 fs::{OpenOptions, create_dir},
318 };
319
320 use rstest::rstest;
321 use strum::IntoEnumIterator;
322 use testresult::TestResult;
323
324 use super::*;
325
326 const PKG_NAMES: &[&str] = &[
327 "pipewire-alsa-1:1.0.7-1-x86_64",
328 "xorg-xvinfo-1.1.5-1-x86_64",
329 "acl-2.3.2-1-x86_64",
330 "archlinux-keyring-20240520-1-any",
331 ];
332
333 #[rstest]
337 #[case(TestFileType::BuildInfo)]
338 #[case(TestFileType::PackageInfo)]
339 #[case(TestFileType::MTree)]
340 fn test_find_files_for_packages(#[case] file_type: TestFileType) -> TestResult {
341 let tmp_dir = tempfile::tempdir()?;
343 let packages_dir = tmp_dir.path().join(PACKAGES_DIR);
344 create_dir(&packages_dir)?;
345
346 let mut expected_files = HashSet::new();
348
349 for (index, repo) in PackageRepositories::iter().enumerate() {
351 let repo_dir = packages_dir.join(repo.to_string());
353 create_dir(&repo_dir)?;
354
355 let pkg = PKG_NAMES[index];
357 let pkg_dir = repo_dir.join(pkg);
358 create_dir(&pkg_dir)?;
359
360 let file_path = pkg_dir.join(file_type.to_string());
362 OpenOptions::new()
363 .create(true)
364 .write(true)
365 .truncate(true)
366 .open(&file_path)?;
367 expected_files.insert(file_path);
368 }
369
370 let runner = TestRunner {
372 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
373 file_type,
374 repositories: PackageRepositories::iter().collect(),
375 };
376 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
377
378 assert_eq!(
379 found_files, expected_files,
380 "Expected that all created package files are also found."
381 );
382
383 Ok(())
384 }
385
386 #[rstest]
390 #[case(TestFileType::RemoteFiles)]
391 #[case(TestFileType::RemoteDesc)]
392 fn test_find_files_for_databases(#[case] file_type: TestFileType) -> TestResult {
393 let tmp_dir = tempfile::tempdir()?;
395 let databases_dir = tmp_dir.path().join(DATABASES_DIR);
396 create_dir(&databases_dir)?;
397
398 let mut expected_files = HashSet::new();
400
401 for (index, repo) in PackageRepositories::iter().enumerate() {
403 let repo_dir = databases_dir.join(repo.to_string());
405 create_dir(&repo_dir)?;
406
407 let pkg = PKG_NAMES[index];
409 let pkg_dir = repo_dir.join(pkg);
410 create_dir(&pkg_dir)?;
411
412 let file_path = pkg_dir.join(file_type.to_string());
414 OpenOptions::new()
415 .create(true)
416 .write(true)
417 .truncate(true)
418 .open(&file_path)?;
419 expected_files.insert(file_path);
420 }
421
422 let runner = TestRunner {
424 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
425 file_type,
426 repositories: PackageRepositories::iter().collect(),
427 };
428 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
429
430 assert_eq!(
431 found_files, expected_files,
432 "Expected that all created databases files are also found."
433 );
434
435 Ok(())
436 }
437
438 #[rstest]
442 #[case(TestFileType::SrcInfo)]
443 fn test_find_files_for_pkgsrc(#[case] file_type: TestFileType) -> TestResult {
444 let tmp_dir = tempfile::tempdir()?;
446 let pkgsrc_dir = tmp_dir.path().join(PKGSRC_DIR);
447 create_dir(&pkgsrc_dir)?;
448
449 let mut expected_files = HashSet::new();
451
452 for pkg in PKG_NAMES {
455 let pkg_dir = pkgsrc_dir.join(pkg);
456 create_dir(&pkg_dir)?;
457
458 let file_path = pkg_dir.join(file_type.to_string());
460 OpenOptions::new()
461 .create(true)
462 .write(true)
463 .truncate(true)
464 .open(&file_path)?;
465 expected_files.insert(file_path);
466 }
467
468 let runner = TestRunner {
470 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
471 file_type,
472 repositories: PackageRepositories::iter().collect(),
473 };
474 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
475
476 assert_eq!(
477 found_files, expected_files,
478 "Expected that all created pkgsrc files are also found."
479 );
480
481 Ok(())
482 }
483}