use std::path::Path;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::channel::{Channel, ChannelAlias, UserChannel};
const MANIFEST_VERSION: semver::Version = semver::Version::new(1, 0, 1);
const HTTP_ERROR_CODES: std::ops::Range<u32> = 400..500;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Manifest {
pub manifest_version: semver::Version,
date: i64,
channels: Vec<Channel>,
}
impl Default for Manifest {
fn default() -> Self {
let date = chrono::Utc::now().timestamp();
Self {
manifest_version: MANIFEST_VERSION,
date,
channels: vec![],
}
}
}
#[derive(Error, Debug)]
pub enum ManifestError {
#[error("Manifest file is empty")]
Empty,
#[error("Webpage {0} is empty")]
EmptyWebpage(String),
#[error("Webpage returned error. Does {0} exist?")]
WebpageError(String),
#[error("Manifest file is not present in `{0}`")]
Missing(String),
#[error("Invalid channel manifest in URI: `{0}`")]
Invalid(String),
#[error("Couldn't reach webpage: `{0}`")]
InternalCurlError(String),
#[error("unsupported channel manifest URI: `{0}`")]
Unsupported(String),
}
impl Manifest {
pub const LOCAL_MANIFEST_URI: &str = "https://0xmiden.github.io/midenup/channel-manifest.json";
pub const PUBLISHED_MANIFEST_URI: &str =
"https://0xmiden.github.io/midenup/channel-manifest.json";
pub fn parse_str(content: &str) -> Result<Manifest, ManifestError> {
let mut manifest = serde_json::from_str::<Manifest>(content)
.map_err(|err| ManifestError::Invalid(format!("failed to parse manifest: {err}")))?;
if !manifest.channels.is_sorted_by_key(|channel| &channel.name) {
manifest.channels.sort_by_key(|channel| channel.name.clone());
}
for channel in manifest.channels.iter_mut() {
if !channel.components.is_sorted_by_key(|c| c.name.as_ref()) {
channel.components.sort_by_key(|c| c.name.clone());
}
}
Ok(manifest)
}
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Manifest, ManifestError> {
let path = path.as_ref();
let manifest_contents = std::fs::read_to_string(path)
.map_err(|_| ManifestError::Missing(path.display().to_string()))?;
if manifest_contents.is_empty() {
return Err(ManifestError::Empty);
}
Self::parse_str(&manifest_contents)
}
pub fn load_from(uri: impl AsRef<str>) -> Result<Manifest, ManifestError> {
let uri = uri.as_ref();
if let Some(manifest_path) = uri.strip_prefix("file://") {
return Self::load_from_file(manifest_path);
}
if !uri.starts_with("https://") {
return Err(ManifestError::Unsupported(uri.to_string()));
}
let mut data = Vec::new();
let mut handle = curl::easy::Easy::new();
handle.url(uri).map_err(|error| {
let mut err = format!("Error code {}: ", error.code());
err.push_str(error.description());
ManifestError::InternalCurlError(err)
})?;
{
let response_code = handle.response_code().map_err(|_| {
ManifestError::InternalCurlError(String::from(
"Failed to get response code; despite HTTP protocol supporting it.",
))
})?;
if HTTP_ERROR_CODES.contains(&response_code) {
return Err(ManifestError::WebpageError(uri.to_string()));
}
let mut transfer = handle.transfer();
transfer
.write_function(|new_data| {
data.extend_from_slice(new_data);
Ok(new_data.len())
})
.unwrap();
transfer.perform().map_err(|error| {
let mut err = format!("Error code {}: ", error.code());
err.push_str(error.description());
ManifestError::InternalCurlError(err)
})?
}
if data.is_empty() {
return Err(ManifestError::EmptyWebpage(uri.to_string()));
}
let manifest_data = core::str::from_utf8(&data).map_err(|err| {
ManifestError::Invalid(format!("manifest contains invalid utf8 data: {err}"))
})?;
Self::parse_str(manifest_data)
}
pub fn last_updated(&self) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::from_timestamp(self.date, 0).expect("manifest has invalid timestamp")
}
pub fn update_last_modified(&mut self) {
self.date = chrono::Utc::now().timestamp();
}
pub fn remove_channel(&mut self, channel_name: semver::Version) {
self.channels.retain(|c| c.name != channel_name);
}
pub fn add_channel(&mut self, channel: Channel) {
if self.is_latest_stable(&channel) {
for channel in self
.channels
.iter_mut()
.filter(|c| c.alias.as_ref().is_some_and(|a| matches!(a, ChannelAlias::Stable)))
{
channel.alias = None
}
}
self.channels.retain(|c| c.name != channel.name);
self.channels.push(channel);
}
pub fn is_latest_stable(&self, channel: &Channel) -> bool {
self.channels.iter().filter(|c| c.is_stable()).all(|c| {
let comparison = channel.name.cmp_precedence(&c.name);
matches!(comparison, std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
})
}
pub fn get_latest_stable(&self) -> Option<&Channel> {
self.channels
.iter()
.find(|c| matches!(c.alias, Some(ChannelAlias::Stable)))
.or_else(|| {
self.channels
.iter()
.filter(|c| c.is_stable())
.max_by(|x, y| x.name.cmp_precedence(&y.name))
})
}
pub fn get_latest_stable_mut(&mut self) -> Option<&mut Channel> {
let stable_version = self.get_latest_stable().map(|channel| channel.name.clone())?;
self.get_channel_by_name_mut(&stable_version)
}
pub fn get_latest_nightly(&self) -> Option<&Channel> {
self.channels.iter().find(|c| c.is_latest_nightly()).or_else(|| {
self.channels
.iter()
.filter(|c| c.is_nightly())
.max_by(|x, y| x.name.cmp_precedence(&y.name))
})
}
pub fn get_latest_nightly_mut(&mut self) -> Option<&mut Channel> {
let nightly_version = self.get_latest_nightly().map(|channel| channel.name.clone())?;
self.get_channel_by_name_mut(&nightly_version)
}
pub fn get_named_nightly(&self, name: impl AsRef<str>) -> Option<&Channel> {
self.channels.iter().find(|c| {
c.alias.as_ref().is_some_and(
|alias| matches!(alias, ChannelAlias::Nightly(Some(tag)) if tag == name.as_ref()),
)
})
}
pub fn get_named_nightly_mut(&mut self, name: impl AsRef<str>) -> Option<&mut Channel> {
self.channels.iter_mut().find(|c| {
c.alias.as_ref().is_some_and(
|alias| matches!(alias, ChannelAlias::Nightly(Some(tag)) if tag == name.as_ref()),
)
})
}
pub fn get_channel_by_name(&self, ver: &semver::Version) -> Option<&Channel> {
self.channels.iter().find(|c| &c.name == ver)
}
pub fn get_channel_by_name_mut(&mut self, ver: &semver::Version) -> Option<&mut Channel> {
self.channels.iter_mut().find(|c| &c.name == ver)
}
pub fn get_channel(&self, channel: &UserChannel) -> Option<&Channel> {
match channel {
UserChannel::Version(v) => self.channels.iter().find(|c| &c.name == v),
UserChannel::Stable => self.get_latest_stable(),
UserChannel::Nightly => self.get_latest_nightly(),
UserChannel::Other(tag) => match tag.strip_prefix("nightly-") {
Some(suffix) => self.get_named_nightly(suffix),
None => self.channels.iter().find(|c| {
c.alias.as_ref().is_some_and(|alias| {
matches!(alias, ChannelAlias::Tag(t) if t ==
tag.as_ref())
})
}),
},
}
}
pub fn get_channel_mut(&mut self, channel: &UserChannel) -> Option<&mut Channel> {
match channel {
UserChannel::Version(v) => self.channels.iter_mut().find(|c| &c.name == v),
UserChannel::Stable => self.get_latest_stable_mut(),
UserChannel::Nightly => self.get_latest_nightly_mut(),
UserChannel::Other(tag) => match tag.strip_prefix("nightly-") {
Some(suffix) => self.get_named_nightly_mut(suffix),
None => self.channels.iter_mut().find(|c| {
c.alias.as_ref().is_some_and(|alias| {
matches!(alias, ChannelAlias::Tag(t) if t ==
tag.as_ref())
})
}),
},
}
}
pub fn get_channels(&self) -> impl Iterator<Item = &Channel> {
self.channels.iter()
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::Manifest;
use crate::{channel::UserChannel, manifest::ChannelAlias, version::Authority};
#[test]
fn validate_current_channel_manifest() {
let manifest = Manifest::load_from("file://manifest/channel-manifest.json")
.expect("Couldn't load manifest");
let _stable = manifest
.get_channel(&UserChannel::Stable)
.expect("Could not convert UserChannel to internal channel representation");
}
#[test]
fn validate_published_channel_manifest() {
let manifest = Manifest::load_from(Manifest::PUBLISHED_MANIFEST_URI)
.expect("Failed to parse upstream manifest.");
let _ = manifest
.get_channel(&UserChannel::Stable)
.expect("Could not convert UserChannel to internal channel representation");
}
#[test]
fn unit_test_manifest_additional() {
const FILE: &str =
"file://tests/data/unit_test_manifest_additional/manifest-non-stable.json";
let manifest = Manifest::load_from(FILE).unwrap();
{
let custom_build = manifest
.get_channel(&UserChannel::Other(Cow::Borrowed("custom-dev-build")))
.unwrap_or_else(|| {
panic!(
"Could not convert UserChannel to internal channel representation from \
{FILE}",
)
});
#[allow(unused_variables)]
{
let prerelease = semver::Prerelease::new("custom-build").unwrap();
assert!(matches!(&custom_build.name, semver::Version { pre: _prerelease, .. }));
}
assert_eq!(
custom_build.alias,
Some(ChannelAlias::Tag(Cow::Borrowed("custom-dev-build")))
);
{
let std_lib = custom_build
.get_component("std")
.unwrap_or_else(|| panic!("Could not find standard library in {FILE}",));
assert!(matches!(std_lib.version, Authority::Path { .. }));
}
}
{
let nightly = manifest.get_channel(&UserChannel::Nightly).unwrap_or_else(|| {
panic!(
"Could not convert UserChannel to internal channel representation from {FILE}",
)
});
assert_eq!(nightly.alias, Some(ChannelAlias::Nightly(None)));
{
let client = nightly
.get_component("client")
.unwrap_or_else(|| panic!("Could not find standard library in {FILE}",));
assert!(matches!(client.version, Authority::Git { .. }));
}
}
}
}