use crate::canonical::{CanonicalError, CanonicalSerializer};
use crate::{
Completeness, CompletenessProof, GraphRevision, NotModified, PlanError, PlanErrorCode,
RefactorOperation, RefactorPlan, RefactorPlanLimits, StatusCode, UncertainReference,
WarningCode,
};
use blazingly_json::Value;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
use sha2::{Digest, Sha256};
use std::{fmt, io, io::Write as _, str::FromStr};
pub const FINGERPRINT_ALGORITHM: &str = "weavatrix.refactor-plan.jcs-sha256.v1";
const DOMAIN_SEPARATOR: &[u8] = b"weavatrix.refactor-plan.jcs-sha256.v1\0";
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PlanFingerprint([u8; 32]);
impl PlanFingerprint {
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn to_hex(self) -> String {
let mut output = String::with_capacity(64);
for byte in self.0 {
use fmt::Write as _;
write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
}
output
}
}
impl fmt::Display for PlanFingerprint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_hex())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FingerprintParseError;
impl fmt::Display for FingerprintParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("fingerprint must be 64 lowercase hexadecimal characters")
}
}
impl std::error::Error for FingerprintParseError {}
impl FromStr for PlanFingerprint {
type Err = FingerprintParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.len() != 64
|| !value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
{
return Err(FingerprintParseError);
}
let mut bytes = [0_u8; 32];
for (index, output) in bytes.iter_mut().enumerate() {
let start = index * 2;
*output = u8::from_str_radix(&value[start..start + 2], 16)
.map_err(|_| FingerprintParseError)?;
}
Ok(Self(bytes))
}
}
impl Serialize for PlanFingerprint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for PlanFingerprint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FingerprintVisitor;
impl Visitor<'_> for FingerprintVisitor {
type Value = PlanFingerprint;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a 64-character lowercase SHA-256 fingerprint")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
value.parse().map_err(E::custom)
}
}
deserializer.deserialize_str(FingerprintVisitor)
}
}
pub fn canonical_plan_bytes(plan: &RefactorPlan) -> Result<Vec<u8>, PlanError> {
canonical_plan_bytes_with_limits(plan, RefactorPlanLimits::default())
}
pub fn canonical_plan_bytes_with_limits(
plan: &RefactorPlan,
limits: RefactorPlanLimits,
) -> Result<Vec<u8>, PlanError> {
crate::validation::validate_for_fingerprint(plan, limits)?;
let mut bytes = Vec::with_capacity(256);
write_canonical_top_level(&mut bytes, plan)?;
Ok(bytes)
}
pub fn fingerprint_plan(plan: &RefactorPlan) -> Result<PlanFingerprint, PlanError> {
fingerprint_plan_with_limits(plan, RefactorPlanLimits::default())
}
pub fn fingerprint_plan_with_limits(
plan: &RefactorPlan,
limits: RefactorPlanLimits,
) -> Result<PlanFingerprint, PlanError> {
crate::validation::validate_for_fingerprint(plan, limits)?;
fingerprint_validated(plan)
}
pub(crate) fn fingerprint_validated(plan: &RefactorPlan) -> Result<PlanFingerprint, PlanError> {
let mut digest = Sha256::new();
digest.update(DOMAIN_SEPARATOR);
let mut writer = io::BufWriter::with_capacity(8 * 1024, DigestWriter(&mut digest));
write_canonical_top_level(&mut writer, plan)?;
writer.flush().map_err(|error| io_error(&error))?;
drop(writer);
Ok(PlanFingerprint(digest.finalize().into()))
}
fn write_canonical_top_level<W: io::Write>(
writer: W,
plan: &RefactorPlan,
) -> Result<(), PlanError> {
let mut entries = top_level_entries(plan);
entries
.sort_unstable_by(|left, right| crate::canonical::compare_utf16_order(left.key, right.key));
write_sorted_entries(writer, &entries).map_err(|error| canonical_error(&error))
}
fn write_sorted_entries<W: io::Write>(
writer: W,
entries: &[Entry<'_>],
) -> Result<(), CanonicalError> {
let mut serializer = CanonicalSerializer::new(writer);
serializer.write_punctuation(b"{")?;
for (index, entry) in entries.iter().enumerate() {
if index != 0 {
serializer.write_punctuation(b",")?;
}
serializer.write_string(entry.key)?;
serializer.write_punctuation(b":")?;
serializer.write_value(&entry.value)?;
}
serializer.write_punctuation(b"}")
}
fn top_level_entries(plan: &RefactorPlan) -> Vec<Entry<'_>> {
let mut entries = vec![
Entry::new("schemaVersion", TopValue::String(&plan.schema_version)),
Entry::new("operation", TopValue::String(&plan.operation)),
Entry::new("operations", TopValue::Operations(&plan.operations)),
];
if let Some(value) = &plan.completeness {
entries.push(Entry::new("completeness", TopValue::Completeness(value)));
}
push_evidence_entries(&mut entries, plan);
for (key, value) in &plan.evidence.extensions {
entries.push(Entry::new(key, TopValue::Extension(value)));
}
entries
}
fn push_evidence_entries<'a>(entries: &mut Vec<Entry<'a>>, plan: &'a RefactorPlan) {
let evidence = &plan.evidence;
if !evidence.graph_revision.is_missing() {
entries.push(Entry::new(
"graphRevision",
TopValue::GraphRevision(&evidence.graph_revision),
));
}
if let Some(value) = &evidence.completeness_proof {
entries.push(Entry::new("completenessProof", TopValue::Proof(value)));
}
if let Some(value) = &evidence.uncertain_references {
entries.push(Entry::new(
"uncertainReferences",
TopValue::Uncertain(value),
));
}
if let Some(value) = &evidence.not_modified {
entries.push(Entry::new("notModified", TopValue::Omitted(value)));
}
if let Some(value) = &evidence.warnings {
entries.push(Entry::new("warnings", TopValue::Warnings(value)));
}
if let Some(value) = &evidence.follow_up {
entries.push(Entry::new("followUp", TopValue::String(value)));
}
if let Some(value) = &evidence.syntax_check {
entries.push(Entry::new("syntaxCheck", TopValue::Status(value)));
}
}
struct Entry<'a> {
key: &'a str,
value: TopValue<'a>,
}
impl<'a> Entry<'a> {
const fn new(key: &'a str, value: TopValue<'a>) -> Self {
Self { key, value }
}
}
enum TopValue<'a> {
String(&'a str),
Operations(&'a [RefactorOperation]),
Completeness(&'a Completeness),
GraphRevision(&'a GraphRevision),
Proof(&'a CompletenessProof),
Uncertain(&'a [UncertainReference]),
Omitted(&'a [NotModified]),
Warnings(&'a [WarningCode]),
Status(&'a StatusCode),
Extension(&'a Value),
}
impl Serialize for TopValue<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::String(value) => value.serialize(serializer),
Self::Operations(value) => value.serialize(serializer),
Self::Completeness(value) => value.serialize(serializer),
Self::GraphRevision(value) => value.serialize(serializer),
Self::Proof(value) => value.serialize(serializer),
Self::Uncertain(value) => value.serialize(serializer),
Self::Omitted(value) => value.serialize(serializer),
Self::Warnings(value) => value.serialize(serializer),
Self::Status(value) => value.serialize(serializer),
Self::Extension(value) => value.serialize(serializer),
}
}
}
struct DigestWriter<'a>(&'a mut Sha256);
impl io::Write for DigestWriter<'_> {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.0.update(buffer);
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn canonical_error(error: &CanonicalError) -> PlanError {
PlanError::new(
PlanErrorCode::JsonEncoding,
format!("could not encode canonical plan JSON: {error}"),
)
}
fn io_error(error: &io::Error) -> PlanError {
PlanError::new(
PlanErrorCode::JsonEncoding,
format!("could not write canonical plan JSON: {error}"),
)
}