runner_core/packs/
index.rs1use std::collections::BTreeMap;
2use std::fs::File;
3use std::io::{BufReader, Read};
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result, bail};
7use reqwest::blocking::Client;
8use semver::Version;
9use serde::Deserialize;
10
11use crate::env::{IndexLocation, PackSource};
12
13use super::{PackDigest, PackRef, PackVersion};
14
15#[derive(Debug, Clone)]
16pub struct Index {
17 tenants: BTreeMap<String, TenantRecord>,
18}
19
20impl Index {
21 pub fn tenants(&self) -> &BTreeMap<String, TenantRecord> {
22 &self.tenants
23 }
24
25 pub fn load(location: &IndexLocation) -> Result<Self> {
26 match location {
27 IndexLocation::File(path) => {
28 let file = File::open(path)
29 .with_context(|| format!("failed to open index {}", path.display()))?;
30 let base_dir = path.parent().map(Path::to_path_buf);
31 Self::from_reader_with_base(BufReader::new(file), base_dir.as_deref())
32 }
33 IndexLocation::Remote(url) => {
34 let client = Client::builder().build()?;
35 let response = client
36 .get(url.clone())
37 .send()
38 .with_context(|| format!("failed to fetch index {}", url))?
39 .error_for_status()
40 .with_context(|| format!("index download failed {}", url))?;
41 let bytes = response
42 .bytes()
43 .context("failed to read index response body")?;
44 Self::from_slice_with_base(&bytes, None)
45 }
46 }
47 }
48
49 pub fn from_reader<R: Read>(reader: R) -> Result<Self> {
50 Self::from_reader_with_base(reader, None)
51 }
52
53 pub fn from_reader_with_base<R: Read>(reader: R, base_dir: Option<&Path>) -> Result<Self> {
54 let raw: RawIndex = serde_json::from_reader(reader).context("index JSON is not valid")?;
55 Ok(Self {
56 tenants: raw.into_tenants(base_dir)?,
57 })
58 }
59
60 pub fn from_slice(bytes: &[u8]) -> Result<Self> {
61 Self::from_slice_with_base(bytes, None)
62 }
63
64 pub fn from_slice_with_base(bytes: &[u8], base_dir: Option<&Path>) -> Result<Self> {
65 let raw: RawIndex = serde_json::from_slice(bytes).context("index JSON is not valid")?;
66 Ok(Self {
67 tenants: raw.into_tenants(base_dir)?,
68 })
69 }
70}
71
72#[derive(Debug, Clone)]
73pub struct TenantRecord {
74 pub main_pack: PackEntry,
75 pub overlays: Vec<PackEntry>,
76}
77
78#[derive(Debug, Clone)]
79pub struct PackEntry {
80 pub reference: PackRef,
81 pub locator: PackLocator,
82 pub content_digest: Option<PackDigest>,
83 pub signature: Option<String>,
84}
85
86impl PackEntry {
87 pub fn locator(&self) -> &PackLocator {
88 &self.locator
89 }
90}
91
92#[derive(Debug, Clone)]
93pub struct PackLocator {
94 raw: String,
95}
96
97impl PackLocator {
98 pub fn new(raw: impl Into<String>) -> Self {
99 Self { raw: raw.into() }
100 }
101
102 pub fn with_fallback(&self, source: PackSource) -> Result<String> {
103 if self.raw.contains("://") {
104 return Ok(self.raw.clone());
105 }
106 match source {
107 PackSource::Fs => Ok(format!("{}://{}", source.scheme(), self.raw)),
108 _ => bail!(
109 "locator `{}` is missing a scheme; specify an explicit URI",
110 self.raw
111 ),
112 }
113 }
114}
115
116#[derive(Deserialize)]
117struct RawIndex {
118 #[serde(flatten)]
119 tenants: BTreeMap<String, RawTenantRecord>,
120}
121
122impl RawIndex {
123 fn into_tenants(self, base_dir: Option<&Path>) -> Result<BTreeMap<String, TenantRecord>> {
124 let mut tenants = BTreeMap::new();
125 for (tenant, raw) in self.tenants {
126 tenants.insert(tenant, raw.into_tenant(base_dir)?);
127 }
128 Ok(tenants)
129 }
130}
131
132#[derive(Deserialize)]
133struct RawTenantRecord {
134 main_pack: RawPackEntry,
135 #[serde(default)]
136 overlays: Vec<RawPackEntry>,
137}
138
139impl RawTenantRecord {
140 fn into_tenant(self, base_dir: Option<&Path>) -> Result<TenantRecord> {
141 Ok(TenantRecord {
142 main_pack: self.main_pack.into_entry(base_dir)?,
143 overlays: self
144 .overlays
145 .into_iter()
146 .map(|entry| entry.into_entry(base_dir))
147 .collect::<Result<Vec<_>>>()?,
148 })
149 }
150}
151
152impl TryFrom<RawTenantRecord> for TenantRecord {
153 type Error = anyhow::Error;
154
155 fn try_from(value: RawTenantRecord) -> Result<Self> {
156 value.into_tenant(None)
157 }
158}
159
160#[derive(Deserialize)]
161struct RawPackEntry {
162 name: String,
163 #[serde(default)]
164 version: Option<String>,
165 #[serde(default)]
166 digest: Option<String>,
167 locator: String,
168 #[serde(default)]
169 signature: Option<String>,
170}
171
172impl RawPackEntry {
173 fn into_entry(self, base_dir: Option<&Path>) -> Result<PackEntry> {
174 let (version, digest) = parse_version_and_digest(self.version, self.digest)?;
175 let locator = resolve_locator(&self.locator, base_dir);
176 Ok(PackEntry {
177 reference: PackRef {
178 name: self.name,
179 version,
180 },
181 locator: PackLocator::new(locator),
182 content_digest: digest,
183 signature: self.signature,
184 })
185 }
186}
187
188impl TryFrom<RawPackEntry> for PackEntry {
189 type Error = anyhow::Error;
190
191 fn try_from(value: RawPackEntry) -> Result<Self> {
192 value.into_entry(None)
193 }
194}
195
196fn parse_version_and_digest(
197 version: Option<String>,
198 digest: Option<String>,
199) -> Result<(PackVersion, Option<PackDigest>)> {
200 match (version, digest) {
201 (Some(ver), digest) => {
202 let version =
203 Version::parse(&ver).with_context(|| format!("invalid semver version `{ver}`"))?;
204 let digest = digest.map(PackDigest::parse).transpose()?;
205 Ok((PackVersion::Semver(version), digest))
206 }
207 (None, Some(digest)) => {
208 let parsed = PackDigest::parse(digest)?;
209 Ok((PackVersion::Digest(parsed.clone()), Some(parsed)))
210 }
211 (None, None) => bail!("pack entry is missing version or digest pin"),
212 }
213}
214
215fn resolve_locator(locator: &str, base_dir: Option<&Path>) -> String {
216 if locator.contains("://") {
217 return locator.to_string();
218 }
219 if let Some(base) = base_dir {
220 let path = PathBuf::from(locator);
221 if path.is_relative() {
222 return base.join(path).to_string_lossy().into_owned();
223 }
224 }
225 locator.to_string()
226}