ifc_lite_wasm/api/
appearance.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4//! Appearance edits use the same Rust planner in native and browser hosts.
5use super::IfcAPI;
6use ifc_lite_processing::appearance::{catalog_appearance, plan_appearance, AppearanceCatalogRequest, AppearanceRequest};
7use wasm_bindgen::prelude::*;
8
9pub(super) fn decode_request<T: serde::de::DeserializeOwned>(request_json: &str) -> Result<T, String> {
10    if request_json.len() > 256 * 1024 {
11        return Err("Appearance request exceeds the scope budget".into());
12    }
13    serde_json::from_str(request_json).map_err(|error| format!("Invalid appearance request: {error}"))
14}
15fn plan_json(content: &[u8], request_json: &str) -> Result<Vec<u8>, String> {
16    let request: AppearanceRequest = decode_request(request_json)?;
17    encode_bounded(&plan_appearance(content, &request)?)
18}
19fn catalog_json(content: &[u8], request_json: &str) -> Result<Vec<u8>, String> {
20    let request: AppearanceCatalogRequest = decode_request(request_json)?;
21    encode_bounded(&catalog_appearance(content, &request)?)
22}
23
24// A writer ceiling stops serialization before allocating an oversized JSON buffer.
25// The host then holds UTF-8 bytes, a JS string and its parsed/structured-cloned graph.
26pub(super) fn encode_bounded(value: &impl serde::Serialize) -> Result<Vec<u8>, String> {
27    struct Output(Vec<u8>);
28    impl std::io::Write for Output {
29        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
30            if bytes.len() > (64 * 1024 * 1024usize).saturating_sub(self.0.len()) {
31                return Err(std::io::Error::other("Appearance JSON exceeds 64 MiB. Choose fewer objects or simplify the source mesh."));
32            }
33            self.0.extend_from_slice(bytes);
34            Ok(bytes.len())
35        }
36        fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
37    }
38    let mut output = Output(Vec::new());
39    serde_json::to_writer(&mut output, value).map_err(|error| format!("Cannot encode appearance output: {error}"))?;
40    Ok(output.0)
41}
42
43#[wasm_bindgen]
44impl IfcAPI {
45    /// Plan image/UV edits against an effective IFC snapshot without mutating it.
46    /// JSON input uses AppearanceRequest; output is UTF-8 AppearancePlan JSON.
47    /// Call from a worker, then validate sourceRevision and allocator before an
48    /// atomic host commit. Preview UVs describe an unsplit canonical mesh item.
49    #[wasm_bindgen(js_name = planAppearance)]
50    pub fn plan_appearance(&self, content: &[u8], request_json: &str) -> Result<Vec<u8>, JsError> {
51        plan_json(content, request_json).map_err(|message| JsError::new(&message))
52    }
53
54    /// Catalog effective IfcProduct classes and IfcRelDefinesByType membership.
55    /// JSON input is AppearanceCatalogRequest; output is AppearanceCatalog JSON.
56    /// Run in a worker and validate sourceRevision before using the selectors.
57    #[wasm_bindgen(js_name = catalogAppearance)]
58    pub fn catalog_appearance(&self, content: &[u8], request_json: &str) -> Result<Vec<u8>, JsError> {
59        catalog_json(content, request_json).map_err(|message| JsError::new(&message))
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::{plan_json, catalog_json, encode_bounded};
66
67    #[test]
68    fn issue_4243_serialization_stops_at_the_output_budget() {
69        // Reused strings keep the test input small while serialized output expands.
70        let chunk = "x".repeat(1024 * 1024);
71        let fields = vec![&chunk; 65];
72        assert!(encode_bounded(&fields).unwrap_err().contains("exceeds 64 MiB"));
73        assert_eq!(encode_bounded(&vec!["small"]).unwrap(), br#"["small"]"#);
74    }
75
76
77    #[test]
78    fn issue_4243_rejects_malformed_and_oversized_requests_before_source_processing() {
79        assert!(plan_json(b"", "{").unwrap_err().contains("Invalid appearance request"));
80        assert!(catalog_json(b"", "{").unwrap_err().contains("Invalid appearance request"));
81        assert!(catalog_json(b"", &" ".repeat(256 * 1024 + 1)).unwrap_err().contains("scope budget"));
82        assert!(plan_json(b"", &" ".repeat(256 * 1024 + 1)).unwrap_err().contains("scope budget"));
83    }
84}