aion-package 0.30.0

Archive validation, content hashing, and namespacing for Aion workflow packages.
Documentation
//! The canonical byte encoding of an emitted command, for package identity.
//!
//! A declared command reached through an action body is EXECUTABLE AUTHORITY:
//! its body lines decide what runs and what each program is told, and its
//! environment and working directory decide the world it runs in. An
//! authority that did not participate in the package hash could be rewritten
//! in storage without changing what the deployment claims to be, so every
//! field encodes — none is an optional tail.
//!
//! The encoding is structural rather than a serialization of the JSON form:
//! every value is length-prefixed and every optional carries a presence byte,
//! so no two distinct contracts can produce the same bytes and no ordering is
//! ever normalized away. Line order and slot order in particular ARE the
//! execution and argv orders, and a `--` standing before an element or after
//! it are different commands.

use super::contract::{
    ArgvSlot, CommandLineContract, CommandParameterContract, DeclaredCommandContract,
    EnvBindingContract,
};
use super::template::{FillPiece, FillTemplate};

/// Append `command`'s canonical identity record to `bytes`.
pub(crate) fn encode(bytes: &mut Vec<u8>, command: &DeclaredCommandContract) {
    text(bytes, &command.name);
    len(bytes, command.parameters.len());
    for parameter in &command.parameters {
        parameter_bytes(bytes, parameter);
    }
    len(bytes, command.lines.len());
    for line in &command.lines {
        line_bytes(bytes, line);
    }
    len(bytes, command.env.len());
    for binding in &command.env {
        env_bytes(bytes, binding);
    }
    optional_text(bytes, command.cwd.as_deref());
    // The emitter never sets the prior-form marker, so every emitted
    // contract contributes a constant absence byte here — but the field is
    // executable authority (it decides whether render refuses), so it
    // encodes like every other field rather than standing outside the hash.
    optional_text(bytes, command.prior_form_refusal.as_deref());
}

fn parameter_bytes(bytes: &mut Vec<u8>, parameter: &CommandParameterContract) {
    text(bytes, &parameter.name);
    optional_text(bytes, parameter.default.as_deref());
}

fn line_bytes(bytes: &mut Vec<u8>, line: &CommandLineContract) {
    len(bytes, line.slots.len());
    for slot in &line.slots {
        slot_bytes(bytes, slot);
    }
}

fn slot_bytes(bytes: &mut Vec<u8>, slot: &ArgvSlot) {
    fill(bytes, &slot.fill);
    text(bytes, &slot.label);
    bytes.push(u8::from(slot.admits_leading_dash));
}

fn env_bytes(bytes: &mut Vec<u8>, binding: &EnvBindingContract) {
    text(bytes, &binding.name);
    text(bytes, &binding.value);
}

fn fill(bytes: &mut Vec<u8>, template: &FillTemplate) {
    len(bytes, template.pieces.len());
    for piece in &template.pieces {
        match piece {
            FillPiece::Literal { text: value } => {
                bytes.push(0);
                text(bytes, value);
            }
            FillPiece::Hole { parameter } => {
                bytes.push(1);
                text(bytes, parameter);
            }
        }
    }
}

fn optional_text(bytes: &mut Vec<u8>, value: Option<&str>) {
    match value {
        Some(value) => {
            bytes.push(1);
            text(bytes, value);
        }
        None => bytes.push(0),
    }
}

fn text(bytes: &mut Vec<u8>, value: &str) {
    len(bytes, value.len());
    bytes.extend_from_slice(value.as_bytes());
}

fn len(bytes: &mut Vec<u8>, value: usize) {
    bytes.extend_from_slice(&(value as u64).to_be_bytes());
}