1use std::{
4 collections::HashSet,
5 fs::{read_dir, read_to_string},
6 path::PathBuf,
7 str::FromStr,
8};
9
10use alpm_buildinfo::BuildInfo;
11use alpm_common::MetadataFile;
12use alpm_mtree::Mtree;
13use alpm_pkginfo::PackageInfo;
14use alpm_srcinfo::SourceInfo;
15use log::{debug, info, warn};
16use rayon::iter::{IntoParallelIterator, ParallelIterator};
17use strum::IntoEnumIterator;
18use voa::{
19 commands::{
20 PurposeAndContext,
21 get_technology_settings,
22 get_voa_config,
23 openpgp_verify,
24 read_openpgp_signatures,
25 read_openpgp_verifiers,
26 },
27 core::{Context, Os, Purpose},
28 openpgp::ModelBasedVerifier,
29 utils::RegularFile,
30};
31
32use crate::{
33 CacheDir,
34 Error,
35 cli::TestFileFormat,
36 consts::{AUR_DIR, DATABASES_DIR, DOWNLOAD_DIR, PACKAGES_DIR, PKGSRC_DIR},
37 sync::{PackageRepositories, filenames_in_dir},
38 ui::get_progress_bar,
39};
40
41fn openpgp_verify_file(
52 file: PathBuf,
53 signature: PathBuf,
54 model_verifier: &ModelBasedVerifier,
55) -> Result<(), Error> {
56 debug!("Verifying {file:?} with {signature:?}");
57
58 let signatures = read_openpgp_signatures(&HashSet::from_iter([RegularFile::try_from(
59 signature.clone(),
60 )?]))?;
61
62 let check_results = openpgp_verify(
63 model_verifier,
64 &signatures,
65 &RegularFile::try_from(file.clone())?,
66 )?;
67
68 for check_result in check_results {
70 if let Some(signer_info) = check_result.signer_info() {
71 debug!(
72 "Successfully verified using {} {}",
73 signer_info
74 .certificate()
75 .fingerprint()
76 .map_err(voa::Error::VoaOpenPgp)?,
77 signer_info.component_fingerprint()
78 )
79 } else {
80 return Err(Error::VoaVerificationFailed {
81 file,
82 signature,
83 context: "".to_string(),
84 });
85 }
86 }
87
88 Ok(())
89}
90
91#[derive(Clone, Debug)]
93pub struct TestRunner {
94 pub cache_dir: CacheDir,
96 pub file_type: TestFileFormat,
98 pub repositories: Option<Vec<PackageRepositories>>,
103}
104
105impl TestRunner {
106 fn repositories(&self) -> Vec<PackageRepositories> {
110 self.repositories
111 .clone()
112 .unwrap_or_else(|| PackageRepositories::iter().collect())
113 }
114
115 fn pkgbases_in_databases(
120 &self,
121 repositories: &[PackageRepositories],
122 ) -> Result<HashSet<String>, Error> {
123 let mut pkgbases = HashSet::new();
124 for repo in repositories {
125 let dir = self
126 .cache_dir
127 .as_ref()
128 .join(DATABASES_DIR)
129 .join(repo.to_string());
130 if !dir.exists() {
131 warn!("The database for {repo} hasn't been downloaded yet, skipping.");
132 continue;
133 }
134 for pkg in filenames_in_dir(&dir)? {
135 let desc = dir.join(pkg).join("desc");
136 let content = read_to_string(&desc).map_err(|source| Error::IoPath {
137 path: desc.clone(),
138 context: "reading the desc file".to_string(),
139 source,
140 })?;
141 if let Some(pkgbase) = content.lines().skip_while(|line| *line != "%BASE%").nth(1) {
142 pkgbases.insert(pkgbase.to_string());
143 }
144 }
145 }
146 Ok(pkgbases)
147 }
148
149 pub fn run_tests(&self) -> Result<(), Error> {
152 let test_files = self.find_files_of_type()?;
153 info!(
154 "Found {} {} files for testing",
155 test_files.len(),
156 self.file_type
157 );
158
159 let os = Os::from_str("arch").map_err(voa::Error::VoaCore)?;
162
163 let (artifact_verifiers, anchors) = if matches!(self.file_type, TestFileFormat::Signatures)
164 {
165 let artifact_verifiers = read_openpgp_verifiers(
166 os.clone(),
167 Purpose::from_str("package").map_err(voa::Error::VoaCore)?,
168 Context::Default,
169 );
170 let anchors = read_openpgp_verifiers(
171 os.clone(),
172 Purpose::from_str("trust-anchor-package").map_err(voa::Error::VoaCore)?,
173 Context::Default,
174 );
175 (artifact_verifiers, anchors)
176 } else {
177 (Vec::new(), Vec::new())
178 };
179
180 let config = get_voa_config();
181 let purpose_and_context = PurposeAndContext::new(
182 Some(Purpose::from_str("package").map_err(voa::Error::VoaCore)?),
183 Some(Context::Default),
184 );
185 let openpgp_settings =
186 get_technology_settings(&config, &os, purpose_and_context.as_ref()).openpgp_settings();
187
188 let model_verifier =
189 ModelBasedVerifier::new(openpgp_settings, &artifact_verifiers, &anchors);
190
191 let progress_bar = get_progress_bar(test_files.len() as u64);
192
193 let asserts: Vec<(PathBuf, Result<(), Error>)> = test_files
195 .into_par_iter()
196 .map(|file| {
197 let result = match self.file_type {
198 TestFileFormat::Buildinfo => BuildInfo::from_file_with_schema(&file, None)
199 .map(|_| ())
200 .map_err(|err| err.into()),
201 TestFileFormat::Srcinfo => SourceInfo::from_file_with_schema(&file, None)
202 .map(|_| ())
203 .map_err(|err| err.into()),
204 TestFileFormat::MTree => Mtree::from_file_with_schema(&file, None)
205 .map(|_| ())
206 .map_err(|err| err.into()),
207 TestFileFormat::Pkginfo => PackageInfo::from_file_with_schema(&file, None)
208 .map(|_| ())
209 .map_err(|err| err.into()),
210 TestFileFormat::RemoteDesc => unimplemented!(),
211 TestFileFormat::RemoteFiles => unimplemented!(),
212 TestFileFormat::LocalDesc => unimplemented!(),
213 TestFileFormat::LocalFiles => unimplemented!(),
214 TestFileFormat::Signatures => {
215 let data = {
216 let mut data = file.clone();
217 data.set_extension("");
218 data
219 };
220
221 openpgp_verify_file(data, file.clone(), &model_verifier)
222 }
223 };
224
225 progress_bar.inc(1);
226 (file, result)
227 })
228 .collect();
229
230 progress_bar.finish_with_message("Validation run finished.");
232
233 let failures: Vec<(PathBuf, Error)> = asserts
235 .into_iter()
236 .filter_map(|(path, result)| {
237 if let Err(err) = result {
238 Some((path, err))
239 } else {
240 None
241 }
242 })
243 .collect();
244
245 if !failures.is_empty() {
246 return Err(Error::TestFailed {
247 failures: failures
248 .iter()
249 .enumerate()
250 .map(|(index, failure)| (index, failure.0.clone(), failure.1.to_string()))
251 .collect::<Vec<_>>(),
252 });
253 }
254
255 Ok(())
256 }
257
258 pub fn find_files_of_type(&self) -> Result<Vec<PathBuf>, Error> {
262 debug!("Searching for files of type {}", self.file_type);
263
264 let mut files = Vec::new();
265 let repositories = self.repositories();
266
267 let type_folders = match self.file_type {
269 TestFileFormat::Buildinfo | TestFileFormat::Pkginfo | TestFileFormat::MTree => {
272 repositories
273 .iter()
274 .map(|repo| {
275 self.cache_dir
276 .as_ref()
277 .join(PACKAGES_DIR)
278 .join(repo.to_string())
279 })
280 .collect()
281 }
282 TestFileFormat::Srcinfo => {
284 let mut dirs = vec![self.cache_dir.as_ref().join(PKGSRC_DIR)];
285 if self.repositories.is_none() {
286 dirs.push(self.cache_dir.as_ref().join(AUR_DIR));
287 }
288 dirs
289 }
290 TestFileFormat::RemoteDesc | TestFileFormat::RemoteFiles => repositories
293 .iter()
294 .map(|repo| {
295 self.cache_dir
296 .as_ref()
297 .join(DATABASES_DIR)
298 .join(repo.to_string())
299 })
300 .collect(),
301 TestFileFormat::Signatures => {
302 let dirs: Vec<PathBuf> = repositories
303 .iter()
304 .map(|repo| {
305 self.cache_dir
306 .as_ref()
307 .join(DOWNLOAD_DIR)
308 .join(PACKAGES_DIR)
309 .join(repo.to_string())
310 })
311 .collect();
312
313 return files_in_dirs_by_extension(
315 dirs.as_slice(),
316 &TestFileFormat::Signatures.to_string(),
317 );
318 }
319 TestFileFormat::LocalDesc | TestFileFormat::LocalFiles => {
320 unimplemented!();
321 }
322 };
323
324 let pkgbase_filter = match (self.file_type, &self.repositories) {
327 (TestFileFormat::Srcinfo, Some(repositories)) => {
328 Some(self.pkgbases_in_databases(repositories)?)
329 }
330 _ => None,
331 };
332
333 for folder in type_folders {
334 debug!("Looking for files in {folder:?}");
335 if !folder.exists() {
336 info!("The directory {folder:?} doesn't exist, skipping.");
337 continue;
338 }
339 for pkg_folder in read_dir(&folder).map_err(|source| Error::IoPath {
343 path: folder.clone(),
344 context: "reading entries in directory".to_string(),
345 source,
346 })? {
347 let pkg_folder = pkg_folder.map_err(|source| Error::IoPath {
348 path: folder.clone(),
349 context: "reading an entry of the directory".to_string(),
350 source,
351 })?;
352
353 if pkgbase_filter.as_ref().is_some_and(|pkgbases| {
355 !pkgbases.contains(&*pkg_folder.file_name().to_string_lossy())
356 }) {
357 continue;
358 }
359 let file_path = pkg_folder.path().join(self.file_type.to_string());
360 if file_path.exists() {
361 files.push(file_path);
362 }
363 }
364 }
365
366 Ok(files)
367 }
368}
369
370fn files_in_dirs_by_extension(dirs: &[PathBuf], extension: &str) -> Result<Vec<PathBuf>, Error> {
382 let mut files = Vec::new();
383
384 for dir in dirs {
385 debug!("Looking for files in {dir:?}");
386 if !dir.exists() {
387 info!("Skipping directory {dir:?} as it does not exist.");
388 continue;
389 }
390
391 for entry in read_dir(dir).map_err(|source| Error::IoPath {
392 path: dir.clone(),
393 context: "reading entries in directory".to_string(),
394 source,
395 })? {
396 let entry = entry.map_err(|source| Error::IoPath {
397 path: dir.clone(),
398 context: "reading an entry of the directory".to_string(),
399 source,
400 })?;
401
402 let file_path = entry.path();
403 if file_path.is_file()
404 && file_path
405 .extension()
406 .is_some_and(|ext| ext.to_str() == Some(extension))
407 {
408 files.push(file_path);
409 }
410 }
411 }
412
413 Ok(files)
414}
415
416#[cfg(test)]
417mod tests {
418 use std::{
419 collections::HashSet,
420 fs::{OpenOptions, create_dir},
421 };
422
423 use rstest::rstest;
424 use testresult::TestResult;
425
426 use super::*;
427
428 const PKG_NAMES: &[&str] = &[
429 "pipewire-alsa-1:1.0.7-1-x86_64",
430 "xorg-xvinfo-1.1.5-1-x86_64",
431 "acl-2.3.2-1-x86_64",
432 "archlinux-keyring-20240520-1-any",
433 ];
434
435 #[rstest]
439 #[case(TestFileFormat::Buildinfo)]
440 #[case(TestFileFormat::Pkginfo)]
441 #[case(TestFileFormat::MTree)]
442 fn test_find_files_for_packages(#[case] file_type: TestFileFormat) -> TestResult {
443 let tmp_dir = tempfile::tempdir()?;
445 let packages_dir = tmp_dir.path().join(PACKAGES_DIR);
446 create_dir(&packages_dir)?;
447
448 let mut expected_files = HashSet::new();
450
451 for (index, repo) in PackageRepositories::iter().enumerate() {
453 let repo_dir = packages_dir.join(repo.to_string());
455 create_dir(&repo_dir)?;
456
457 let pkg = PKG_NAMES[index];
459 let pkg_dir = repo_dir.join(pkg);
460 create_dir(&pkg_dir)?;
461
462 let file_path = pkg_dir.join(file_type.to_string());
464 OpenOptions::new()
465 .create(true)
466 .write(true)
467 .truncate(true)
468 .open(&file_path)?;
469 expected_files.insert(file_path);
470 }
471
472 let runner = TestRunner {
474 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
475 file_type,
476 repositories: Some(PackageRepositories::iter().collect()),
477 };
478 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
479
480 assert_eq!(
481 found_files, expected_files,
482 "Expected that all created package files are also found."
483 );
484
485 Ok(())
486 }
487
488 #[rstest]
492 #[case(TestFileFormat::RemoteFiles)]
493 #[case(TestFileFormat::RemoteDesc)]
494 fn test_find_files_for_databases(#[case] file_type: TestFileFormat) -> TestResult {
495 let tmp_dir = tempfile::tempdir()?;
497 let databases_dir = tmp_dir.path().join(DATABASES_DIR);
498 create_dir(&databases_dir)?;
499
500 let mut expected_files = HashSet::new();
502
503 for (index, repo) in PackageRepositories::iter().enumerate() {
505 let repo_dir = databases_dir.join(repo.to_string());
507 create_dir(&repo_dir)?;
508
509 let pkg = PKG_NAMES[index];
511 let pkg_dir = repo_dir.join(pkg);
512 create_dir(&pkg_dir)?;
513
514 let file_path = pkg_dir.join(file_type.to_string());
516 OpenOptions::new()
517 .create(true)
518 .write(true)
519 .truncate(true)
520 .open(&file_path)?;
521 expected_files.insert(file_path);
522 }
523
524 let runner = TestRunner {
526 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
527 file_type,
528 repositories: Some(PackageRepositories::iter().collect()),
529 };
530 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
531
532 assert_eq!(
533 found_files, expected_files,
534 "Expected that all created databases files are also found."
535 );
536
537 Ok(())
538 }
539
540 #[rstest]
544 #[case(TestFileFormat::Srcinfo)]
545 fn test_find_files_for_pkgsrc(#[case] file_type: TestFileFormat) -> TestResult {
546 let tmp_dir = tempfile::tempdir()?;
548 let pkgsrc_dir = tmp_dir.path().join(PKGSRC_DIR);
549 create_dir(&pkgsrc_dir)?;
550
551 let mut expected_files = HashSet::new();
553
554 for pkg in PKG_NAMES {
557 let pkg_dir = pkgsrc_dir.join(pkg);
558 create_dir(&pkg_dir)?;
559
560 let file_path = pkg_dir.join(file_type.to_string());
562 OpenOptions::new()
563 .create(true)
564 .write(true)
565 .truncate(true)
566 .open(&file_path)?;
567 expected_files.insert(file_path);
568 }
569
570 let runner = TestRunner {
572 cache_dir: CacheDir::from(tmp_dir.path().to_owned()),
573 file_type,
574 repositories: None,
575 };
576 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
577
578 assert_eq!(
579 found_files, expected_files,
580 "Expected that all created pkgsrc files are also found."
581 );
582
583 Ok(())
584 }
585}