use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use reqwest::blocking::Client;
use semver::Version;
use serde::Deserialize;
use crate::env::{IndexLocation, PackSource};
use super::{PackDigest, PackRef, PackVersion};
#[derive(Debug, Clone)]
pub struct Index {
tenants: BTreeMap<String, TenantRecord>,
}
impl Index {
pub fn tenants(&self) -> &BTreeMap<String, TenantRecord> {
&self.tenants
}
pub fn load(location: &IndexLocation) -> Result<Self> {
match location {
IndexLocation::File(path) => {
let file = File::open(path)
.with_context(|| format!("failed to open index {}", path.display()))?;
let base_dir = path.parent().map(Path::to_path_buf);
Self::from_reader_with_base(BufReader::new(file), base_dir.as_deref())
}
IndexLocation::Remote(url) => {
let client = Client::builder().build()?;
let response = client
.get(url.clone())
.send()
.with_context(|| format!("failed to fetch index {}", url))?
.error_for_status()
.with_context(|| format!("index download failed {}", url))?;
let bytes = response
.bytes()
.context("failed to read index response body")?;
Self::from_slice_with_base(&bytes, None)
}
}
}
pub fn from_reader<R: Read>(reader: R) -> Result<Self> {
Self::from_reader_with_base(reader, None)
}
pub fn from_reader_with_base<R: Read>(reader: R, base_dir: Option<&Path>) -> Result<Self> {
let raw: RawIndex = serde_json::from_reader(reader).context("index JSON is not valid")?;
Ok(Self {
tenants: raw.into_tenants(base_dir)?,
})
}
pub fn from_slice(bytes: &[u8]) -> Result<Self> {
Self::from_slice_with_base(bytes, None)
}
pub fn from_slice_with_base(bytes: &[u8], base_dir: Option<&Path>) -> Result<Self> {
let raw: RawIndex = serde_json::from_slice(bytes).context("index JSON is not valid")?;
Ok(Self {
tenants: raw.into_tenants(base_dir)?,
})
}
}
#[derive(Debug, Clone)]
pub struct TenantRecord {
pub main_pack: PackEntry,
pub overlays: Vec<PackEntry>,
}
#[derive(Debug, Clone)]
pub struct PackEntry {
pub reference: PackRef,
pub locator: PackLocator,
pub content_digest: Option<PackDigest>,
pub signature: Option<String>,
}
impl PackEntry {
pub fn locator(&self) -> &PackLocator {
&self.locator
}
}
#[derive(Debug, Clone)]
pub struct PackLocator {
raw: String,
}
impl PackLocator {
pub fn new(raw: impl Into<String>) -> Self {
Self { raw: raw.into() }
}
pub fn with_fallback(&self, source: PackSource) -> Result<String> {
if self.raw.contains("://") {
return Ok(self.raw.clone());
}
match source {
PackSource::Fs => Ok(format!("{}://{}", source.scheme(), self.raw)),
_ => bail!(
"locator `{}` is missing a scheme; specify an explicit URI",
self.raw
),
}
}
}
#[derive(Deserialize)]
struct RawIndex {
#[serde(flatten)]
tenants: BTreeMap<String, RawTenantRecord>,
}
impl RawIndex {
fn into_tenants(self, base_dir: Option<&Path>) -> Result<BTreeMap<String, TenantRecord>> {
let mut tenants = BTreeMap::new();
for (tenant, raw) in self.tenants {
tenants.insert(tenant, raw.into_tenant(base_dir)?);
}
Ok(tenants)
}
}
#[derive(Deserialize)]
struct RawTenantRecord {
main_pack: RawPackEntry,
#[serde(default)]
overlays: Vec<RawPackEntry>,
}
impl RawTenantRecord {
fn into_tenant(self, base_dir: Option<&Path>) -> Result<TenantRecord> {
Ok(TenantRecord {
main_pack: self.main_pack.into_entry(base_dir)?,
overlays: self
.overlays
.into_iter()
.map(|entry| entry.into_entry(base_dir))
.collect::<Result<Vec<_>>>()?,
})
}
}
impl TryFrom<RawTenantRecord> for TenantRecord {
type Error = anyhow::Error;
fn try_from(value: RawTenantRecord) -> Result<Self> {
value.into_tenant(None)
}
}
#[derive(Deserialize)]
struct RawPackEntry {
name: String,
#[serde(default)]
version: Option<String>,
#[serde(default)]
digest: Option<String>,
locator: String,
#[serde(default)]
signature: Option<String>,
}
impl RawPackEntry {
fn into_entry(self, base_dir: Option<&Path>) -> Result<PackEntry> {
let (version, digest) = parse_version_and_digest(self.version, self.digest)?;
let locator = resolve_locator(&self.locator, base_dir);
Ok(PackEntry {
reference: PackRef {
name: self.name,
version,
},
locator: PackLocator::new(locator),
content_digest: digest,
signature: self.signature,
})
}
}
impl TryFrom<RawPackEntry> for PackEntry {
type Error = anyhow::Error;
fn try_from(value: RawPackEntry) -> Result<Self> {
value.into_entry(None)
}
}
fn parse_version_and_digest(
version: Option<String>,
digest: Option<String>,
) -> Result<(PackVersion, Option<PackDigest>)> {
match (version, digest) {
(Some(ver), digest) => {
let version =
Version::parse(&ver).with_context(|| format!("invalid semver version `{ver}`"))?;
let digest = digest.map(PackDigest::parse).transpose()?;
Ok((PackVersion::Semver(version), digest))
}
(None, Some(digest)) => {
let parsed = PackDigest::parse(digest)?;
Ok((PackVersion::Digest(parsed.clone()), Some(parsed)))
}
(None, None) => bail!("pack entry is missing version or digest pin"),
}
}
fn resolve_locator(locator: &str, base_dir: Option<&Path>) -> String {
if locator.contains("://") {
return locator.to_string();
}
if let Some(base) = base_dir {
let path = PathBuf::from(locator);
if path.is_relative() {
return base.join(path).to_string_lossy().into_owned();
}
}
locator.to_string()
}