1use std::path::{Path, PathBuf};
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct DocumentModel {
51 pub uri: String,
53 pub root_type: String,
55 pub class_rust_path: Option<String>,
57 pub build_path: Option<String>,
60 pub populate_path: Option<String>,
62 pub public: bool,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct XamlMetadata {
71 pub name: String,
73 pub crate_name: String,
75 pub documents: Vec<DocumentModel>,
77 pub dependencies: Vec<String>,
81}
82
83impl XamlMetadata {
84 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 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 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
168fn 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
187enum 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
239struct 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 #[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 #[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}