ifc_lite_wasm/api/
appearance.rs1use 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
24pub(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 #[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 #[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 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}