1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fs::File;
4use std::io::{BufReader, Read};
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context, Result, anyhow, bail};
8use greentic_pack::builder::PackManifest;
9use greentic_pack::reader::{PackLoad, SigningPolicy, VerifyReport, open_pack};
10use semver::Version;
11
12use crate::env::PackConfig;
13
14pub use cache::PackCache;
15pub use index::{Index, PackEntry, TenantRecord};
16pub use resolver::{FetchResponse, ResolverRegistry};
17pub use verify::PackVerifier;
18
19mod cache;
20mod index;
21pub mod resolver;
22mod verify;
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct PackRef {
27 pub name: String,
28 pub version: PackVersion,
29}
30
31impl PackRef {
32 pub fn cache_key(&self) -> String {
33 format!(
34 "{}-{}",
35 sanitize_segment(&self.name),
36 self.version.cache_label()
37 )
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub enum PackVersion {
44 Semver(Version),
45 Digest(PackDigest),
46}
47
48impl PackVersion {
49 pub fn cache_label(&self) -> Cow<'_, str> {
50 match self {
51 Self::Semver(v) => Cow::Owned(v.to_string()),
52 Self::Digest(digest) => Cow::Owned(digest.cache_label()),
53 }
54 }
55
56 pub fn as_digest(&self) -> Option<&PackDigest> {
57 match self {
58 Self::Digest(digest) => Some(digest),
59 _ => None,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub struct PackDigest {
67 raw: String,
68 algorithm: String,
69 value: String,
70}
71
72impl PackDigest {
73 pub fn parse(raw: impl Into<String>) -> Result<Self> {
74 let raw_string = raw.into();
75 let (algorithm, value) = {
76 let (algorithm_raw, value_raw) = raw_string
77 .split_once(':')
78 .ok_or_else(|| anyhow!("invalid digest `{raw_string}`; expected algo:value"))?;
79 if algorithm_raw.is_empty() || value_raw.is_empty() {
80 bail!("invalid digest format `{raw_string}`");
81 }
82 (algorithm_raw.to_ascii_lowercase(), value_raw.to_string())
83 };
84 Ok(Self {
85 raw: raw_string,
86 algorithm,
87 value,
88 })
89 }
90
91 pub fn sha256_from_bytes(bytes: &[u8]) -> Self {
92 use sha2::{Digest, Sha256};
93 let mut hasher = Sha256::new();
94 hasher.update(bytes);
95 let digest = hasher.finalize();
96 Self {
97 raw: format!("sha256:{digest:0x}"),
98 algorithm: "sha256".into(),
99 value: format!("{digest:0x}"),
100 }
101 }
102
103 pub fn algorithm(&self) -> &str {
104 &self.algorithm
105 }
106
107 pub fn value(&self) -> &str {
108 &self.value
109 }
110
111 pub fn as_str(&self) -> &str {
112 &self.raw
113 }
114
115 pub fn raw_string(&self) -> String {
116 self.raw.clone()
117 }
118
119 pub fn cache_label(&self) -> String {
120 self.raw.replace(':', "_")
121 }
122
123 pub fn matches_file(&self, path: &Path) -> Result<bool> {
124 let computed = compute_digest(path)?;
125 Ok(computed.raw.eq_ignore_ascii_case(&self.raw))
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct ResolvedPack {
132 pub reference: PackRef,
133 pub locator: String,
134 pub path: PathBuf,
135 pub manifest: PackManifest,
136 pub digest: PackDigest,
137 pub report: VerifyReport,
138}
139
140#[derive(Debug, Clone)]
142pub struct TenantPacks {
143 pub main: ResolvedPack,
144 pub overlays: Vec<ResolvedPack>,
145}
146
147#[derive(Debug, Clone)]
148pub struct ResolvedSet {
149 pub tenants: BTreeMap<String, TenantPacks>,
150}
151
152impl ResolvedSet {
153 pub fn tenants(&self) -> &BTreeMap<String, TenantPacks> {
154 &self.tenants
155 }
156}
157
158pub struct PackManager {
160 cfg: PackConfig,
161 cache: PackCache,
162 registry: ResolverRegistry,
163 verifier: Option<PackVerifier>,
164}
165
166impl PackManager {
167 pub fn new(cfg: PackConfig) -> Result<Self> {
168 let verifier = cfg
169 .public_key
170 .as_deref()
171 .map(PackVerifier::from_env_value)
172 .transpose()?;
173 let mut registry = ResolverRegistry::default();
174 registry.register_builtin()?;
175 Ok(Self {
176 cache: PackCache::new(cfg.cache_dir.clone()),
177 cfg,
178 registry,
179 verifier,
180 })
181 }
182
183 pub fn resolve_all_for_index(&self, index: &Index) -> Result<ResolvedSet> {
185 let mut tenants = BTreeMap::new();
186 for (tenant, record) in index.tenants() {
187 let main = self.resolve_entry(&record.main_pack)?;
188 let mut overlays = Vec::new();
189 for overlay in &record.overlays {
190 overlays.push(self.resolve_entry(overlay)?);
191 }
192 tenants.insert(tenant.clone(), TenantPacks { main, overlays });
193 }
194 Ok(ResolvedSet { tenants })
195 }
196
197 fn resolve_entry(&self, entry: &PackEntry) -> Result<ResolvedPack> {
198 let locator = entry
199 .locator
200 .with_fallback(self.cfg.source)
201 .context("pack locator missing scheme")?;
202 let response = self
203 .registry
204 .fetch(&locator)
205 .with_context(|| format!("resolver failed for {}", locator))?;
206
207 let fetched_digest = compute_digest(response.path())?;
208 if entry
209 .content_digest
210 .as_ref()
211 .or_else(|| entry.reference.version.as_digest())
212 .map(|expected| {
213 expected
214 .as_str()
215 .eq_ignore_ascii_case(fetched_digest.as_str())
216 })
217 == Some(false)
218 {
219 let expected = entry
220 .content_digest
221 .as_ref()
222 .or_else(|| entry.reference.version.as_digest())
223 .map(|value| value.as_str())
224 .unwrap_or("<unknown>");
225 bail!(
226 "digest mismatch for {}: expected {}, found {}",
227 entry.reference.name,
228 expected,
229 fetched_digest.as_str()
230 );
231 }
232
233 if let Some(verifier) = &self.verifier {
234 let signature = entry.signature.as_deref().ok_or_else(|| {
235 anyhow!(
236 "signature missing for pack {} but PACK_PUBLIC_KEY is configured",
237 entry.reference.name
238 )
239 })?;
240 verifier.verify(fetched_digest.as_str().as_bytes(), signature)?;
241 }
242
243 let cached = self.cache.store(entry, response.path(), &fetched_digest)?;
244 let PackLoad {
245 manifest, report, ..
246 } = open_pack(&cached, SigningPolicy::DevOk).map_err(|err| {
247 anyhow!(
248 "failed to open cached pack {}: {}",
249 cached.display(),
250 err.message
251 )
252 })?;
253
254 Ok(ResolvedPack {
255 reference: entry.reference.clone(),
256 locator,
257 path: cached,
258 manifest,
259 digest: fetched_digest,
260 report,
261 })
262 }
263}
264
265fn compute_digest(path: &Path) -> Result<PackDigest> {
266 use sha2::{Digest, Sha256};
267 const BUF_SIZE: usize = 64 * 1024;
268 let file = File::open(path)
269 .with_context(|| format!("failed to open {} for hashing", path.display()))?;
270 let mut reader = BufReader::new(file);
271 let mut hasher = Sha256::new();
272 let mut buf = [0u8; BUF_SIZE];
273 loop {
274 let read = reader.read(&mut buf)?;
275 if read == 0 {
276 break;
277 }
278 hasher.update(&buf[..read]);
279 }
280 let digest = hasher.finalize();
281 PackDigest::parse(format!("sha256:{digest:0x}"))
282}
283
284fn sanitize_segment(value: &str) -> String {
285 value
286 .chars()
287 .map(|ch| match ch {
288 '/' | '\\' | ':' => '_',
289 other => other,
290 })
291 .collect()
292}