sofie-engine 0.2.0

Screens: the template parser and renderer, and the component catalog.
Documentation
use super::{FORMAT, MAGIC, tag};
use crate::catalog::Kind;
use crate::language::{Argument, Call};
use crate::message::{Message, Recorded};
use crate::syntax::{
    Attribute, Conditional, Expression, Loop, MAX_DEPTH, Op, Path, Position, Root, Written,
};

const REBUILD: &str = "rebuild the app so its build script compiles its screens again";

pub(crate) fn read(bytes: &[u8]) -> Result<Vec<Op<Written>>, String> {
    let rest = bytes
        .strip_prefix(MAGIC)
        .ok_or_else(|| format!("these are not a compiled template's bytes; {REBUILD}"))?;
    let (version, rest) = rest
        .split_first_chunk::<2>()
        .ok_or_else(|| damaged("the format version"))?;
    let version = u16::from_le_bytes(*version);
    if version != FORMAT {
        return Err(format!(
            "the screen was compiled in format {version}, and this sofie-engine reads format {FORMAT}; {REBUILD}"
        ));
    }
    let mut reader = Reader {
        bytes: rest,
        depth: 0,
        expression_depth: 0,
    };
    let ops = reader.ops()?;
    if !reader.bytes.is_empty() {
        return Err(damaged("bytes after the program"));
    }
    Ok(ops)
}

fn damaged(what: &str) -> String {
    format!("the compiled screen is damaged at {what}; {REBUILD}")
}

struct Reader<'b> {
    bytes: &'b [u8],
    depth: usize,
    expression_depth: usize,
}

