voa/commands.rs
1//! Commands to interact with VOA.
2
3use std::{
4 collections::BTreeMap,
5 fmt::Debug,
6 io::stdin,
7 path::{Path, PathBuf},
8};
9
10use log::info;
11use voa_core::{
12 LoadPathList,
13 Verifier,
14 VerifierWriter,
15 Voa,
16 identifiers::{Context, Os, Purpose, Technology},
17};
18use voa_openpgp::{OpenPgpImport, import::destructured::load_from_dir};
19
20use crate::{
21 Error,
22 utils::{DirOrFile, DirOrFileType},
23};
24
25/// Returns a writable VOA load path.
26///
27/// Gathers the list of writable VOA load paths for the calling user and returns the first from the
28/// list.
29/// If `runtime` is `true`, the ephemeral load path of the calling user is selected instead.
30///
31/// # Errors
32///
33/// Returns an error if no [`LoadPath`][voa_core::LoadPath] can be found.
34///
35/// # Examples
36///
37/// ```
38/// use voa::commands::get_writable_load_path;
39///
40/// # fn main() -> Result<(), voa::Error> {
41/// let config_dir = get_writable_load_path(false)?;
42/// let runtime_dir = get_writable_load_path(true)?;
43/// # Ok(())
44/// # }
45/// ```
46pub fn get_writable_load_path(runtime: bool) -> Result<PathBuf, Error> {
47 let load_path_list = LoadPathList::from_effective_user();
48
49 let filter = voa_core::LoadPathFilter {
50 ephemeral: runtime,
51 writable: true,
52 };
53 let load_path = load_path_list
54 .filter(&filter)
55 .first()
56 .cloned()
57 .ok_or(Error::NoLoadPath)?;
58
59 Ok(load_path.path.clone())
60}
61
62/// Returns an implementation of [`VerifierWriter`] from an input.
63///
64/// Depending on `technology`, attempts to load a verifier from file or directory if `input` is a
65/// [`DirOrFile`]. Attempts to load a verifier from [`stdin`] if `input` is [`None`].
66///
67/// # Note
68///
69/// Currently only supports [`Technology::OpenPGP`].
70///
71/// # Errors
72///
73/// Returns an error if
74///
75/// - a verifier cannot be loaded from file/directory or stdin,
76/// - an unsupported `technology` is provided.
77///
78/// # Examples
79///
80/// ```
81/// use std::io::Write;
82///
83/// use tempfile::{NamedTempFile, tempdir};
84/// use voa::commands::load_verifier;
85///
86/// # fn main() -> testresult::TestResult {
87/// // Write a generic OpenPGP certificate to a temporary file.
88/// let cert = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
89///
90/// xjMEaNBDAhYJKwYBBAHaRw8BAQdAzjzrpQ/AEteCmzjd1xTdXGaHV0VKSm4HLy6l
91/// HVcmWT3NH0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLm9yZz7CmgQQFggAQgUC
92/// aNBDAhYhBEauMg3lOimFWKbyoPtSEBy0DfYKAhsDAh4BBAsJCAcGFQ4KCQwIARYN
93/// JwkCCAIHAgkBCAEHAQIZAQAKCRD7UhActA32CkhIAP9bhoLJeZRCAc+q1kFEkstT
94/// uXBPlzHagF6ghuUfToMmVQD+KaakONKSekglKR4rJxzhleQJ4qsptt1gjXX13QgF
95/// Xwo=
96/// =Pkv9
97/// -----END PGP PUBLIC KEY BLOCK-----"#;
98/// let mut temp_file = NamedTempFile::new()?;
99/// write!(temp_file, "{cert}")?;
100/// let input_path = temp_file.path();
101///
102/// // Load an OpenPGP verifier from file.
103/// let verifier = load_verifier(Some(input_path.try_into()?), "openpgp".parse()?)?;
104///
105/// // Loading a verifier from file for an unknown technology will fail.
106/// assert!(load_verifier(Some(input_path.try_into()?), "foo".parse()?).is_err());
107/// # Ok(())
108/// # }
109/// ```
110pub fn load_verifier(
111 input: Option<DirOrFile>,
112 technology: Technology,
113) -> Result<impl VerifierWriter + Debug, Error> {
114 match technology {
115 Technology::OpenPGP => Ok(if let Some(path) = input {
116 match path.typ {
117 DirOrFileType::Dir => load_from_dir(&path)?,
118 DirOrFileType::File => OpenPgpImport::from_file(&path)?,
119 }
120 } else {
121 OpenPgpImport::from_reader(stdin())?
122 }),
123 technology => Err(Error::UnsupportedTechnology { technology }),
124 }
125}
126
127/// Writes a `verifier` to a VOA hierarchy in a directory.
128///
129/// The [`VerifierWriter`] implementation writes its on-disk representation to a specific location
130/// in a VOA hierarchy in a VOA base path directory based on the `os`, `purpose` and optional
131/// `context` identifier.
132///
133/// # Errors
134///
135/// Returns an error if [`VerifierWriter::write_to_hierarchy`] fails.
136///
137/// # Examples
138///
139/// ```
140/// use std::io::Write;
141///
142/// use tempfile::{NamedTempFile, tempdir};
143/// use voa::commands::{load_verifier, write_verifier_to_hierarchy};
144///
145/// # fn main() -> testresult::TestResult {
146/// // Write a generic OpenPGP certificate to a temporary file.
147/// let cert = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
148///
149/// xjMEaNBDAhYJKwYBBAHaRw8BAQdAzjzrpQ/AEteCmzjd1xTdXGaHV0VKSm4HLy6l
150/// HVcmWT3NH0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLm9yZz7CmgQQFggAQgUC
151/// aNBDAhYhBEauMg3lOimFWKbyoPtSEBy0DfYKAhsDAh4BBAsJCAcGFQ4KCQwIARYN
152/// JwkCCAIHAgkBCAEHAQIZAQAKCRD7UhActA32CkhIAP9bhoLJeZRCAc+q1kFEkstT
153/// uXBPlzHagF6ghuUfToMmVQD+KaakONKSekglKR4rJxzhleQJ4qsptt1gjXX13QgF
154/// Xwo=
155/// =Pkv9
156/// -----END PGP PUBLIC KEY BLOCK-----"#;
157/// let mut temp_file = NamedTempFile::new()?;
158/// write!(temp_file, "{cert}")?;
159/// let input_path = temp_file.path();
160/// // Load an OpenPGP verifier from file.
161/// let verifier = load_verifier(Some(input_path.try_into()?), "openpgp".parse()?)?;
162/// // Prepare a temporary output directory.
163/// let temp_dir = tempdir()?;
164///
165/// // Write a verifier to a location in a temporary VOA hierarchy.
166/// write_verifier_to_hierarchy(verifier, temp_dir, "os".parse()?, "packages".parse()?, None)?;
167/// # Ok(())
168/// # }
169/// ```
170pub fn write_verifier_to_hierarchy(
171 verifier: impl VerifierWriter,
172 base_path: impl AsRef<Path>,
173 os: Os,
174 purpose: Purpose,
175 context: Option<Context>,
176) -> Result<(), Error> {
177 let base_path = base_path.as_ref();
178 info!("Writing verifier to VOA base path: {base_path:?}");
179 verifier.write_to_hierarchy(base_path, os, purpose, context)?;
180 Ok(())
181}
182
183/// Searches for all verifiers matching a query of VOA identifiers.
184pub fn search_verifiers(
185 os: Os,
186 purpose: Purpose,
187 context: Option<Context>,
188 technology: Option<Technology>,
189) -> Result<BTreeMap<PathBuf, Vec<Verifier>>, Error> {
190 let context = if let Some(context) = context {
191 context
192 } else {
193 Context::Default
194 };
195 let technology = if let Some(technology) = technology {
196 technology
197 } else {
198 Technology::OpenPGP
199 };
200
201 let voa = Voa::new();
202 let verifiers = voa.lookup(os, purpose, context, technology);
203
204 Ok(verifiers)
205}
206
207#[cfg(test)]
208mod tests {
209 use libc::geteuid;
210 use rstest::rstest;
211 use testresult::TestResult;
212
213 use super::*;
214
215 #[rstest]
216 #[case::runtime_dir(true)]
217 #[case::config_dir(false)]
218 fn get_writable_load_path_succeeds(#[case] runtime: bool) -> TestResult {
219 let load_path = get_writable_load_path(runtime)?;
220
221 let euid = unsafe { geteuid() };
222
223 eprintln!("Load path: {load_path:?}");
224 if runtime {
225 assert!(load_path.starts_with("/run"))
226 } else if euid < 1000 {
227 assert_eq!(load_path, PathBuf::from("/etc/voa"))
228 } else {
229 assert!(load_path.ends_with(".config/voa"))
230 }
231
232 Ok(())
233 }
234
235 #[test]
236 fn load_verifier_fails_on_unsupported_technology() -> TestResult {
237 let result = load_verifier(None, Technology::Custom("foo".parse()?));
238 match result {
239 Err(Error::UnsupportedTechnology { .. }) => {}
240 Err(error) => panic!("Did not raise Error::UnsupportedTechnology but {error}"),
241 Ok(verifier) => {
242 panic!("Is expected to fail, but succeeded to load verifier: {verifier:?}")
243 }
244 }
245
246 Ok(())
247 }
248}