use base64::prelude::*;
use blake3::Hash;
use monostate::MustBe;
use semver::{Version, VersionReq};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{BTreeMap, BTreeSet};
struct HashDef;
impl HashDef {
fn deserialize<'de, D>(deserializer: D) -> Result<Hash, D::Error>
where
D: Deserializer<'de>
{
let parts = <(u64, u64, u64, u64)>::deserialize(deserializer)?;
let mut hash = [0u8; 32];
hash[0 .. 8].clone_from_slice(&parts.0.to_be_bytes());
hash[8 .. 16].clone_from_slice(&parts.1.to_be_bytes());
hash[16 .. 24].clone_from_slice(&parts.2.to_be_bytes());
hash[24 .. 32].clone_from_slice(&parts.3.to_be_bytes());
Ok(hash.into())
}
fn serialize<S>(this: &Hash, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
let hash = this.as_bytes();
let parts = (
u64::from_be_bytes((&hash[0 .. 8]).try_into().unwrap()),
u64::from_be_bytes((&hash[8 .. 16]).try_into().unwrap()),
u64::from_be_bytes((&hash[16 .. 24]).try_into().unwrap()),
u64::from_be_bytes((&hash[24 .. 32]).try_into().unwrap())
);
parts.serialize(serializer)
}
}
#[derive(Deserialize, Eq, PartialEq, PartialOrd, Ord, Serialize)]
#[rustfmt::skip]
struct Dependency(
String,
Option<Version>,
#[serde(skip_serializing_if = "Option::is_none", default)]
Option<String>
);
impl Dependency {
fn new(crate_name: String, version: Option<Version>, lib_name: String) -> Self {
let lib_name = (lib_name != crate_name).then(|| lib_name);
Self(crate_name, version, lib_name)
}
fn crate_name(&self) -> &str {
&self.0
}
fn version(&self) -> Option<&Version> {
self.1.as_ref()
}
fn lib_name(&self) -> &str {
self.2.as_deref().unwrap_or_else(|| self.crate_name())
}
}
#[derive(Deserialize, Serialize)]
struct DependencyInfoV1 {
#[serde(rename = "m")]
markdown_version: u8,
#[serde(rename = "t", with = "HashDef")]
template_hash: Hash,
#[serde(rename = "r", with = "HashDef")]
rustdoc_hash: Hash,
#[serde(rename = "d")]
dependencies: BTreeSet<Dependency>
}
#[derive(Deserialize, Serialize)]
#[serde(untagged)]
enum DependencyInfoImpl {
V1(MustBe!(1u8), DependencyInfoV1)
}
impl DependencyInfoImpl {
fn new(markdown_version: u8, template: &str, rustdoc: &str) -> Self {
Self::V1(Default::default(), DependencyInfoV1 {
markdown_version,
template_hash: blake3::hash(template.as_bytes()),
rustdoc_hash: blake3::hash(rustdoc.as_bytes()),
dependencies: BTreeSet::new()
})
}
fn markdown_version(&self) -> u8 {
match self {
Self::V1(_, info) => info.markdown_version
}
}
fn is_template_up2date(&self, template: &str) -> bool {
let hash = blake3::hash(template.as_bytes());
match self {
Self::V1(_, info) => info.template_hash == hash
}
}
fn is_rustdoc_up2date(&self, rustdoc: &str) -> bool {
let hash = blake3::hash(rustdoc.as_bytes());
match self {
Self::V1(_, info) => info.rustdoc_hash == hash
}
}
fn is_empty(&self) -> bool {
match self {
Self::V1(_, info) => info.dependencies.is_empty()
}
}
fn dependencies(&self) -> BTreeMap<&str, (Option<&Version>, &str)> {
match self {
Self::V1(_, info) => info
.dependencies
.iter()
.map(|dep| (dep.crate_name(), (dep.version(), dep.lib_name())))
.collect()
}
}
fn add_dependency(
&mut self,
crate_name: String,
version: Option<Version>,
lib_name: String
) {
match self {
Self::V1(_, info) => {
info.dependencies
.insert(Dependency::new(crate_name, version, lib_name));
}
}
}
}
pub struct DependencyInfo(DependencyInfoImpl);
impl DependencyInfo {
#[inline]
pub fn markdown_version() -> u8 {
1
}
pub fn new(template: &str, rustdoc: &str) -> Self {
Self(DependencyInfoImpl::new(
Self::markdown_version(),
template,
rustdoc
))
}
pub fn decode(data: String) -> anyhow::Result<Self> {
let bytes = BASE64_URL_SAFE_NO_PAD.decode(data)?;
Ok(Self(serde_cbor::from_slice(&bytes)?))
}
pub fn encode(&self) -> String {
BASE64_URL_SAFE_NO_PAD.encode(serde_cbor::to_vec(&self.0).unwrap())
}
pub fn check_input(&self, template: &str, rustdoc: &str) -> bool {
self.0.is_template_up2date(template) && self.0.is_rustdoc_up2date(rustdoc)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn add_dependency(
&mut self,
crate_name: String,
version: Option<Version>,
lib_name: String
) {
self.0.add_dependency(crate_name, version, lib_name)
}
pub fn check_outdated(&self) -> bool {
self.0.markdown_version() != Self::markdown_version()
}
pub fn check_dependency(
&self,
crate_name: &str,
req: Option<&VersionReq>,
lib_name: &str,
allow_missing: bool
) -> bool {
let dependencies = self.0.dependencies();
let (dep_ver, dep_lib_name) = match dependencies.get(crate_name) {
Some(dep) => dep,
None => return allow_missing
};
if lib_name != *dep_lib_name {
return false;
}
if let Some(req) = req {
match dep_ver {
None => return false,
Some(dep_ver) if !req.matches(dep_ver) => return false,
_ => {}
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::DependencyInfo;
use base64::prelude::*;
use semver::Version;
const TEMPLATE: &str = include_str!("README.j2");
const RUSTDOC: &str = "This is the best crate ever!";
macro_rules! req {
(^ $major:tt) => {
req_impl!($major, None, None)
};
(^ $major:tt, $minor:tt) => {
req_impl!($major, Some($minor), None)
};
(^ $major:tt, $minor:tt, $patch:tt) => {
req_impl!($major, Some($minor), Some($patch))
};
}
macro_rules! req_impl {
($major:expr, $minor:expr, $patch:expr) => {{
let req: semver::VersionReq = [semver::Comparator {
op: semver::Op::Caret,
major: $major,
minor: $minor,
patch: $patch,
pre: Default::default()
}]
.into_iter()
.collect();
req
}};
}
#[test]
fn test_dep_info() {
let mut dep_info = DependencyInfo::new(TEMPLATE, RUSTDOC);
assert!(dep_info.check_input(TEMPLATE, RUSTDOC));
assert!(!dep_info.check_input(TEMPLATE, ""));
assert!(!dep_info.check_input("", RUSTDOC));
assert!(dep_info.is_empty());
assert!(!dep_info.check_dependency("anyhow", None, "anyhow", false));
assert!(dep_info.check_dependency("anyhow", None, "anyhow", true));
let version_1_0_1: Version = "1.0.1".parse().unwrap();
let req_1 = req!(^1);
let req_1_0_1 = req!(^1,0,1);
let req_1_1 = req!(^1,1);
dep_info.add_dependency("anyhow".into(), Some(version_1_0_1), "anyhow".into());
assert!(dep_info.check_dependency("anyhow", None, "anyhow", false));
assert!(dep_info.check_dependency("anyhow", Some(&req_1), "anyhow", false));
assert!(dep_info.check_dependency("anyhow", Some(&req_1_0_1), "anyhow", false));
assert!(!dep_info.check_dependency("anyhow", Some(&req_1_1), "anyhow", false));
assert!(!dep_info.check_dependency("anyhow", Some(&req_1), "any_how", false));
let encoded = dep_info.encode();
println!(
"encoded: {}",
hex::encode_upper(BASE64_URL_SAFE_NO_PAD.decode(&encoded).unwrap())
);
let dep_info = DependencyInfo::decode(encoded).unwrap();
assert!(dep_info.check_input(TEMPLATE, RUSTDOC));
assert!(dep_info.check_dependency("anyhow", Some(&req_1_0_1), "anyhow", false));
}
}