use crate::{dist::*, error::Error, util};
use chrono::{DateTime, Utc};
use hex;
use log::{debug, info};
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::{borrow::Borrow, collections::HashMap, ffi::OsStr, fs::File, io, path::Path};
mod v1;
mod v2;
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Digests {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<serde_with::hex::Hex>")]
sha1: Option<[u8; 20]>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<serde_with::hex::Hex>")]
sha256: Option<[u8; 32]>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<serde_with::hex::Hex>")]
sha512: Option<[u8; 64]>,
}
impl Digests {
pub fn sha1(&self) -> Option<&[u8; 20]> {
self.sha1.as_ref()
}
pub fn sha256(&self) -> Option<&[u8; 32]> {
self.sha256.as_ref()
}
pub fn sha512(&self) -> Option<&[u8; 64]> {
self.sha512.as_ref()
}
pub fn validate<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
info!(archive=path.as_ref().file_name().unwrap_or_else(|| OsStr::new("archive")).to_str(); "Validating");
self._validate(File::open(path)?)
}
fn _validate<P: io::Read + io::Seek>(&self, mut file: P) -> Result<(), Error> {
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
let mut ok = false;
if let Some(digest) = self.sha512() {
compare(&mut file, digest, Sha512::new(), "SHA-512")?;
info!(sha512:display=hex::encode(digest); "✅");
ok = true;
}
if let Some(digest) = self.sha256() {
compare(&mut file, digest, Sha256::new(), "SHA-256")?;
info!(sha256:display=hex::encode(digest); "✅");
ok = true;
}
if let Some(digest) = self.sha1() {
compare(&mut file, digest, Sha1::new(), "SHA-1")?;
info!(sha1:display=hex::encode(digest); "✅");
ok = true;
}
if ok {
return Ok(());
}
Err(Error::Missing("digests"))
}
}
fn compare<P, D>(mut file: P, digest: &[u8], mut hasher: D, alg: &'static str) -> Result<(), Error>
where
P: io::Read + io::Seek,
D: digest::Digest + io::Write,
{
file.rewind()?;
io::copy(&mut file, &mut hasher)?;
let hash = hasher.finalize();
if constant_time_eq::constant_time_eq(hash.as_slice(), digest) {
return Ok(());
}
Err(Error::Digest(alg, hex::encode(hash), hex::encode(digest)))
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct ReleasePayload {
user: String,
date: DateTime<Utc>,
uri: String,
digests: Digests,
}
impl ReleasePayload {
pub fn user(&self) -> &str {
self.user.as_str()
}
pub fn date(&self) -> &DateTime<Utc> {
self.date.borrow()
}
pub fn uri(&self) -> &str {
self.uri.as_str()
}
pub fn digests(&self) -> &Digests {
self.digests.borrow()
}
}
#[derive(Serialize, PartialEq, Debug)]
pub struct Release {
#[serde(flatten)]
dist: Distribution,
certs: HashMap<String, Value>,
#[serde(skip_serializing)]
release: ReleasePayload,
}
impl<'de> Deserialize<'de> for Release {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct ReleaseInitial {
#[serde(flatten)]
dist: Distribution,
certs: HashMap<String, Value>,
}
let rel = ReleaseInitial::deserialize(deserializer)?;
debug!(release:display = format!("{}-{}", rel.dist.name(), rel.dist.version());"Deserialized");
let Some(Value::Object(jws)) = rel.certs.get("pgxn") else {
return Err(de::Error::custom("invalid or missing pgxn release data"));
};
let Some(Value::String(b64)) = jws.get("payload") else {
return Err(de::Error::custom("missing or invalid pgxn payload"));
};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
let json = URL_SAFE_NO_PAD.decode(b64).map_err(de::Error::custom)?;
let pay = serde_json::from_slice(&json).map_err(de::Error::custom)?;
let mut v = crate::valid::Validator::new();
debug!(release:display = format!("{}-{}", rel.dist.name(), rel.dist.version()); "Validate");
v.validate_payload(&pay).map_err(de::Error::custom)?;
Ok(Release {
dist: rel.dist,
certs: rel.certs,
release: serde_json::from_value(pay).map_err(de::Error::custom)?,
})
}
}
impl Release {
fn from_version(version: u8, meta: Value) -> Result<Self, Error> {
match version {
1 => v1::from_value(meta),
2 => v2::from_value(meta),
_ => Err(Error::UnknownSpec),
}
}
pub fn load<P: AsRef<Path>>(file: P) -> Result<Self, Error> {
let meta: Value = serde_json::from_reader(File::open(file)?)?;
meta.try_into()
}
pub fn name(&self) -> &str {
self.dist.name()
}
pub fn version(&self) -> &semver::Version {
self.dist.version()
}
pub fn abs_tract(&self) -> &str {
self.dist.abs_tract()
}
pub fn description(&self) -> Option<&str> {
self.dist.description()
}
pub fn producer(&self) -> Option<&str> {
self.dist.producer()
}
pub fn license(&self) -> &str {
self.dist.license()
}
pub fn spec(&self) -> &Spec {
self.dist.spec()
}
pub fn maintainers(&self) -> &[Maintainer] {
self.dist.maintainers()
}
pub fn classifications(&self) -> Option<&Classifications> {
self.dist.classifications()
}
pub fn contents(&self) -> &Contents {
self.dist.contents()
}
pub fn ignore(&self) -> Option<&[String]> {
self.dist.ignore()
}
pub fn dependencies(&self) -> Option<&Dependencies> {
self.dist.dependencies()
}
pub fn resources(&self) -> Option<&Resources> {
self.dist.resources()
}
pub fn artifacts(&self) -> Option<&[Artifact]> {
self.dist.artifacts()
}
pub fn release(&self) -> &ReleasePayload {
self.release.borrow()
}
pub fn certs(&self) -> &HashMap<String, Value> {
self.certs.borrow()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.dist.custom_props()
}
}
impl TryFrom<Value> for Release {
type Error = Error;
fn try_from(meta: Value) -> Result<Self, Self::Error> {
let mut validator = crate::valid::Validator::new();
let version = validator.validate_release(&meta)?;
Release::from_version(version, meta)
}
}
impl TryFrom<&[Value]> for Release {
type Error = Error;
fn try_from(meta: &[Value]) -> Result<Self, Self::Error> {
if meta.is_empty() {
return Err(Error::Param("meta contains no values"));
}
let version = util::get_version(&meta[0]).ok_or_else(|| Error::UnknownSpec)?;
let mut v2 = match version {
1 => v1::to_v2(&meta[0])?,
2 => meta[0].clone(),
_ => unreachable!(),
};
for patch in meta[1..].iter() {
json_patch::merge(&mut v2, patch)
}
let mut validator = crate::valid::Validator::new();
validator.validate_release(&v2)?;
Release::from_version(2, v2)
}
}
impl TryFrom<Release> for Value {
type Error = Error;
fn try_from(meta: Release) -> Result<Self, Self::Error> {
let val = serde_json::to_value(meta)?;
Ok(val)
}
}
impl TryFrom<&String> for Release {
type Error = Error;
fn try_from(str: &String) -> Result<Self, Self::Error> {
let meta: Value = serde_json::from_str(str)?;
meta.try_into()
}
}
impl TryFrom<Release> for String {
type Error = Error;
fn try_from(meta: Release) -> Result<Self, Self::Error> {
let val = serde_json::to_string(&meta)?;
Ok(val)
}
}
#[cfg(test)]
mod tests;