impl Reader<'_> {
    fn ops(&mut self) -> Result<Vec<Op<Written>>, String> {
        self.depth += 1;
        if self.depth > MAX_DEPTH {
            return Err(damaged("a nesting deeper than any template's"));
        }
        let count = self.number()?;
        let ops = (0..count).map(|_| self.op()).collect();
        self.depth -= 1;
        ops
    }

    fn op(&mut self) -> Result<Op<Written>, String> {
        Ok(match self.byte()? {
            tag::MARKUP => Op::Markup(self.text()?),
            tag::TEXT => Op::Text(self.expression()?),
            tag::URL => Op::Url(self.expression()?),
            tag::IDENTIFIED => Op::Identified {
                id: self.expression()?,
                body: self.ops()?,
            },
            tag::COMPONENT => Op::Component(self.component()?),
            tag::IF => Op::If(Conditional {
                condition: self.expression()?,
                then_branch: self.ops()?,
                else_branch: self.ops()?,
            }),
            tag::EACH => Op::Each(Loop {
                list: self.expression()?,
                has_index: self.byte()? != 0,
                body: self.ops()?,
            }),
            _ => return Err(damaged("an op")),
        })
    }

    fn component(&mut self) -> Result<Written, String> {
        let name = self.text()?;
        let kind = Kind::named(&name).ok_or_else(|| {
            format!(
                "the screen uses component `{name}`, which this app's sofie-engine features leave out; \
                 its catalog has {}",
                Kind::list()
            )
        })?;
        let position = self.position()?;
        let count = self.number()?;
        let attributes = (0..count)
            .map(|_| {
                Ok(Attribute {
                    name: self.text()?,
                    value: self.expression()?,
                    position: self.position()?,
                })
            })
            .collect::<Result<_, String>>()?;
        Ok(Written {
            kind,
            attributes,
            children: self.ops()?,
            position,
        })
    }

    fn expression(&mut self) -> Result<Expression, String> {
        self.expression_depth += 1;
        if self.expression_depth > MAX_DEPTH {
            return Err(damaged("an expression nesting deeper than any template's"));
        }
        let expression = self.unnested_expression();
        self.expression_depth -= 1;
        expression
    }

    fn unnested_expression(&mut self) -> Result<Expression, String> {
        Ok(match self.byte()? {
            tag::PATH => {
                let root = match self.number()? {
                    0 => Root::Data,
                    slot => Root::Slot(slot - 1),
                };
                let count = self.number()?;
                let fields = (0..count).map(|_| self.text()).collect::<Result<_, _>>()?;
                Expression::Path(Path { root, fields })
            }
            tag::QUOTED => Expression::Text(self.text()?),
            tag::INTEGER => {
                let (number, rest) = self
                    .bytes
                    .split_first_chunk::<8>()
                    .ok_or_else(|| damaged("an integer"))?;
                self.bytes = rest;
                Expression::Integer(i64::from_le_bytes(*number))
            }
            tag::AUTHORED => Expression::Authored(self.text()?),
            tag::INTERPOLATED => {
                let count = self.number()?;
                let parts = (0..count)
                    .map(|_| self.expression())
                    .collect::<Result<_, _>>()?;
                Expression::Interpolated(parts)
            }
            tag::TRANSLATE => self.translation()?,
            call @ (tag::FORMAT_CALL | tag::LABEL) => self.call(call)?,
            tag::GATE => self.gate()?,
            _ => return Err(damaged("an expression")),
        })
    }

    fn translation(&mut self) -> Result<Expression, String> {
        let key = self.text()?;
        let count = self.number()?;
        let arguments = (0..count)
            .map(|_| {
                Ok(Argument {
                    name: self.text()?,
                    value: self.expression()?,
                })
            })
            .collect::<Result<_, String>>()?;
        let recorded = self.recorded()?;
        let call = Call::translate(key, arguments, self.position()?, recorded);
        Ok(Expression::Call(Box::new(call)))
    }

    fn recorded(&mut self) -> Result<Option<Recorded>, String> {
        let forms = self.byte()?;
        if forms == 0 {
            return Ok(None);
        }
        let other = self.message()?;
        let one = match forms {
            1 => None,
            2 => Some(self.message()?),
            _ => return Err(damaged("a recorded text")),
        };
        Ok(Some(Recorded { other, one }))
    }

    fn message(&mut self) -> Result<Message, String> {
        Message::parse(&self.text()?).map_err(|_| damaged("a recorded text"))
    }

    #[cfg(feature = "i18n")]
    fn call(&mut self, call: u8) -> Result<Expression, String> {
        use crate::translations::Format;
        let call = match call {
            tag::FORMAT_CALL => Call::Format {
                format: match self.byte()? {
                    0 => Format::Number,
                    1 => Format::Money,
                    tag::DATE_FORMAT => Format::Date,
                    _ => return Err(damaged("a format")),
                },
                value: self.expression()?,
                position: self.position()?,
            },
            _ => Call::Label {
                key: self.text()?,
                humanized: self.text()?,
                position: self.position()?,
            },
        };
        Ok(Expression::Call(Box::new(call)))
    }

    #[cfg(not(feature = "i18n"))]
    fn call(&mut self, call: u8) -> Result<Expression, String> {
        if call != tag::FORMAT_CALL || self.byte()? != tag::DATE_FORMAT {
            return Err(
                "the screen uses `number(…)`, `money(…)` or a label, which need sofie-engine's \
                 `i18n` feature"
                    .to_owned(),
            );
        }
        let value = self.expression()?;
        let call = Call::date(value, self.position()?);
        Ok(Expression::Call(Box::new(call)))
    }

    #[cfg(feature = "gates")]
    fn gate(&mut self) -> Result<Expression, String> {
        use crate::rendering::{Asks, Gate};
        let asks = match self.byte()? {
            0 => Asks::Permission,
            1 => Asks::Flag,
            2 => Asks::Entitlement,
            _ => return Err(damaged("a condition")),
        };
        Ok(Expression::Gate(Gate {
            asks,
            name: self.text()?,
        }))
    }

    #[cfg(not(feature = "gates"))]
    #[expect(
        clippy::unused_self,
        reason = "reads the condition under the `gates` feature"
    )]
    fn gate(&mut self) -> Result<Expression, String> {
        Err(
            "the screen uses `can(…)` or `enabled(…)`, which need sofie-engine's `gates` feature"
                .to_owned(),
        )
    }

    fn position(&mut self) -> Result<Position, String> {
        Ok(Position {
            line: self.number()?,
            column: self.number()?,
        })
    }

    fn text(&mut self) -> Result<String, String> {
        let length = self.number()?;
        if length > self.bytes.len() {
            return Err(damaged("a text"));
        }
        let (text, rest) = self.bytes.split_at(length);
        self.bytes = rest;
        String::from_utf8(text.to_vec()).map_err(|_| damaged("a text that is not UTF-8"))
    }

    fn byte(&mut self) -> Result<u8, String> {
        let (&byte, rest) = self.bytes.split_first().ok_or_else(|| damaged("its end"))?;
        self.bytes = rest;
        Ok(byte)
    }

    fn number(&mut self) -> Result<usize, String> {
        let mut number: usize = 0;
        for shift in (0..usize::BITS).step_by(7) {
            let byte = self.byte()?;
            number |= usize::from(byte & 0x7f) << shift;
            if byte & 0x80 == 0 {
                return Ok(number);
            }
        }
        Err(damaged("a number"))
    }
}

#[cfg(test)]
mod tests {
    use super::super::{FORMAT, MAGIC, tag};
    use super::read;

    #[test]
    fn deeply_nested_expressions_are_refused_not_followed() {
        let mut bytes = MAGIC.to_vec();
        bytes.extend(FORMAT.to_le_bytes());
        bytes.extend([1, tag::TEXT]);
        for _ in 0..200_000 {
            bytes.extend([tag::INTERPOLATED, 1]);
        }
        bytes.extend([tag::AUTHORED, 0]);

        let error = read(&bytes).expect_err("the nesting is refused");

        assert!(error.contains("nesting"), "{error}");
    }
}