1use greentic_config_types::PathsConfig;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use anyhow::{Context, Result, anyhow, bail};
6use url::Url;
7
8#[derive(Debug, Clone)]
10pub struct PackConfig {
11 pub source: PackSource,
12 pub index_location: IndexLocation,
13 pub cache_dir: PathBuf,
14 pub public_key: Option<String>,
15 pub network: Option<greentic_config_types::NetworkConfig>,
16}
17
18impl PackConfig {
19 pub fn default_for_paths(paths: &PathsConfig) -> Result<Self> {
21 let cache_dir = paths.cache_dir.join("packs");
22 let default_index = paths.greentic_root.join("index.json");
23 let index_location = if default_index.exists() {
24 IndexLocation::File(default_index)
25 } else if Path::new("examples/index.json").exists() {
26 IndexLocation::File(PathBuf::from("examples/index.json"))
27 } else {
28 IndexLocation::File(default_index)
29 };
30 Ok(Self {
31 source: PackSource::Fs,
32 index_location,
33 cache_dir,
34 public_key: None,
35 network: None,
36 })
37 }
38
39 pub fn from_packs(cfg: &greentic_config_types::PacksConfig) -> Result<Self> {
41 let index_location = match &cfg.source {
42 greentic_config_types::PackSourceConfig::LocalIndex { path } => {
43 IndexLocation::File(path.clone())
44 }
45 greentic_config_types::PackSourceConfig::HttpIndex { url } => {
46 IndexLocation::from_value(url)?
47 }
48 greentic_config_types::PackSourceConfig::OciRegistry { reference } => {
49 IndexLocation::from_value(reference)?
50 }
51 };
52 let public_key = cfg
53 .trust
54 .as_ref()
55 .and_then(|trust| trust.public_keys.first().cloned());
56 Ok(Self {
57 source: PackSource::Fs,
58 index_location,
59 cache_dir: cfg.cache_dir.clone(),
60 public_key,
61 network: None,
62 })
63 }
64}
65
66#[derive(Debug, Clone)]
68pub enum IndexLocation {
69 File(PathBuf),
70 Remote(Url),
71}
72
73impl IndexLocation {
74 pub fn from_value(value: &str) -> Result<Self> {
75 if value.starts_with("http://") || value.starts_with("https://") {
76 let url = Url::parse(value).context("PACK_INDEX_URL is not a valid URL")?;
77 return Ok(Self::Remote(url));
78 }
79 if value.starts_with("file://") {
80 let url = Url::parse(value).context("PACK_INDEX_URL is not a valid file:// URL")?;
81 let path = url
82 .to_file_path()
83 .map_err(|_| anyhow!("PACK_INDEX_URL points to an invalid file URI"))?;
84 return Ok(Self::File(path));
85 }
86 Ok(Self::File(PathBuf::from(value)))
87 }
88
89 pub fn display(&self) -> String {
90 match self {
91 Self::File(path) => path.display().to_string(),
92 Self::Remote(url) => url.to_string(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PackSource {
100 Fs,
101 Http,
102 Oci,
103 S3,
104 Gcs,
105 AzBlob,
106}
107
108impl PackSource {
109 pub fn scheme(self) -> &'static str {
110 match self {
111 Self::Fs => "fs",
112 Self::Http => "http",
113 Self::Oci => "oci",
114 Self::S3 => "s3",
115 Self::Gcs => "gcs",
116 Self::AzBlob => "azblob",
117 }
118 }
119}
120
121impl FromStr for PackSource {
122 type Err = anyhow::Error;
123
124 fn from_str(value: &str) -> Result<Self> {
125 match value.to_ascii_lowercase().as_str() {
126 "fs" => Ok(Self::Fs),
127 "http" | "https" => Ok(Self::Http),
128 "oci" => Ok(Self::Oci),
129 "s3" => Ok(Self::S3),
130 "gcs" => Ok(Self::Gcs),
131 "azblob" | "azure" | "azureblob" => Ok(Self::AzBlob),
132 other => bail!("unsupported PACK_SOURCE `{other}`"),
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use greentic_config_types::{PackSourceConfig, PackTrustConfig, PacksConfig, PathsConfig};
141
142 fn paths(root: &std::path::Path) -> PathsConfig {
143 PathsConfig {
144 greentic_root: root.join("greentic"),
145 state_dir: root.join("state"),
146 cache_dir: root.join("cache"),
147 logs_dir: root.join("logs"),
148 }
149 }
150
151 #[test]
152 fn index_location_parses_remote_and_file_values() {
153 match IndexLocation::from_value("https://example.com/index.json").unwrap() {
154 IndexLocation::Remote(url) => {
155 assert_eq!(url.as_str(), "https://example.com/index.json")
156 }
157 IndexLocation::File(_) => panic!("expected remote index"),
158 }
159
160 match IndexLocation::from_value("file:///tmp/index.json").unwrap() {
161 IndexLocation::File(path) => assert_eq!(path, PathBuf::from("/tmp/index.json")),
162 IndexLocation::Remote(_) => panic!("expected file index"),
163 }
164
165 match IndexLocation::from_value("relative/index.json").unwrap() {
166 IndexLocation::File(path) => assert_eq!(path, PathBuf::from("relative/index.json")),
167 IndexLocation::Remote(_) => panic!("expected local file"),
168 }
169 }
170
171 #[test]
172 fn default_for_paths_prefers_greentic_root_index_when_present() {
173 let temp = tempfile::tempdir_in(std::env::current_dir().expect("cwd")).expect("tempdir");
174 let path_cfg = paths(temp.path());
175 std::fs::create_dir_all(&path_cfg.cache_dir).expect("cache dir");
176 std::fs::create_dir_all(&path_cfg.greentic_root).expect("greentic root");
177 let expected_index = path_cfg.greentic_root.join("index.json");
178 std::fs::write(&expected_index, "{}").expect("index file");
179
180 let config = PackConfig::default_for_paths(&path_cfg).expect("default config");
181 match config.index_location {
182 IndexLocation::File(path) => assert_eq!(path, expected_index),
183 IndexLocation::Remote(_) => panic!("expected local example index"),
184 }
185 assert_eq!(config.cache_dir, path_cfg.cache_dir.join("packs"));
186 }
187
188 #[test]
189 fn from_packs_uses_cache_and_public_key() {
190 let cfg = PacksConfig {
191 source: PackSourceConfig::HttpIndex {
192 url: "https://example.com/index.json".into(),
193 },
194 cache_dir: PathBuf::from("/tmp/packs-cache"),
195 index_cache_ttl_secs: None,
196 trust: Some(PackTrustConfig {
197 public_keys: vec!["ed25519:abc".into()],
198 require_signatures: true,
199 }),
200 };
201
202 let pack = PackConfig::from_packs(&cfg).expect("pack config");
203 assert_eq!(pack.cache_dir, PathBuf::from("/tmp/packs-cache"));
204 assert_eq!(pack.public_key.as_deref(), Some("ed25519:abc"));
205 match pack.index_location {
206 IndexLocation::Remote(url) => {
207 assert_eq!(url.as_str(), "https://example.com/index.json")
208 }
209 IndexLocation::File(_) => panic!("expected remote index"),
210 }
211 }
212
213 #[test]
214 fn pack_source_accepts_aliases() {
215 assert_eq!(PackSource::from_str("https").unwrap(), PackSource::Http);
216 assert_eq!(
217 PackSource::from_str("azureblob").unwrap(),
218 PackSource::AzBlob
219 );
220 assert_eq!(PackSource::AzBlob.scheme(), "azblob");
221 assert!(PackSource::from_str("ftp").is_err());
222 }
223}