Skip to main content

runner_core/packs/
index.rs

1use 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}