Skip to main content

ferroui_build_scan/
xaml_metadata.rs

1//! The `.xamlmeta` file of a crate with compiled markup, as far as the
2//! compiler reads it today: the assembly and its compiled documents
3//! (docs/porting/xaml.md, 9.5.1 `AssemblyModel` / `DocumentModel`, 9.6.3,
4//! 9.7.3).
5//!
6//! Upstream's compiler reaches the compiled documents of a referenced
7//! assembly through that assembly's metadata: the type
8//! `CompiledFerroXaml.!FerroResources` (the port's name of it) with one static `Build:<path>`
9//! method per document, and the `x:Class` types. A Rust crate cannot be
10//! inspected that way, so a crate with compiled markup describes its
11//! documents in this file, and the compiler of a dependent crate synthesises
12//! the same type from it (`rust_emitter::CompiledMarkupTypeSystem` of the loader).
13//!
14//! # Transport
15//!
16//! The file reaches dependent crates through Cargo `links` metadata
17//! (`DEP_<CRATE>_XAML_XAMLMETA`, 9.6.3 and 9.6.8): a crate whose build script
18//! compiles its documents (`ferroui-build`) writes the file to `OUT_DIR`, and
19//! a crate whose compiled markup is checked in keeps the file next to its
20//! generated `compiled_xaml.rs` (drift-tested with it); the build script of
21//! either prints its path for the build scripts of the dependents.
22//!
23//! # Format
24//!
25//! JSON, written by [`XamlMetadata::to_json`] (deterministic: the same model
26//! gives the same text):
27//!
28//! ```text
29//! {
30//!   "name": "Tests",                                  assembly name
31//!   "crate_name": "include_fixture_theme",            crate, as paths spell it
32//!   "documents": [
33//!     {
34//!       "uri": "ferres://Tests/Style.xaml",
35//!       "root_type": "FerroUI.Styling.Style",          full name of the type the document builds
36//!       "class_rust_path": null,                       the x:Class type, when the document has one
37//!       "build_path": "::include_fixture_theme::compiled_xaml::build_style_xaml",
38//!       "populate_path": null,
39//!       "public": true                                 x:ClassModifier
40//!     }
41//!   ],
42//!   "dependencies": ["../Other/compiled_xaml.xamlmeta"]   relative to this file, or absolute
43//! }
44//! ```
45
46use std::path::{Path, PathBuf};
47
48/// A compiled document of a crate (`DocumentModel`).
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct DocumentModel {
51    /// The URI the document is loaded by (`ferres://<assembly>/<path>`).
52    pub uri: String,
53    /// The full name (`Namespace.Name`) of the type of the root of the document.
54    pub root_type: String,
55    /// The public Rust path of the `x:Class` type of the document.
56    pub class_rust_path: Option<String>,
57    /// The absolute Rust path of the build function of a document without a class:
58    /// `fn(Option<Rc<dyn IServiceProvider>>) -> Result<Ref<T>, XamlLoadException>`.
59    pub build_path: Option<String>,
60    /// The absolute Rust path of the populate function of the document.
61    pub populate_path: Option<String>,
62    /// Whether the document is public (`x:ClassModifier`): only a public document can be
63    /// included from another crate.
64    pub public: bool,
65}
66
67/// The compiled markup a crate exports to its dependents (`AssemblyModel`, the part of
68/// it the compiler reads today).
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct XamlMetadata {
71    /// The assembly name of the crate.
72    pub name: String,
73    /// The name of the crate, as Rust paths spell it.
74    pub crate_name: String,
75    /// The compiled documents, in the order of the compilation.
76    pub documents: Vec<DocumentModel>,
77    /// The `.xamlmeta` files of the crates this crate includes documents of, relative to
78    /// the directory of this file or absolute (the files of a build are in the `OUT_DIR`
79    /// of each crate).
80    pub dependencies: Vec<String>,
81}
82
83impl XamlMetadata {
84    /// The model as the text of a `.xamlmeta` file.
85    pub fn to_json(&self) -> String {
86        let mut text = String::from("{\n");
87        text.push_str(&format!("  \"name\": {},\n", json_string(&self.name)));
88        text.push_str(&format!("  \"crate_name\": {},\n", json_string(&self.crate_name)));
89        text.push_str("  \"documents\": [");
90        for (index, document) in self.documents.iter().enumerate() {
91            text.push_str(if index == 0 { "\n" } else { ",\n" });
92            text.push_str("    {\n");
93            text.push_str(&format!("      \"uri\": {},\n", json_string(&document.uri)));
94            text.push_str(&format!("      \"root_type\": {},\n", json_string(&document.root_type)));
95            text.push_str(&format!("      \"class_rust_path\": {},\n", json_optional(&document.class_rust_path)));
96            text.push_str(&format!("      \"build_path\": {},\n", json_optional(&document.build_path)));
97            text.push_str(&format!("      \"populate_path\": {},\n", json_optional(&document.populate_path)));
98            text.push_str(&format!("      \"public\": {}\n", document.public));
99            text.push_str("    }");
100        }
101        text.push_str(if self.documents.is_empty() { "],\n" } else { "\n  ],\n" });
102        text.push_str("  \"dependencies\": [");
103        let dependencies: Vec<String> = self.dependencies.iter().map(|path| json_string(path)).collect();
104        text.push_str(&dependencies.join(", "));
105        text.push_str("]\n}\n");
106        text
107    }
108
109    /// Reads the model from the text of a `.xamlmeta` file.
110    pub fn parse(text: &str) -> Result<Self, String> {
111        let value = JsonParser { text: text.as_bytes(), at: 0 }.document()?;
112        let object = value.object("the file")?;
113        let documents = field(object, "documents")?
114            .array("documents")?
115            .iter()
116            .map(|document| {
117                let document = document.object("a document")?;
118                Ok(DocumentModel {
119                    uri: field(document, "uri")?.string("uri")?.to_string(),
120                    root_type: field(document, "root_type")?.string("root_type")?.to_string(),
121                    class_rust_path: field(document, "class_rust_path")?.optional_string("class_rust_path")?,
122                    build_path: field(document, "build_path")?.optional_string("build_path")?,
123                    populate_path: field(document, "populate_path")?.optional_string("populate_path")?,
124                    public: field(document, "public")?.boolean("public")?,
125                })
126            })
127            .collect::<Result<Vec<_>, String>>()?;
128        let dependencies = field(object, "dependencies")?
129            .array("dependencies")?
130            .iter()
131            .map(|path| path.string("a dependency").map(str::to_string))
132            .collect::<Result<Vec<_>, String>>()?;
133        Ok(Self {
134            name: field(object, "name")?.string("name")?.to_string(),
135            crate_name: field(object, "crate_name")?.string("crate_name")?.to_string(),
136            documents,
137            dependencies,
138        })
139    }
140
141    /// Reads the `.xamlmeta` file `path` and, transitively, the files of its
142    /// dependencies: every crate whose documents the compiler may reach through it
143    /// (`DEP_*` metadata exists only for direct dependencies, 9.6.3). Each assembly is
144    /// listed once, `path` first.
145    pub fn read(path: impl AsRef<Path>) -> Result<Vec<XamlMetadata>, String> {
146        let mut read: Vec<(PathBuf, XamlMetadata)> = Vec::new();
147        let mut pending = vec![path.as_ref().to_path_buf()];
148        while let Some(path) = pending.pop() {
149            let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
150            let metadata = Self::parse(&text).map_err(|e| format!("{}: {e}", path.display()))?;
151            if read.iter().any(|(_, known)| known.name == metadata.name) {
152                continue;
153            }
154            let directory = path.parent().map(Path::to_path_buf).unwrap_or_default();
155            for dependency in metadata.dependencies.iter().rev() {
156                pending.push(directory.join(dependency));
157            }
158            read.push((path, metadata));
159        }
160        Ok(read.into_iter().map(|(_, metadata)| metadata).collect())
161    }
162}
163
164fn json_optional(value: &Option<String>) -> String {
165    value.as_deref().map_or_else(|| "null".to_string(), json_string)
166}
167
168/// `text` as a JSON string literal.
169fn json_string(text: &str) -> String {
170    let mut literal = String::with_capacity(text.len() + 2);
171    literal.push('"');
172    for character in text.chars() {
173        match character {
174            '"' => literal.push_str("\\\""),
175            '\\' => literal.push_str("\\\\"),
176            '\n' => literal.push_str("\\n"),
177            '\r' => literal.push_str("\\r"),
178            '\t' => literal.push_str("\\t"),
179            c if (c as u32) < 0x20 => literal.push_str(&format!("\\u{:04x}", c as u32)),
180            c => literal.push(c),
181        }
182    }
183    literal.push('"');
184    literal
185}
186
187/// A JSON value.
188enum JsonValue {
189    Null,
190    Boolean(bool),
191    Number,
192    String(String),
193    Array(Vec<JsonValue>),
194    Object(Vec<(String, JsonValue)>),
195}
196
197impl JsonValue {
198    fn object(&self, what: &str) -> Result<&[(String, JsonValue)], String> {
199        match self {
200            JsonValue::Object(fields) => Ok(fields),
201            _ => Err(format!("{what} is not an object")),
202        }
203    }
204
205    fn array(&self, what: &str) -> Result<&[JsonValue], String> {
206        match self {
207            JsonValue::Array(items) => Ok(items),
208            _ => Err(format!("{what} is not an array")),
209        }
210    }
211
212    fn string(&self, what: &str) -> Result<&str, String> {
213        match self {
214            JsonValue::String(text) => Ok(text),
215            _ => Err(format!("{what} is not a string")),
216        }
217    }
218
219    fn optional_string(&self, what: &str) -> Result<Option<String>, String> {
220        match self {
221            JsonValue::Null => Ok(None),
222            JsonValue::String(text) => Ok(Some(text.clone())),
223            _ => Err(format!("{what} is neither a string nor null")),
224        }
225    }
226
227    fn boolean(&self, what: &str) -> Result<bool, String> {
228        match self {
229            JsonValue::Boolean(value) => Ok(*value),
230            _ => Err(format!("{what} is not a boolean")),
231        }
232    }
233}
234
235fn field<'a>(object: &'a [(String, JsonValue)], name: &str) -> Result<&'a JsonValue, String> {
236    object.iter().find(|(key, _)| key == name).map(|(_, value)| value).ok_or_else(|| format!("\"{name}\" is missing"))
237}
238
239/// A reader of the JSON the model is written in (RFC 8259; numbers are accepted and not
240/// kept, the model has none).
241struct JsonParser<'a> {
242    text: &'a [u8],
243    at: usize,
244}
245
246impl JsonParser<'_> {
247    fn document(mut self) -> Result<JsonValue, String> {
248        let value = self.value()?;
249        self.whitespace();
250        if self.at != self.text.len() {
251            return Err(self.error("text after the value"));
252        }
253        Ok(value)
254    }
255
256    fn error(&self, what: &str) -> String {
257        let line = self.text[..self.at.min(self.text.len())].iter().filter(|b| **b == b'\n').count() + 1;
258        format!("invalid JSON at line {line}: {what}")
259    }
260
261    fn whitespace(&mut self) {
262        while self.at < self.text.len() && matches!(self.text[self.at], b' ' | b'\t' | b'\n' | b'\r') {
263            self.at += 1;
264        }
265    }
266
267    fn expect(&mut self, byte: u8) -> Result<(), String> {
268        self.whitespace();
269        if self.text.get(self.at) == Some(&byte) {
270            self.at += 1;
271            Ok(())
272        } else {
273            Err(self.error(&format!("expected '{}'", byte as char)))
274        }
275    }
276
277    fn keyword(&mut self, keyword: &str, value: JsonValue) -> Result<JsonValue, String> {
278        if self.text[self.at..].starts_with(keyword.as_bytes()) {
279            self.at += keyword.len();
280            Ok(value)
281        } else {
282            Err(self.error("unexpected character"))
283        }
284    }
285
286    fn value(&mut self) -> Result<JsonValue, String> {
287        self.whitespace();
288        match self.text.get(self.at) {
289            Some(b'{') => {
290                self.at += 1;
291                let mut fields = Vec::new();
292                self.whitespace();
293                if self.text.get(self.at) == Some(&b'}') {
294                    self.at += 1;
295                    return Ok(JsonValue::Object(fields));
296                }
297                loop {
298                    self.whitespace();
299                    let key = self.string()?;
300                    self.expect(b':')?;
301                    fields.push((key, self.value()?));
302                    self.whitespace();
303                    match self.text.get(self.at) {
304                        Some(b',') => self.at += 1,
305                        Some(b'}') => {
306                            self.at += 1;
307                            return Ok(JsonValue::Object(fields));
308                        }
309                        _ => return Err(self.error("expected ',' or '}'")),
310                    }
311                }
312            }
313            Some(b'[') => {
314                self.at += 1;
315                let mut items = Vec::new();
316                self.whitespace();
317                if self.text.get(self.at) == Some(&b']') {
318                    self.at += 1;
319                    return Ok(JsonValue::Array(items));
320                }
321                loop {
322                    items.push(self.value()?);
323                    self.whitespace();
324                    match self.text.get(self.at) {
325                        Some(b',') => self.at += 1,
326                        Some(b']') => {
327                            self.at += 1;
328                            return Ok(JsonValue::Array(items));
329                        }
330                        _ => return Err(self.error("expected ',' or ']'")),
331                    }
332                }
333            }
334            Some(b'"') => Ok(JsonValue::String(self.string()?)),
335            Some(b't') => self.keyword("true", JsonValue::Boolean(true)),
336            Some(b'f') => self.keyword("false", JsonValue::Boolean(false)),
337            Some(b'n') => self.keyword("null", JsonValue::Null),
338            Some(b'-' | b'0'..=b'9') => {
339                while self.at < self.text.len() && matches!(self.text[self.at], b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9') {
340                    self.at += 1;
341                }
342                Ok(JsonValue::Number)
343            }
344            Some(_) => Err(self.error("unexpected character")),
345            None => Err(self.error("unexpected end")),
346        }
347    }
348
349    fn hex4(&mut self) -> Result<u32, String> {
350        let digits = self.text.get(self.at..self.at + 4).ok_or_else(|| self.error("incomplete escape"))?;
351        let digits = std::str::from_utf8(digits).map_err(|_| self.error("invalid escape"))?;
352        let value = u32::from_str_radix(digits, 16).map_err(|_| self.error("invalid escape"))?;
353        self.at += 4;
354        Ok(value)
355    }
356
357    fn string(&mut self) -> Result<String, String> {
358        if self.text.get(self.at) != Some(&b'"') {
359            return Err(self.error("expected a string"));
360        }
361        self.at += 1;
362        let mut bytes: Vec<u8> = Vec::new();
363        loop {
364            let Some(&byte) = self.text.get(self.at) else {
365                return Err(self.error("unterminated string"));
366            };
367            self.at += 1;
368            match byte {
369                b'"' => break,
370                b'\\' => {
371                    let Some(&escape) = self.text.get(self.at) else {
372                        return Err(self.error("unterminated string"));
373                    };
374                    self.at += 1;
375                    let character = match escape {
376                        b'"' => '"',
377                        b'\\' => '\\',
378                        b'/' => '/',
379                        b'b' => '\u{8}',
380                        b'f' => '\u{c}',
381                        b'n' => '\n',
382                        b'r' => '\r',
383                        b't' => '\t',
384                        b'u' => {
385                            let first = self.hex4()?;
386                            let code = if (0xd800..0xdc00).contains(&first) && self.text[self.at..].starts_with(b"\\u") {
387                                self.at += 2;
388                                let second = self.hex4()?;
389                                0x10000 + ((first - 0xd800) << 10) + (second.wrapping_sub(0xdc00) & 0x3ff)
390                            } else {
391                                first
392                            };
393                            char::from_u32(code).ok_or_else(|| self.error("invalid escape"))?
394                        }
395                        _ => return Err(self.error("invalid escape")),
396                    };
397                    let mut buffer = [0u8; 4];
398                    bytes.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes());
399                }
400                byte if byte < 0x20 => return Err(self.error("control character in a string")),
401                byte => bytes.push(byte),
402            }
403        }
404        String::from_utf8(bytes).map_err(|_| self.error("invalid UTF-8"))
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    /// Not from upstream: the model is read back as it was written.
413    #[test]
414    fn metadata_round_trips_through_its_text() {
415        let metadata = XamlMetadata {
416            name: "\u{30a2}\u{30bb}\u{30f3}\u{30d6}\u{30ea}".to_string(),
417            crate_name: "fixture".to_string(),
418            documents: vec![
419                DocumentModel {
420                    uri: "ferres://Tests/Folder/\"Quoted\".xaml".to_string(),
421                    root_type: "FerroUI.Styling.Style".to_string(),
422                    class_rust_path: None,
423                    build_path: Some("::fixture::compiled_xaml::build_quoted_xaml".to_string()),
424                    populate_path: None,
425                    public: true,
426                },
427                DocumentModel {
428                    uri: "ferres://Tests/Theme.xaml".to_string(),
429                    root_type: "Fixture.Theme".to_string(),
430                    class_rust_path: Some("::fixture::Theme".to_string()),
431                    build_path: None,
432                    populate_path: Some("::fixture::compiled_xaml::populate".to_string()),
433                    public: false,
434                },
435            ],
436            dependencies: vec!["../Other/compiled_xaml.xamlmeta".to_string()],
437        };
438        let text = metadata.to_json();
439        assert_eq!(XamlMetadata::parse(&text), Ok(metadata));
440        let empty = XamlMetadata { name: "A".into(), crate_name: "a".into(), documents: Vec::new(), dependencies: Vec::new() };
441        assert_eq!(XamlMetadata::parse(&empty.to_json()), Ok(empty));
442    }
443
444    /// Not from upstream: escapes and errors of the reader.
445    #[test]
446    fn metadata_reader_decodes_escapes_and_reports_errors() {
447        let text = r#"{"name": "Aé😀\/", "crate_name": "a", "documents": [], "dependencies": [], "extra": [1.5e3, -2]}"#;
448        assert_eq!(XamlMetadata::parse(text).map(|m| m.name), Ok("A\u{e9}\u{1f600}/".to_string()));
449        assert_eq!(
450            XamlMetadata::parse(r#"{"name": "A", "documents": [], "dependencies": []}"#),
451            Err("\"crate_name\" is missing".to_string())
452        );
453        assert_eq!(XamlMetadata::parse("{\"name\": \n tru}"), Err("invalid JSON at line 2: unexpected character".to_string()));
454    }
455}