Skip to main content

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 += "&lt;",
141            '>' => s += "&gt;",
142            '\'' => s += "&apos;",
143            '&' => s += "&amp;",
144            '"' => s += "&quot;",
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*/