use std::collections::BTreeMap;
use serde::Deserialize;
use super::contract::ArgvSlot;
use super::template::{FillPiece, FillTemplate};
pub(crate) type PriorCommandIdentities = BTreeMap<String, BTreeMap<String, Vec<u8>>>;
#[derive(Debug, thiserror::Error)]
pub(crate) enum PriorFormError {
#[error("contract entry is not valid JSON: {source}")]
Json {
source: serde_json::Error,
},
#[error(
"declared command `{command}` on queue `{task_queue}` carries both the prior archive \
form (`program`) and the current form (`lines`); no release mints this, so the \
contract bytes were edited"
)]
AmbiguousForm {
task_queue: String,
command: String,
},
#[error(
"the contract mixes prior-form and current-form declared commands (queue `{task_queue}`, \
action `{action}` differs from the rest); an archive is minted whole by one encoder, \
so the contract bytes were edited"
)]
MixedForms {
task_queue: String,
action: String,
},
#[error(
"declared command on queue `{task_queue}`, action `{action}` carries the prior archive \
form but does not decode as it: {source}"
)]
Undecodable {
task_queue: String,
action: String,
source: serde_json::Error,
},
}
#[derive(Deserialize)]
struct PriorParameter {
name: String,
list: bool,
#[serde(default)]
default: Option<FillTemplate>,
}
#[derive(Deserialize)]
struct PriorEnvBinding {
name: String,
value: FillTemplate,
}
#[derive(Deserialize)]
struct PriorDeclaredCommand {
name: String,
#[serde(default)]
parameters: Vec<PriorParameter>,
program: Vec<String>,
#[serde(default)]
args: Vec<ArgvSlot>,
#[serde(default)]
env: Vec<PriorEnvBinding>,
#[serde(default)]
cwd: Option<String>,
#[serde(default)]
hardened_path: Option<FillTemplate>,
#[serde(default)]
timeout_ms: Option<i64>,
#[serde(default)]
timeout_owner: Option<String>,
}
pub(crate) fn prior_command_identities(
contract_json: &[u8],
) -> Result<PriorCommandIdentities, PriorFormError> {
let value: serde_json::Value =
serde_json::from_slice(contract_json).map_err(|source| PriorFormError::Json { source })?;
let mut identities = PriorCommandIdentities::new();
let mut current_form_seen: Option<(String, String)> = None;
let workers = value
.get("workers")
.and_then(serde_json::Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for worker in workers {
let task_queue = worker
.get("task_queue")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_owned();
let actions = worker
.get("actions")
.and_then(serde_json::Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for action in actions {
let action_name = action
.get("name")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_owned();
let Some(body) = action.get("body") else {
continue;
};
if body.get("kind").and_then(serde_json::Value::as_str) != Some("command") {
continue;
}
let Some(command) = body.get("command") else {
continue;
};
let prior = command.get("program").is_some();
let current = command.get("lines").is_some();
if prior && current {
return Err(PriorFormError::AmbiguousForm {
task_queue,
command: command
.get("name")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_owned(),
});
}
if !prior {
if identities.is_empty() {
if current_form_seen.is_none() {
current_form_seen = Some((task_queue.clone(), action_name));
}
continue;
}
return Err(PriorFormError::MixedForms {
task_queue,
action: action_name,
});
}
if let Some((seen_queue, seen_action)) = current_form_seen.take() {
return Err(PriorFormError::MixedForms {
task_queue: seen_queue,
action: seen_action,
});
}
let decoded = PriorDeclaredCommand::deserialize(command).map_err(|source| {
PriorFormError::Undecodable {
task_queue: task_queue.clone(),
action: action_name.clone(),
source,
}
})?;
let mut record = Vec::new();
encode(&mut record, &decoded);
identities
.entry(task_queue.clone())
.or_default()
.insert(action_name, record);
}
}
Ok(identities)
}
fn encode(bytes: &mut Vec<u8>, command: &PriorDeclaredCommand) {
text(bytes, &command.name);
len(bytes, command.parameters.len());
for parameter in &command.parameters {
parameter_bytes(bytes, parameter);
}
len(bytes, command.program.len());
for word in &command.program {
text(bytes, word);
}
len(bytes, command.args.len());
for slot in &command.args {
slot_bytes(bytes, slot);
}
len(bytes, command.env.len());
for binding in &command.env {
env_bytes(bytes, binding);
}
optional_text(bytes, command.cwd.as_deref());
optional_fill(bytes, command.hardened_path.as_ref());
match command.timeout_ms {
Some(value) => {
bytes.push(1);
bytes.extend_from_slice(&value.to_be_bytes());
}
None => bytes.push(0),
}
optional_text(bytes, command.timeout_owner.as_deref());
}
fn parameter_bytes(bytes: &mut Vec<u8>, parameter: &PriorParameter) {
text(bytes, ¶meter.name);
bytes.push(u8::from(parameter.list));
optional_fill(bytes, parameter.default.as_ref());
}
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: &PriorEnvBinding) {
text(bytes, &binding.name);
fill(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_fill(bytes: &mut Vec<u8>, template: Option<&FillTemplate>) {
match template {
Some(template) => {
bytes.push(1);
fill(bytes, template);
}
None => bytes.push(0),
}
}
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());
}