mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 23:32:14 +00:00
561 lines
24 KiB
Rust
561 lines
24 KiB
Rust
//! Action sets another app saves as text (`.aia` files that start `/version`), read into their
|
|
//! actions and steps without changing anything (`actionSet.read`).
|
|
//!
|
|
//! The text is a set of fields and blocks. A slash name followed by a value sets a field, and a
|
|
//! slash name followed by `{` opens a block that `}` closes. A value is a decimal number, text in
|
|
//! parentheses, a byte count and hexadecimal bytes in square brackets (`[ 5 48656c6c6f ]`, UTF-8
|
|
//! text, which can run over several words), or a byte count and hexadecimal bytes in angle brackets
|
|
//! (`< 3 0a0b0c >`, raw bytes). The set's `/version` comes first, and its `/name` is hexadecimal
|
|
//! text. Each action is an `/action-N` block with a hexadecimal `/name`; each step is an `/event-N`
|
|
//! block inside one, with its `/internalName` in parentheses, the `/localizedName` it is shown with
|
|
//! as hexadecimal text, and `/isOn` 0 when it is left out of playback; each value a step was
|
|
//! recorded with is a `/parameter-N` block inside it, which is counted, and whose hexadecimal
|
|
//! `/value` is checked against its byte count. The set's `/actionCount`, an action's `/eventCount`
|
|
//! and a step's `/parameterCount` give how many actions, steps and recorded values follow.
|
|
//! The layout follows the README of Inventsable's Redesigning-Illustrator-Actions-in-Scripting
|
|
//! (https://github.com/Inventsable/Redesigning-Illustrator-Actions-in-Scripting), the forum post
|
|
//! "Made a two-way aia to json converter"
|
|
//! (https://community.adobe.com/t5/illustrator-discussions/made-a-two-way-aia-to-json-converter/m-p/13751909),
|
|
//! sttk3's article on names as UTF-8 hexadecimal
|
|
//! (https://sttk3.com/blog/tips/illustrator/dynamic-generate-action.html), and the MIT-licensed
|
|
//! reader in zachshallbetter/adobe-mcp (https://github.com/zachshallbetter/adobe-mcp). The raw form
|
|
//! follows the forum thread "Creating a dynamic action"
|
|
//! (https://community.adobe.com/questions-652/creating-a-dynamic-action-to-use-with-app-doscript-method-814406).
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::{CommandSpec, EngineError, Result, Session, always, bad, cmd, text_param};
|
|
|
|
/// The most bytes of a file read.
|
|
const MAX_BYTES: usize = 8 << 20;
|
|
/// The most bytes taken in: [`MAX_BYTES`] after a byte order mark, and one more that shows the text
|
|
/// goes on.
|
|
const READ_LIMIT: usize = MAX_BYTES + 4;
|
|
/// The most blocks open at once.
|
|
const MAX_DEPTH: usize = 16;
|
|
/// The most actions read.
|
|
const MAX_ACTIONS: usize = 1000;
|
|
/// The most steps read in one action.
|
|
const MAX_STEPS: usize = 10_000;
|
|
/// The most steps read in the set: a small text can hold hundreds of thousands of empty steps, and
|
|
/// the answer holds an object for each.
|
|
const MAX_SET_STEPS: usize = 100_000;
|
|
/// The longest name kept (bytes).
|
|
const MAX_NAME: usize = 1024;
|
|
/// The most tokens listed as unread.
|
|
const MAX_UNREAD: usize = 1000;
|
|
/// The longest unread token shown (characters).
|
|
const UNREAD_SHOWN: usize = 80;
|
|
|
|
/// The extensions of action sets other apps save.
|
|
pub const EXTS: &[&str] = &["aia"];
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![cmd!(
|
|
query "actionSet.read",
|
|
"Read Action Set File",
|
|
[],
|
|
None,
|
|
"{path? | data?: the file's text | dataBase64?, name?: the file's name (default: the path's)} read an action set another app saved as text (.aia, starting \"/version\") without changing anything → {set: the set's /name (else the file's name without its extension, else empty), version, actions: [{name, steps: [{name: the step's /internalName, label: its /localizedName, or null when it has none or it is blank, on: false when /isOn is 0, params: how many /parameter blocks it has, line}]}], unread: [{line, text}] (tokens that aren't read, at most 1000), truncated: true when a cap stopped the reading (8 MiB, 16 open blocks, 1,000 actions, 10,000 steps in an action, 100,000 steps in the set; names past 1,024 bytes are cut), when the text ends inside a block or a value or after a field's name, when /actionCount, /eventCount or a step's /parameterCount differs from the actions, steps or recorded values read (a negative count, one too large for a number and one that isn't a number always differ), or when a hexadecimal name, label or recorded /value isn't as many bytes as its count says}. Give exactly one of path, data and dataBase64. Text that doesn't start with /version and a number is an error",
|
|
always,
|
|
read
|
|
)]
|
|
}
|
|
|
|
/// A step of an action: an `/event-N` block.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct AiaStep {
|
|
/// Its `/internalName`.
|
|
pub name: String,
|
|
/// Its `/localizedName`, if it has one.
|
|
pub label: Option<String>,
|
|
/// Its `/isOn`, if it has one.
|
|
pub on: Option<i64>,
|
|
/// How many `/parameter-N` blocks it has.
|
|
pub params: usize,
|
|
/// Its `/parameterCount`, if it has one.
|
|
pub declared: Option<i64>,
|
|
/// The line its block opens on (from 1).
|
|
pub line: usize,
|
|
}
|
|
|
|
/// An action: an `/action-N` block.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct AiaAction {
|
|
pub name: String,
|
|
pub steps: Vec<AiaStep>,
|
|
/// Its `/eventCount`, if it has one.
|
|
pub declared: Option<i64>,
|
|
}
|
|
|
|
/// An action set read from text.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct AiaSet {
|
|
pub version: i64,
|
|
/// The set's `/name`, if it has one.
|
|
pub name: Option<String>,
|
|
pub actions: Vec<AiaAction>,
|
|
/// Its `/actionCount`, if it has one.
|
|
pub declared: Option<i64>,
|
|
/// The tokens that weren't read, with their lines.
|
|
pub unread: Vec<(usize, String)>,
|
|
/// A cap stopped the reading or cut a name, the text ends inside a block or a value or after a
|
|
/// field's name, its counts differ from the actions, steps and recorded values read (a negative
|
|
/// count, one too large for an `i64` and one that isn't a number always differ), or a hexadecimal
|
|
/// name, label or recorded `/value` isn't as many bytes as its count says.
|
|
pub truncated: bool,
|
|
/// How many steps were read in all the actions.
|
|
steps: usize,
|
|
}
|
|
|
|
/// A token of the text.
|
|
enum Token {
|
|
/// A slash name.
|
|
Name(Vec<u8>),
|
|
/// An integer; `None` when it doesn't fit an `i64`.
|
|
Int(Option<i64>),
|
|
/// Text in parentheses, a backslash taking the next byte as it is.
|
|
Text(Vec<u8>),
|
|
/// The bytes of `[ count hex… ]`; `None` when the hexadecimal digits don't make whole bytes or
|
|
/// as many bytes as the count says.
|
|
Hex(Option<Vec<u8>>),
|
|
/// `< count hex… >`.
|
|
Raw,
|
|
Open,
|
|
Close,
|
|
/// Anything else, as written.
|
|
Other(Vec<u8>),
|
|
}
|
|
|
|
/// The tokens of `text`, each with its line.
|
|
struct Tokens<'a> {
|
|
text: &'a [u8],
|
|
at: usize,
|
|
line: usize,
|
|
/// The text ended inside a value in parentheses, square brackets or angle brackets.
|
|
unclosed: bool,
|
|
}
|
|
|
|
impl<'a> Tokens<'a> {
|
|
/// Past one byte, counting a line at LF, at CR alone, and once for CRLF.
|
|
fn step(&mut self) -> Option<u8> {
|
|
let b = *self.text.get(self.at)?;
|
|
if b == b'\n' || b == b'\r' && self.text.get(self.at + 1) != Some(&b'\n') {
|
|
self.line += 1;
|
|
}
|
|
self.at += 1;
|
|
Some(b)
|
|
}
|
|
|
|
/// The bytes up to `end` (not taken in); `None` when the text ends first.
|
|
fn until(&mut self, end: u8) -> Option<&'a [u8]> {
|
|
let (text, start) = (self.text, self.at);
|
|
while text.get(self.at).is_some_and(|&b| b != end) {
|
|
self.step();
|
|
}
|
|
let inside = text.get(start..self.at)?;
|
|
text.get(self.at)?;
|
|
self.at += 1;
|
|
Some(inside)
|
|
}
|
|
|
|
fn next_token(&mut self) -> Option<(usize, Token)> {
|
|
while self.text.get(self.at).is_some_and(|&b| b.is_ascii_whitespace() || b == 0) {
|
|
self.step();
|
|
}
|
|
let line = self.line;
|
|
let b = *self.text.get(self.at)?;
|
|
let ends = |b: u8| b.is_ascii_whitespace() || b == 0 || b"{}[]()<>/".contains(&b);
|
|
let token = match b {
|
|
b'{' | b'}' => {
|
|
self.at += 1;
|
|
if b == b'{' { Token::Open } else { Token::Close }
|
|
}
|
|
b'(' => {
|
|
self.at += 1;
|
|
let (mut text, mut depth) = (vec![], 0usize);
|
|
self.unclosed = true;
|
|
while let Some(c) = self.step() {
|
|
match c {
|
|
b'\\' => text.extend(self.step()),
|
|
b'(' => {
|
|
depth += 1;
|
|
text.push(c);
|
|
}
|
|
b')' if depth == 0 => {
|
|
self.unclosed = false;
|
|
break;
|
|
}
|
|
b')' => {
|
|
depth -= 1;
|
|
text.push(c);
|
|
}
|
|
_ => text.push(c),
|
|
}
|
|
}
|
|
Token::Text(text)
|
|
}
|
|
b'[' | b'<' => {
|
|
self.at += 1;
|
|
let close = if b == b'[' { b']' } else { b'>' };
|
|
let inside = self.until(close);
|
|
self.unclosed = inside.is_none();
|
|
let inside = inside.unwrap_or_default().to_vec();
|
|
if b == b'<' {
|
|
Token::Raw
|
|
} else {
|
|
// The first word is the byte count; the bytes follow in one or more words.
|
|
let mut words = inside.split(|c| c.is_ascii_whitespace()).filter(|w| !w.is_empty());
|
|
let count = words.next().and_then(|w| std::str::from_utf8(w).ok()).and_then(|w| w.parse::<usize>().ok());
|
|
let digits: Vec<u8> = words.flatten().copied().collect();
|
|
Token::Hex(hex_bytes(&digits).filter(|b| Some(b.len()) == count))
|
|
}
|
|
}
|
|
b'/' => {
|
|
self.at += 1;
|
|
let start = self.at;
|
|
while self.text.get(self.at).is_some_and(|&c| !ends(c)) {
|
|
self.at += 1;
|
|
}
|
|
Token::Name(self.text.get(start..self.at).unwrap_or_default().to_vec())
|
|
}
|
|
_ => {
|
|
let start = self.at;
|
|
while self.text.get(self.at).is_some_and(|&c| !ends(c)) {
|
|
self.at += 1;
|
|
}
|
|
// Always past at least one byte.
|
|
self.at = self.at.max(start + 1);
|
|
let word = self.text.get(start..self.at).unwrap_or_default();
|
|
let number = word.strip_prefix(b"-").or_else(|| word.strip_prefix(b"+")).unwrap_or(word);
|
|
if !number.is_empty() && number.iter().all(u8::is_ascii_digit) {
|
|
Token::Int(std::str::from_utf8(word).ok().and_then(|s| s.parse().ok()))
|
|
} else {
|
|
Token::Other(word.to_vec())
|
|
}
|
|
}
|
|
};
|
|
Some((line, token))
|
|
}
|
|
}
|
|
|
|
/// The bytes hexadecimal `digits` stand for, two digits a byte.
|
|
fn hex_bytes(digits: &[u8]) -> Option<Vec<u8>> {
|
|
let nibble = |c: u8| (c as char).to_digit(16).and_then(|d| u8::try_from(d).ok());
|
|
if !digits.len().is_multiple_of(2) {
|
|
return None;
|
|
}
|
|
digits.chunks(2).map(|pair| Some((nibble(*pair.first()?)? << 4) | nibble(*pair.get(1)?)?)).collect()
|
|
}
|
|
|
|
/// What a block is.
|
|
enum Kind {
|
|
Action,
|
|
Step,
|
|
Param,
|
|
/// A block that isn't read, or one past a cap.
|
|
Other,
|
|
}
|
|
|
|
/// `name` ends with `-` and a number after `prefix`, as `/action-1` does.
|
|
fn numbered(name: &[u8], prefix: &[u8]) -> bool {
|
|
name.strip_prefix(prefix).is_some_and(|n| !n.is_empty() && n.iter().all(u8::is_ascii_digit))
|
|
}
|
|
|
|
/// A name as text: at most [`MAX_NAME`] bytes, read as UTF-8 with replacement characters.
|
|
fn name_text(name: &[u8], set: &mut AiaSet) -> String {
|
|
if name.len() > MAX_NAME {
|
|
set.truncated = true;
|
|
}
|
|
let mut s = String::from_utf8_lossy(name.get(..MAX_NAME).unwrap_or(name)).into_owned();
|
|
// A character cut at the limit reads as one replacement character.
|
|
if name.len() > MAX_NAME && s.ends_with('\u{FFFD}') {
|
|
s.pop();
|
|
}
|
|
s
|
|
}
|
|
|
|
/// How the unread list shows a value in square brackets that isn't as many bytes as its count says.
|
|
const BAD_HEX: &str = "[ … ] (not the hexadecimal bytes its count says)";
|
|
|
|
/// List a token that isn't read.
|
|
fn unread(set: &mut AiaSet, line: usize, text: impl AsRef<[u8]>) {
|
|
if set.unread.len() < MAX_UNREAD {
|
|
let text: String = String::from_utf8_lossy(text.as_ref()).chars().take(UNREAD_SHOWN).collect();
|
|
set.unread.push((line, text));
|
|
}
|
|
}
|
|
|
|
/// What the block `name` that opens on `line` inside the blocks `stack` is: a new action, step or
|
|
/// recorded value of `set`, or a block that isn't read (listed when it is out of place).
|
|
fn open_block(set: &mut AiaSet, stack: &[Kind], name: &[u8], line: usize) -> Kind {
|
|
match stack.last() {
|
|
_ if stack.len() >= MAX_DEPTH => {
|
|
set.truncated = true;
|
|
Kind::Other
|
|
}
|
|
None if numbered(name, b"action-") => {
|
|
if set.actions.len() < MAX_ACTIONS {
|
|
set.actions.push(AiaAction::default());
|
|
Kind::Action
|
|
} else {
|
|
set.truncated = true;
|
|
Kind::Other
|
|
}
|
|
}
|
|
Some(Kind::Action) if numbered(name, b"event-") => match set.actions.last_mut() {
|
|
Some(a) if a.steps.len() < MAX_STEPS && set.steps < MAX_SET_STEPS => {
|
|
a.steps.push(AiaStep { line, ..AiaStep::default() });
|
|
set.steps += 1;
|
|
Kind::Step
|
|
}
|
|
_ => {
|
|
set.truncated = true;
|
|
Kind::Other
|
|
}
|
|
},
|
|
Some(Kind::Step) if numbered(name, b"parameter-") => {
|
|
if let Some(s) = set.actions.last_mut().and_then(|a| a.steps.last_mut()) {
|
|
s.params += 1;
|
|
}
|
|
Kind::Param
|
|
}
|
|
Some(Kind::Param | Kind::Other) => Kind::Other,
|
|
_ => {
|
|
unread(set, line, format!("/{} {{", String::from_utf8_lossy(name)));
|
|
Kind::Other
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set the field `name` of the block `kind` (the set when `None`) to the hexadecimal text `bytes`
|
|
/// on `line`: the set's or an action's `/name`, or a step's `/localizedName`.
|
|
fn set_hex(set: &mut AiaSet, kind: Option<&Kind>, name: Option<&[u8]>, bytes: Option<Vec<u8>>, line: usize) {
|
|
let Some(bytes) = bytes else {
|
|
set.truncated = true;
|
|
unread(set, line, BAD_HEX);
|
|
return;
|
|
};
|
|
match (kind, name) {
|
|
(None, Some(b"name")) => set.name = Some(name_text(&bytes, set)),
|
|
(Some(Kind::Action), Some(b"name")) => {
|
|
let text = name_text(&bytes, set);
|
|
if let Some(a) = set.actions.last_mut() {
|
|
a.name = text;
|
|
}
|
|
}
|
|
(Some(Kind::Step), Some(b"localizedName")) => {
|
|
let text = name_text(&bytes, set);
|
|
if let Some(s) = set.actions.last_mut().and_then(|a| a.steps.last_mut()) {
|
|
s.label = Some(text);
|
|
}
|
|
}
|
|
(_, Some(_)) => {}
|
|
(_, None) => unread(set, line, "[ … ]"),
|
|
}
|
|
}
|
|
|
|
/// Mark `set` truncated when the hexadecimal `/value` on `line` isn't as many bytes as its count
|
|
/// says.
|
|
fn check_value(set: &mut AiaSet, bytes: Option<Vec<u8>>, line: usize) {
|
|
if bytes.is_none() {
|
|
set.truncated = true;
|
|
unread(set, line, BAD_HEX);
|
|
}
|
|
}
|
|
|
|
/// Mark `set` truncated when the field `name` of the block `kind` (the set when `None`) is a count
|
|
/// (the set's `/actionCount`, an action's `/eventCount` or a step's `/parameterCount`) whose value is
|
|
/// not a number that fits an `i64`.
|
|
fn bad_count(set: &mut AiaSet, kind: Option<&Kind>, name: Option<&[u8]>) {
|
|
if matches!((kind, name), (None, Some(b"actionCount")) | (Some(Kind::Action), Some(b"eventCount")) | (Some(Kind::Step), Some(b"parameterCount")))
|
|
{
|
|
set.truncated = true;
|
|
}
|
|
}
|
|
|
|
/// Set the field `name` of the block `kind` (the set when `None`) to the integer `value` on `line`:
|
|
/// the set's `/actionCount`, an action's `/eventCount`, a step's `/parameterCount`, or a step's
|
|
/// `/isOn`. A count too large for an `i64` marks the set truncated.
|
|
fn set_int(set: &mut AiaSet, kind: Option<&Kind>, name: Option<&[u8]>, value: Option<i64>, line: usize) {
|
|
if value.is_none() {
|
|
bad_count(set, kind, name);
|
|
}
|
|
match (kind, name) {
|
|
(None, Some(b"actionCount")) => set.declared = value,
|
|
(Some(Kind::Action), Some(b"eventCount")) => {
|
|
if let Some(a) = set.actions.last_mut() {
|
|
a.declared = value;
|
|
}
|
|
}
|
|
(Some(Kind::Step), Some(b"parameterCount")) => {
|
|
if let Some(s) = set.actions.last_mut().and_then(|a| a.steps.last_mut()) {
|
|
s.declared = value;
|
|
}
|
|
}
|
|
(Some(Kind::Step), Some(b"isOn")) => {
|
|
if let Some(s) = set.actions.last_mut().and_then(|a| a.steps.last_mut()) {
|
|
s.on = value;
|
|
}
|
|
}
|
|
(_, Some(_)) => {}
|
|
(_, None) => unread(set, line, value.map_or_else(|| "(a number too large)".to_string(), |v| v.to_string())),
|
|
}
|
|
}
|
|
|
|
/// Read an action set saved as text. An error when the text isn't one: it doesn't start with
|
|
/// `/version` and an integer.
|
|
pub fn read_aia(bytes: &[u8]) -> std::result::Result<AiaSet, String> {
|
|
let mut set = AiaSet::default();
|
|
let text = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
|
|
let text = match text.get(..MAX_BYTES) {
|
|
Some(head) if text.len() > MAX_BYTES => {
|
|
set.truncated = true;
|
|
head
|
|
}
|
|
_ => text,
|
|
};
|
|
let mut tokens = Tokens { text, at: 0, line: 1, unclosed: false };
|
|
match (tokens.next_token(), tokens.next_token()) {
|
|
(Some((_, Token::Name(n))), Some((_, Token::Int(Some(v))))) if n == b"version" => set.version = v,
|
|
_ => return Err("not an action set: the text doesn't start with /version and a number".into()),
|
|
}
|
|
let mut stack: Vec<Kind> = vec![];
|
|
// A slash name waiting for its value or its `{`: (line, name).
|
|
let mut pending: Option<(usize, Vec<u8>)> = None;
|
|
while let Some((line, token)) = tokens.next_token() {
|
|
let in_step = matches!(stack.last(), Some(Kind::Step));
|
|
let field = |pending: &mut Option<(usize, Vec<u8>)>| pending.take().map(|(_, n)| n);
|
|
match token {
|
|
Token::Name(name) => {
|
|
if let Some((l, n)) = pending.replace((line, name)) {
|
|
bad_count(&mut set, stack.last(), Some(&n));
|
|
unread(&mut set, l, [b"/".as_slice(), &n].concat());
|
|
}
|
|
}
|
|
Token::Open => {
|
|
let (l, name) = pending.take().unwrap_or((line, vec![]));
|
|
bad_count(&mut set, stack.last(), Some(&name));
|
|
let kind = open_block(&mut set, &stack, &name, l);
|
|
stack.push(kind);
|
|
}
|
|
Token::Close => {
|
|
if let Some((l, n)) = pending.take() {
|
|
bad_count(&mut set, stack.last(), Some(&n));
|
|
unread(&mut set, l, [b"/".as_slice(), &n].concat());
|
|
}
|
|
if stack.pop().is_none() {
|
|
unread(&mut set, line, "}");
|
|
}
|
|
}
|
|
Token::Hex(bytes) if matches!(stack.last(), Some(Kind::Param)) => {
|
|
if field(&mut pending).as_deref() == Some(b"value".as_slice()) {
|
|
check_value(&mut set, bytes, line);
|
|
}
|
|
}
|
|
// The other values inside a recorded value, or a block that isn't read.
|
|
_ if matches!(stack.last(), Some(Kind::Param | Kind::Other)) => {
|
|
pending = None;
|
|
}
|
|
Token::Hex(bytes) => {
|
|
let name = field(&mut pending);
|
|
bad_count(&mut set, stack.last(), name.as_deref());
|
|
set_hex(&mut set, stack.last(), name.as_deref(), bytes, line);
|
|
}
|
|
Token::Text(t) => {
|
|
let name = field(&mut pending);
|
|
bad_count(&mut set, stack.last(), name.as_deref());
|
|
match name {
|
|
Some(n) if in_step && n == b"internalName" => {
|
|
let name = name_text(&t, &mut set);
|
|
if let Some(s) = set.actions.last_mut().and_then(|a| a.steps.last_mut()) {
|
|
s.name = name;
|
|
}
|
|
}
|
|
Some(_) => {}
|
|
None => unread(&mut set, line, [b"(".as_slice(), &t, b")"].concat()),
|
|
}
|
|
}
|
|
Token::Int(v) => {
|
|
let name = field(&mut pending);
|
|
set_int(&mut set, stack.last(), name.as_deref(), v, line);
|
|
}
|
|
Token::Raw => {
|
|
let name = field(&mut pending);
|
|
bad_count(&mut set, stack.last(), name.as_deref());
|
|
if name.is_none() {
|
|
unread(&mut set, line, "< … >");
|
|
}
|
|
}
|
|
Token::Other(word) => {
|
|
// A real number, or another word as a field's value.
|
|
let name = field(&mut pending);
|
|
bad_count(&mut set, stack.last(), name.as_deref());
|
|
if name.is_none() {
|
|
unread(&mut set, line, word);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// The text ends after a field's name, before its value.
|
|
if let Some((l, n)) = pending {
|
|
set.truncated = true;
|
|
unread(&mut set, l, [b"/".as_slice(), &n].concat());
|
|
}
|
|
// An unclosed value is the last token: it takes the rest of the text.
|
|
if tokens.unclosed {
|
|
set.truncated = true;
|
|
unread(&mut set, tokens.line, "a value not closed at the end");
|
|
}
|
|
if !stack.is_empty() {
|
|
set.truncated = true;
|
|
unread(&mut set, tokens.line, format!("blocks left open at the end: {}", stack.len()));
|
|
}
|
|
// Not as many actions, steps or recorded values as the counts give; a negative count never matches.
|
|
let differs = |declared: Option<i64>, read: usize| declared.is_some_and(|d| !usize::try_from(d).is_ok_and(|d| d == read));
|
|
let step_differs = |a: &AiaAction| a.steps.iter().any(|s| differs(s.declared, s.params));
|
|
if differs(set.declared, set.actions.len()) || set.actions.iter().any(|a| differs(a.declared, a.steps.len()) || step_differs(a)) {
|
|
set.truncated = true;
|
|
unread(&mut set, tokens.line, "not as many actions, steps or recorded values as the counts give");
|
|
}
|
|
Ok(set)
|
|
}
|
|
|
|
fn read(_: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "actionSet.read";
|
|
let (path, data, b64) = (text_param(p, "path", C)?, text_param(p, "data", C)?, text_param(p, "dataBase64", C)?);
|
|
let name = text_param(p, "name", C)?;
|
|
let bytes: std::borrow::Cow<[u8]> = match (path, data, b64) {
|
|
(Some(path), None, None) => super::fileio::read_file_head(path, READ_LIMIT)?.into(),
|
|
(None, Some(text), None) => text.as_bytes().get(..READ_LIMIT).unwrap_or(text.as_bytes()).into(),
|
|
(None, None, Some(b64)) => super::scripts::decode_head(b64, READ_LIMIT).ok_or_else(|| bad(C, "bad dataBase64"))?.into(),
|
|
_ => return Err(bad(C, "give exactly one of `path`, `data` and `dataBase64`")),
|
|
};
|
|
let set = read_aia(&bytes).map_err(EngineError::Other)?;
|
|
let file = name.or(path).unwrap_or_default();
|
|
let stem = std::path::Path::new(file).file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
|
|
let name = set.name.clone().filter(|n| !n.trim().is_empty()).unwrap_or(stem);
|
|
let actions: Vec<Value> = set
|
|
.actions
|
|
.iter()
|
|
.map(|a| {
|
|
let steps: Vec<Value> = a
|
|
.steps
|
|
.iter()
|
|
.map(|s| {
|
|
let label = s.label.clone().filter(|l| !l.trim().is_empty());
|
|
json!({ "name": s.name, "label": label, "on": s.on != Some(0), "params": s.params, "line": s.line })
|
|
})
|
|
.collect();
|
|
json!({ "name": a.name, "steps": steps })
|
|
})
|
|
.collect();
|
|
Ok(json!({
|
|
"set": name,
|
|
"version": set.version,
|
|
"actions": actions,
|
|
"unread": set.unread.iter().map(|(line, text)| json!({ "line": line, "text": text })).collect::<Vec<_>>(),
|
|
"truncated": set.truncated,
|
|
}))
|
|
}
|