serde_llsd/ser/xml.rs
1//! # ser/xml.rs -- XML serializer for LLSD
2//!
3//!
4//! Library for serializing and de-serializing data in
5//! Linden Lab Structured Data format.
6//!
7//! Format documentation is at http://wiki.secondlife.com/wiki/LLSD
8//!
9//! XML format.
10//
11// Animats
12// February, 2021.
13// License: LGPL.
14//
15//
16// Much like Serde-JSON, this will serialize and de-serialize only trees of LLSDValue items.
17
18use crate::LLSDValue;
19use anyhow::Error;
20use base64;
21use base64::Engine;
22use chrono;
23use chrono::TimeZone;
24use std::io::Write;
25//
26// Constants
27//
28pub const LLSDXMLPREFIX: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<llsd>\n";
29pub const LLSDXMLSENTINEL: &str = "<?xml"; // Must begin with this.
30const INDENT: usize = 4; // indent 4 spaces if asked
31
32// By convention, the public API of a Serde serializer is one or more `to_abc`
33// functions such as `to_string`, `to_bytes`, or `to_writer` depending on what
34// Rust types the serializer is able to produce as output.
35//
36
37/// LLSDValue to Writer
38pub fn to_writer<W: Write>(
39 writer: &mut W,
40 value: &LLSDValue,
41 do_indent: bool,
42) -> Result<(), Error> {
43 write!(writer, "{}", LLSDXMLPREFIX)?; // Standard XML prefix
44 generate_value(writer, value, if do_indent { INDENT } else { 0 }, 0);
45 write!(writer, "</llsd>")?;
46 writer.flush()?;
47 Ok(())
48}
49
50/// LLSDValue to String.
51/// Pretty prints out the value as XML. Indents by 4 spaces if requested.
52pub fn to_string(val: &LLSDValue, do_indent: bool) -> Result<String, Error> {
53 let mut s: Vec<u8> = Vec::new();
54 to_writer(&mut s, val, do_indent)?;
55 Ok(std::str::from_utf8(&s)?.to_string())
56}
57
58/// Generate one <TYPE> VALUE </TYPE> output. VALUE is recursive.
59fn generate_value<W: Write>(writer: &mut W, val: &LLSDValue, spaces: usize, indent: usize) {
60 // Output a single tag
61 fn tag<W: Write>(writer: &mut W, tag: &str, close: bool, indent: usize) {
62 if indent > 0 {
63 let _ = write!(writer, "{:1$}", " ", indent);
64 };
65 let _ = writeln!(writer, "<{}{}>", if close { "/" } else { "" }, tag);
66 }
67
68 // Internal fn - write out one tag with a value.
69 fn tag_value<W: Write>(writer: &mut W, tag: &str, text: &str, indent: usize) {
70 if indent > 0 {
71 let _ = write!(writer, "{:1$}", " ", indent);
72 };
73 if text.is_empty() {
74 // if empty, write as null tag
75 let _ = writeln!(writer, "<{} />", tag);
76 } else {
77 let _ = writeln!(writer, "<{}>{}</{}>", tag, xml_escape(text), tag);
78 }
79 }
80
81 // Use SL "nan", not Rust "NaN"
82 fn f64_to_xml(v: f64) -> String {
83 let ss = v.to_string();
84 if ss == "NaN" {
85 "nan".to_string()
86 } else {
87 ss
88 }
89 }
90 // Emit XML for all possible types.
91 match val {
92 LLSDValue::Undefined => tag_value(writer, "undef", "", indent),
93 LLSDValue::Boolean(v) => {
94 tag_value(writer, "boolean", if *v { "true" } else { "false" }, indent)
95 }
96 LLSDValue::String(v) => tag_value(writer, "string", v.as_str(), indent),
97 LLSDValue::URI(v) => tag_value(writer, "uri", v.as_str(), indent),
98 LLSDValue::Integer(v) => tag_value(writer, "integer", v.to_string().as_str(), indent),
99 LLSDValue::Real(v) => tag_value(writer, "real", f64_to_xml(*v).as_str(), indent),
100 LLSDValue::UUID(v) => tag_value(writer, "uuid", v.to_string().as_str(), indent),
101 LLSDValue::Binary(v) => tag_value(
102 writer,
103 "binary",
104 base64::engine::general_purpose::STANDARD.encode(v).as_str(),
105 indent,
106 ),
107 LLSDValue::Date(v) => tag_value(
108 writer,
109 "date",
110 &chrono::Utc
111 .timestamp_opt(*v, 0)
112 .earliest()
113 .unwrap() // may panic for times prior to January 1, 1970.
114 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
115 indent,
116 ),
117 LLSDValue::Map(v) => {
118 tag(writer, "map", false, indent);
119 for (key, value) in v {
120 tag_value(writer, "key", key, indent + spaces);
121 generate_value(writer, value, spaces, indent + spaces);
122 }
123 tag(writer, "map", true, indent);
124 }
125 LLSDValue::Array(v) => {
126 tag(writer, "array", false, indent);
127 for value in v {
128 generate_value(writer, value, spaces, indent + spaces);
129 }
130 tag(writer, "array", true, indent);
131 }
132 };
133}
134
135/// XML standard character escapes.
136fn xml_escape(unescaped: &str) -> String {
137 let mut s = String::new();
138 for ch in unescaped.chars() {
139 match ch {
140 '<' => s += "<",
141 '>' => s += ">",
142 '\'' => s += "'",
143 '&' => s += "&",
144 '"' => s += """,
145 _ => s.push(ch),
146 }
147 }
148 s
149}
150/*
151// Unit tests
152
153#[test]
154fn xmlparsetest1() {
155 const TESTXMLNAN: &str = r#"
156<?xml version="1.0" encoding="UTF-8"?>
157<llsd>
158<array>
159<real>nan</real>
160<real>0</real>
161<undef />
162</array>
163</llsd>
164"#;
165
166 const TESTXML1: &str = r#"
167<?xml version="1.0" encoding="UTF-8"?>
168<llsd>
169<map>
170 <key>region_id</key>
171 <uuid>67153d5b-3659-afb4-8510-adda2c034649</uuid>
172 <key>scale</key>
173 <string>one minute</string>
174 <key>simulator statistics</key>
175 <map>
176 <key>time dilation</key><real>0.9878624</real>
177 <key>sim fps</key><real>44.38898</real>
178 <key>pysics fps</key><real>44.38906</real>
179 <key>lsl instructions per second</key><real>0</real>
180 <key>total task count</key><real>4</real>
181 <key>active task count</key><real>0</real>
182 <key>active script count</key><real>4</real>
183 <key>main agent count</key><real>0</real>
184 <key>child agent count</key><real>0</real>
185 <key>inbound packets per second</key><real>1.228283</real>
186 <key>outbound packets per second</key><real>1.277508</real>
187 <key>pending downloads</key><real>0</real>
188 <key>pending uploads</key><real>0.0001096525</real>
189 <key>frame ms</key><real>0.7757886</real>
190 <key>net ms</key><real>0.3152919</real>
191 <key>sim other ms</key><real>0.1826937</real>
192 <key>sim physics ms</key><real>0.04323055</real>
193 <key>agent ms</key><real>0.01599029</real>
194 <key>image ms</key><real>0.01865955</real>
195 <key>script ms</key><real>0.1338836</real>
196 <!-- Comment - some additional test values -->
197 <key>hex number</key><binary encoding="base16">0fa1</binary>
198 <key>base64 number</key><binary>SGVsbG8gd29ybGQ=</binary>
199 <key>date</key><date>2006-02-01T14:29:53Z</date>
200 <key>array</key>
201 <array>
202 <boolean>false</boolean>
203 <integer>42</integer>
204 <undef/>
205 <uuid/>
206 <boolean>1</boolean>
207 </array>
208 </map>
209</map>
210</llsd>
211"#;
212
213 fn trytestcase(teststr: &str) {
214 // Internal utility function.
215 // Parse canned XML test case into internal format.
216 // Must not contain NaN, because NaN != Nan and the equal test will
217 let parsed1 = parse(teststr).unwrap();
218 println!("Parse of {}: \n{:#?}", teststr, parsed1);
219 // Generate XML back from parsed version.
220 let generated = to_xml_string(&parsed1, true).unwrap();
221 // Parse that.
222 let parsed2 = parse(&generated).unwrap();
223 // Check that parses match.
224 assert_eq!(parsed1, parsed2);
225 }
226 trytestcase(TESTXML1);
227 // Test NAN case
228 {
229 let parsed1 = parse(TESTXMLNAN).unwrap();
230 println!("Parse of {}: \n{:#?}", TESTXMLNAN, parsed1);
231 // Generate XML back from parsed version.
232 let generated = to_xml_string(&parsed1, true).unwrap();
233 // Remove all white space for comparison
234 let s1 = TESTXMLNAN.replace(" ", "").replace("\n", "");
235 let s2 = generated.replace(" ", "").replace("\n", "");
236 assert_eq!(s1, s2);
237 }
238}
239*/