Skip to main content

forma_ir/contracts/
state.rs

1//! Checked, bounded request snapshots. No request slot is retained by this module.
2use crate::format::{IrError, IslandEntry, MAX_ISLAND_STATE_BYTES};
3use crate::parser::IrModule;
4use crate::schema::{PropField, PropType};
5use crate::slot::{SlotData, SlotValue};
6
7const PREFIX: &str = "{\"v\":1,\"s\":[],\"i\":[";
8const SUFFIX: &str = "]}";
9static NULL: SlotValue = SlotValue::Null;
10
11/// A complete, checked island-state envelope for one rendered page.
12///
13/// The encoded bytes own each occurrence's declared inputs as they were at its
14/// ISLAND_START. They contain no references to mutable slots. Construction is
15/// private, and the complete escaped UTF-8 envelope is capped at one MiB.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct IslandState {
18    json: String,
19    count: usize,
20}
21impl IslandState {
22    /// Compact JSON safe as the text of an application/json script: every `<`
23    /// is escaped. The server places it once in the head and charges its bytes
24    /// to the final response budget.
25    pub fn as_json(&self) -> &str {
26        &self.json
27    }
28    /// Whether the walk executed no island occurrences.
29    pub fn is_empty(&self) -> bool {
30        self.count == 0
31    }
32    /// Number of executed island occurrences, including repeated list rows.
33    pub fn len(&self) -> usize {
34        self.count
35    }
36}
37
38pub(crate) struct StateBuilder {
39    encoded: String,
40    count: usize,
41}
42impl StateBuilder {
43    pub(crate) fn new() -> Self {
44        Self {
45            encoded: PREFIX.to_owned(),
46            count: 0,
47        }
48    }
49    /// Encode directly into capped owned storage, transactionally. The suffix
50    /// is reserved on every append; a failed entry is discarded in its entirety.
51    pub(crate) fn snapshot(
52        &mut self,
53        module: &IrModule,
54        entry: &IslandEntry,
55        slots: &SlotData,
56    ) -> Result<(usize, String), IrError> {
57        let before = self.encoded.len();
58        let occurrence = self.count;
59        let hash = entry
60            .digest
61            .iter()
62            .map(|byte| format!("{byte:02x}"))
63            .collect::<String>();
64        let result = (|| {
65            if entry.digest.len() != 8 {
66                return Err(invalid(entry.id, &[], "identity digest is not eight bytes"));
67            }
68            let fields = module
69                .schema
70                .objects
71                .get(entry.schema_object as usize)
72                .ok_or_else(|| invalid(entry.id, &[], "missing declared island object"))?;
73            if fields.len() != entry.slot_ids.len() {
74                return Err(invalid(
75                    entry.id,
76                    &[],
77                    "ordered input count differs from declared fields",
78                ));
79            }
80            if occurrence > 0 {
81                self.push(",")?;
82            }
83            self.push("{\"h\":\"")?;
84            self.push(&hash)?;
85            self.push("\",\"p\":")?;
86            self.project(
87                module,
88                entry.id,
89                Task::Object {
90                    fields,
91                    values: ObjectValues::Inputs(&entry.slot_ids, slots),
92                    index: 0,
93                    emitted: false,
94                },
95            )?;
96            self.push("}")?;
97            Ok(())
98        })();
99        if let Err(error) = result {
100            self.encoded.truncate(before);
101            return Err(error);
102        }
103        self.count += 1;
104        Ok((occurrence, hash))
105    }
106    pub(crate) fn finish(mut self) -> Result<IslandState, IrError> {
107        let charged = self
108            .encoded
109            .len()
110            .checked_add(SUFFIX.len())
111            .ok_or_else(limit)?;
112        if charged > MAX_ISLAND_STATE_BYTES {
113            return Err(limit());
114        }
115        self.encoded.push_str(SUFFIX);
116        // Finalization reads only owned bytes, never the module or slots.
117        if self.encoded.len() != charged {
118            return Err(limit());
119        }
120        Ok(IslandState {
121            json: self.encoded,
122            count: self.count,
123        })
124    }
125    fn push(&mut self, text: &str) -> Result<(), IrError> {
126        let next = self
127            .encoded
128            .len()
129            .checked_add(text.len())
130            .and_then(|n| n.checked_add(SUFFIX.len()))
131            .ok_or_else(limit)?;
132        if next > MAX_ISLAND_STATE_BYTES {
133            return Err(limit());
134        }
135        self.encoded.push_str(text);
136        Ok(())
137    }
138    fn string(&mut self, text: &str) -> Result<(), IrError> {
139        self.push("\"")?;
140        for ch in text.chars() {
141            match ch {
142                '"' => self.push("\\\"")?,
143                '\\' => self.push("\\\\")?,
144                '\n' => self.push("\\n")?,
145                '\r' => self.push("\\r")?,
146                '\t' => self.push("\\t")?,
147                '\u{08}' => self.push("\\b")?,
148                '\u{0c}' => self.push("\\f")?,
149                '<' => self.push("\\u003c")?,
150                '\0'..='\u{1f}' => {
151                    self.push(&format!("\\u{:04x}", ch as u8))?;
152                }
153                _ => {
154                    let mut bytes = [0; 4];
155                    self.push(ch.encode_utf8(&mut bytes))?;
156                }
157            }
158        }
159        self.push("\"")
160    }
161    fn project<'a>(
162        &mut self,
163        module: &'a IrModule,
164        island: u16,
165        first: Task<'a>,
166    ) -> Result<(), IrError> {
167        let mut tasks = vec![first];
168        let mut path = Vec::new();
169        // Array and object frames schedule one child at a time. The work stack
170        // is bounded by the already validated acyclic schema, not request size.
171        while let Some(task) = tasks.pop() {
172            match task {
173                Task::PathEnd => {
174                    path.pop();
175                }
176                Task::Value(kind, value) => match (kind, value) {
177                    (PropType::Optional(_), SlotValue::Null) => self.push("null")?,
178                    (PropType::Optional(inner), _) => tasks.push(Task::Value(inner, value)),
179                    (PropType::Boolean, SlotValue::Bool(value)) => {
180                        self.push(if *value { "true" } else { "false" })?
181                    }
182                    (PropType::String, SlotValue::Text(value)) => self.string(value)?,
183                    (PropType::Number, SlotValue::Number(number)) if number.is_finite() => {
184                        self.push(&value.to_text())?
185                    }
186                    (PropType::Date, SlotValue::Number(number))
187                        if number.is_finite()
188                            && number.fract() == 0.0
189                            && number.abs() <= 8_640_000_000_000_000.0 =>
190                    {
191                        self.push(&value.to_text())?
192                    }
193                    (PropType::Enum(members), SlotValue::Text(value))
194                        if members.iter().any(|id| {
195                            module.strings.get(*id).is_ok_and(|member| member == value)
196                        }) =>
197                    {
198                        self.string(value)?
199                    }
200                    (PropType::Array(inner), SlotValue::Array(values)) => tasks.push(Task::Array {
201                        kind: inner,
202                        values,
203                        index: 0,
204                    }),
205                    (PropType::Object(id), SlotValue::Object(values)) => {
206                        let fields = module
207                            .schema
208                            .objects
209                            .get(*id as usize)
210                            .ok_or_else(|| invalid(island, &path, "missing declared object"))?;
211                        tasks.push(Task::Object {
212                            fields,
213                            values: ObjectValues::Named(values),
214                            index: 0,
215                            emitted: false,
216                        });
217                    }
218                    _ => return Err(invalid(island, &path, "missing or invalid declared value")),
219                },
220                Task::Array {
221                    kind,
222                    values,
223                    index,
224                } => {
225                    if index == 0 {
226                        self.push("[")?;
227                    }
228                    if let Some(value) = values.get(index) {
229                        if index > 0 {
230                            self.push(",")?;
231                        }
232                        tasks.push(Task::Array {
233                            kind,
234                            values,
235                            index: index + 1,
236                        });
237                        path.push(Path::Index(index));
238                        tasks.push(Task::PathEnd);
239                        tasks.push(Task::Value(kind, value));
240                    } else {
241                        self.push("]")?;
242                    }
243                }
244                Task::Object {
245                    fields,
246                    values,
247                    index,
248                    emitted,
249                } => {
250                    if index == 0 {
251                        self.push("{")?;
252                    }
253                    if let Some(field) = fields.get(index) {
254                        let name = module.strings.get(field.name)?;
255                        let value = match values {
256                            ObjectValues::Inputs(ids, slots) => slots.get(ids[index]),
257                            ObjectValues::Named(entries) => {
258                                let mut matches = entries.iter().filter(|(key, _)| key == name);
259                                let value = matches.next().map_or(&NULL, |(_, value)| value);
260                                if matches.next().is_some() {
261                                    path.push(Path::Field(name));
262                                    return Err(invalid(
263                                        island,
264                                        &path,
265                                        "duplicate declared input field",
266                                    ));
267                                }
268                                value
269                            }
270                        };
271                        if matches!(field.kind, PropType::Optional(_))
272                            && matches!(value, SlotValue::Null)
273                        {
274                            tasks.push(Task::Object {
275                                fields,
276                                values,
277                                index: index + 1,
278                                emitted,
279                            });
280                            continue;
281                        }
282                        if emitted {
283                            self.push(",")?;
284                        }
285                        self.string(name)?;
286                        self.push(":")?;
287                        tasks.push(Task::Object {
288                            fields,
289                            values,
290                            index: index + 1,
291                            emitted: true,
292                        });
293                        path.push(Path::Field(name));
294                        tasks.push(Task::PathEnd);
295                        tasks.push(Task::Value(&field.kind, value));
296                    } else {
297                        self.push("}")?;
298                    }
299                }
300            }
301        }
302        Ok(())
303    }
304}
305#[derive(Clone, Copy)]
306enum ObjectValues<'a> {
307    Inputs(&'a [u16], &'a SlotData),
308    Named(&'a [(String, SlotValue)]),
309}
310enum Task<'a> {
311    Value(&'a PropType, &'a SlotValue),
312    Array {
313        kind: &'a PropType,
314        values: &'a [SlotValue],
315        index: usize,
316    },
317    Object {
318        fields: &'a [PropField],
319        values: ObjectValues<'a>,
320        index: usize,
321        emitted: bool,
322    },
323    PathEnd,
324}
325enum Path<'a> {
326    Field(&'a str),
327    Index(usize),
328}
329fn invalid(island: u16, path: &[Path<'_>], reason: &'static str) -> IrError {
330    use std::fmt::Write;
331    let mut name = String::from("$");
332    for segment in path {
333        match segment {
334            Path::Field(field) => {
335                let _ = write!(name, "[{field:?}]");
336            }
337            Path::Index(index) => {
338                let _ = write!(name, "[{index}]");
339            }
340        }
341    }
342    IrError::InvalidIslandState(Box::new(crate::format::IslandStateViolation {
343        island,
344        path: name,
345        reason,
346    }))
347}
348fn limit() -> IrError {
349    IrError::IslandStateLimitExceeded {
350        limit: MAX_ISLAND_STATE_BYTES,
351    }
352}