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::{
14 PurposeAndContext,
15 get_technology_settings,
16 get_voa_config,
17 openpgp_verify,
18 read_openpgp_signatures,
19 read_openpgp_verifiers,
20 },
21 core::{Context, Os, Purpose},
22 openpgp::ModelBasedVerifier,
23 utils::RegularFile,
24};
25
26use crate::{
27 CacheDir,
28 Error,
29 cli::TestFileType,
30 consts::{AUR_DIR, DATABASES_DIR, DOWNLOAD_DIR, PACKAGES_DIR, PKGSRC_DIR},
31 sync::PackageRepositories,
32 ui::get_progress_bar,
33};
34
35fn openpgp_verify_file(
46 file: PathBuf,
47 signature: PathBuf,
48 model_verifier: &ModelBasedVerifier,
49) -> Result<(), Error> {
50 debug!("Verifying {file:?} with {signature:?}");
51
52 let signatures = read_openpgp_signatures(&HashSet::from_iter([RegularFile::try_from(
53 signature.clone(),
54 )?]))?;
55
56 let check_results = openpgp_verify(
57 model_verifier,
58 &signatures,
59 &RegularFile::try_from(file.clone())?,
60 )?;
61
62 for check_result in check_results {
64 if let Some(signer_info) = check_result.signer_info() {
65 debug!(
66 "Successfully verified using {} {}",
67 signer_info
68 .certificate()
69 .fingerprint()
70 .map_err(voa::Error::VoaOpenPgp)?,
71 signer_info.component_fingerprint()
72 )
73 } else {
74 return Err(Error::VoaVerificationFailed {
75 file,
76 signature,
77 context: "".to_string(),
78 });
79 }
80 }
81
82 Ok(())
83}
84
85#[derive(Clone, Debug)]
87pub struct TestRunner {
88 pub cache_dir: CacheDir,
90 pub file_type: TestFileType,
92 pub repositories: Vec<PackageRepositories>,
94}
95
96impl TestRunner {
97 pub fn run_tests(&self) -> Result<(), Error> {
100 let test_files = self.find_files_of_type()?;
101 info!(
102 "Found {} {} files for testing",
103 test_files.len(),
104 self.file_type
105 );
106
107 let os = Os::from_str("arch").map_err(voa::Error::VoaCore)?;
110
111 let (artifact_verifiers, anchors) = if matches!(self.file_type, TestFileType::Signatures) {
112 let artifact_verifiers = read_openpgp_verifiers(
113 os.clone(),
114 Purpose::from_str("package").map_err(voa::Error::VoaCore)?,
115 Context::Default,
116 );
117 let anchors = read_openpgp_verifiers(
118 os.clone(),
119 Purpose::from_str("trust-anchor-package").map_err(voa::Error::VoaCore)?,
120 Context::Default,
121 );
122 (artifact_verifiers, anchors)
123 } else {
124 (Vec::new(), Vec::new())
125 };
126
127 let config = get_voa_config();
128 let purpose_and_context = PurposeAndContext::new(
129 Some(Purpose::from_str("package").map_err(voa::Error::VoaCore)?),
130 Some(Context::Default),
131 );
132 let openpgp_settings =
133 get_technology_settings(&config, &os, purpose_and_context.as_ref()).openpgp_settings();
134
135 let model_verifier =
136 ModelBasedVerifier::new(openpgp_settings, &artifact_verifiers, &anchors);
137
138 let progress_bar = get_progress_bar(test_files.len() as u64);
139
140 let asserts: Vec<(PathBuf, Result<(), Error>)> = test_files
142 .into_par_iter()
143 .map(|file| {
144 let result = match self.file_type {
145 TestFileType::BuildInfo => BuildInfo::from_file_with_schema(&file, None)
146 .map(|_| ())
147 .map_err(|err| err.into()),
148 TestFileType::SrcInfo => SourceInfo::from_file_with_schema(&file, None)
149 .map(|_| ())
150 .map_err(|err| err.into()),
151 TestFileType::MTree => Mtree::from_file_with_schema(&file, None)
152 .map(|_| ())
153 .map_err(|err| err.into()),
154 TestFileType::PackageInfo => PackageInfo::from_file_with_schema(&file, None)
155 .map(|_| ())
156 .map_err(|err| err.into()),
157 TestFileType::RemoteDesc => unimplemented!(),
158 TestFileType::RemoteFiles => unimplemented!(),
159 TestFileType::LocalDesc => unimplemented!(),
160 TestFileType::LocalFiles => unimplemented!(),
161 TestFileType::Signatures => {
162 let data = {
163 let mut data = file.clone();
164 data.set_extension("");
165 data
166 };
167
168 openpgp_verify_file(data, file.clone(), &model_verifier)
169 }
170 };
171
172 progress_bar.inc(1);
173 (file, result)
174 })
175 .collect();
176
177 progress_bar.finish_with_message("Validation run finished.");
179
180 let failures: Vec<(PathBuf, Error)> = asserts
182 .into_iter()
183 .filter_map(|(path, result)| {
184 if let Err(err) = result {
185 Some((path, err))
186 } else {
187 None
188 }
189 })
190 .collect();
191
192 if !failures.is_empty() {
193 return Err(Error::TestFailed {
194 failures: failures
195 .iter()
196 .enumerate()
197 .map(|(index, failure)| (index, failure.0.clone(), failure.1.to_string()))
198 .collect::<Vec<_>>(),
199 });
200 }
201
202 Ok(())
203 }
204
205 pub fn find_files_of_type(&self) -> Result<Vec<PathBuf>, Error> {
209 debug!("Searching for files of type {}", self.file_type);
210
211 let mut files = Vec::new();
212
213 let type_folders = match self.file_type {
215 TestFileType::BuildInfo | TestFileType::PackageInfo | TestFileType::MTree => self
218 .repositories
219 .iter()
220 .map(|repo| {
221 self.cache_dir
222 .as_ref()
223 .join(PACKAGES_DIR)
224 .join(repo.to_string())
225 })
226 .collect(),
227 TestFileType::SrcInfo => vec![
228 self.cache_dir.as_ref().join(PKGSRC_DIR),
229 self.cache_dir.as_ref().join(AUR_DIR),
230 ],
231 TestFileType::RemoteDesc | TestFileType::RemoteFiles => self
234 .repositories
235 .iter()
236 .map(|repo| {
237 self.cache_dir
238 .as_ref()
239 .join(DATABASES_DIR)
240 .join(repo.to_string())
241 })
242 .collect(),
243 TestFileType::Signatures => {
244 let dirs: Vec<PathBuf> = self
245 .repositories
246 .iter()
247 .map(|repo| {
248 self.cache_dir
249 .as_ref()
250 .join(DOWNLOAD_DIR)
251 .join(PACKAGES_DIR)
252 .join(repo.to_string())
253 })
254 .collect();
255
256 return files_in_dirs_by_extension(
258 dirs.as_slice(),
259 &TestFileType::Signatures.to_string(),
260 );
261 }
262 TestFileType::LocalDesc | TestFileType::LocalFiles => {
263 unimplemented!();
264 }
265 };
266
267 for folder in type_folders {
268 debug!("Looking for files in {folder:?}");
269 if !folder.exists() {
270 info!("The directory {folder:?} doesn't exist, skipping.");
271 continue;
272 }
273 for pkg_folder in read_dir(&folder).map_err(|source| Error::IoPath {
277 path: folder.clone(),
278 context: "reading entries in directory".to_string(),
279 source,
280 })? {
281 let pkg_folder = pkg_folder.map_err(|source| Error::IoPath {
282 path: folder.clone(),
283 context: "reading an entry of the directory".to_string(),
284 source,
285 })?;
286 let file_path = pkg_folder.path().join(self.file_type.to_string());
287 if file_path.exists() {
288 files.push(file_path);
289 }
290 }
291 }
292
293 Ok(files)
294 }
295}
296
297fn files_in_dirs_by_extension(dirs: &[PathBuf], extension: &str) -> Result<Vec<PathBuf>, Error> {
309 let mut files = Vec::new();
310
311 for dir in dirs {
312 debug!("Looking for files in {dir:?}");
313 if !dir.exists() {
314 info!("Skipping directory {dir:?} as it does not exist.");
315 continue;
316 }
317
318 for entry in read_dir(dir).map_err(|source| Error::IoPath {
319 path: dir.clone(),
320 context: "reading entries in directory".to_string(),
321 source,
322 })? {
323 let entry = entry.map_err(|source| Error::IoPath {
324 path: dir.clone(),
325 context: "reading an entry of the directory".to_string(),
326 source,
327 })?;
328
329 let file_path = entry.path();
330 if file_path.is_file()
331 && file_path
332 .extension()
333 .is_some_and(|ext| ext.to_str() == Some(extension))
334 {
335 files.push(file_path);
336 }
337 }
338 }
339
340 Ok(files)
341}
342
343#[cfg(test)]
344mod tests {
345 use std::{
346 collections::HashSet,
347 fs::{OpenOptions, create_dir},
348 };
349
350 use rstest::rstest;
351 use strum::IntoEnumIterator;
352 use testresult::TestResult;
353
354 use super::*;
355
356 const PKG_NAMES: &[&str] = &[
357 "pipewire-alsa-1:1.0.7-1-x86_64",
358 "xorg-xvinfo-1.1.5-1-x86_64",
359 "acl-2.3.2-1-x86_64",
360 "archlinux-keyring-20240520-1-any",
361 ];
362
363 #[rstest]
367 #[case(TestFileType::BuildInfo)]
368 #[case(TestFileType::PackageInfo)]
369 #[case(TestFileType::MTree)]
370 fn test_find_files_for_packages(#[case] file_type: TestFileType) -> TestResult {
371 let tmp_dir = tempfile::tempdir()?;
373 let packages_dir = tmp_dir.path().join(PACKAGES_DIR);
374 create_dir(&packages_dir)?;
375
376 let mut expected_files = HashSet::new();
378
379 for (index, repo) in PackageRepositories::iter().enumerate() {
381 let repo_dir = packages_dir.join(repo.to_string());
383 create_dir(&repo_dir)?;
384
385 let pkg = PKG_NAMES[index];
387 let pkg_dir = repo_dir.join(pkg);
388 create_dir(&pkg_dir)?;
389
390 let file_path = pkg_dir.join(file_type.to_string());
392 OpenOptions::new()
393 .create(true)
394 .write(true)
395 .truncate(true)
396 .open(&file_path)?;
397 expected_files.insert(file_path);
398 }
399
400 let runner = TestRunner {
402 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
403 file_type,
404 repositories: PackageRepositories::iter().collect(),
405 };
406 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
407
408 assert_eq!(
409 found_files, expected_files,
410 "Expected that all created package files are also found."
411 );
412
413 Ok(())
414 }
415
416 #[rstest]
420 #[case(TestFileType::RemoteFiles)]
421 #[case(TestFileType::RemoteDesc)]
422 fn test_find_files_for_databases(#[case] file_type: TestFileType) -> TestResult {
423 let tmp_dir = tempfile::tempdir()?;
425 let databases_dir = tmp_dir.path().join(DATABASES_DIR);
426 create_dir(&databases_dir)?;
427
428 let mut expected_files = HashSet::new();
430
431 for (index, repo) in PackageRepositories::iter().enumerate() {
433 let repo_dir = databases_dir.join(repo.to_string());
435 create_dir(&repo_dir)?;
436
437 let pkg = PKG_NAMES[index];
439 let pkg_dir = repo_dir.join(pkg);
440 create_dir(&pkg_dir)?;
441
442 let file_path = pkg_dir.join(file_type.to_string());
444 OpenOptions::new()
445 .create(true)
446 .write(true)
447 .truncate(true)
448 .open(&file_path)?;
449 expected_files.insert(file_path);
450 }
451
452 let runner = TestRunner {
454 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
455 file_type,
456 repositories: PackageRepositories::iter().collect(),
457 };
458 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
459
460 assert_eq!(
461 found_files, expected_files,
462 "Expected that all created databases files are also found."
463 );
464
465 Ok(())
466 }
467
468 #[rstest]
472 #[case(TestFileType::SrcInfo)]
473 fn test_find_files_for_pkgsrc(#[case] file_type: TestFileType) -> TestResult {
474 let tmp_dir = tempfile::tempdir()?;
476 let pkgsrc_dir = tmp_dir.path().join(PKGSRC_DIR);
477 create_dir(&pkgsrc_dir)?;
478
479 let mut expected_files = HashSet::new();
481
482 for pkg in PKG_NAMES {
485 let pkg_dir = pkgsrc_dir.join(pkg);
486 create_dir(&pkg_dir)?;
487
488 let file_path = pkg_dir.join(file_type.to_string());
490 OpenOptions::new()
491 .create(true)
492 .write(true)
493 .truncate(true)
494 .open(&file_path)?;
495 expected_files.insert(file_path);
496 }
497
498 let runner = TestRunner {
500 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
501 file_type,
502 repositories: PackageRepositories::iter().collect(),
503 };
504 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
505
506 assert_eq!(
507 found_files, expected_files,
508 "Expected that all created pkgsrc files are also found."
509 );
510
511 Ok(())
512 }
513}