llsd-rs 0.1.6

A Rust library for parsing and serializing LLSD (Linden Lab Structured Data) format.
Documentation
use std::io::Write;

use base64::prelude::*;
use chrono::DateTime;
use uuid::Uuid;
use xml::{EventReader, EventWriter};

use crate::Uri;

use super::Llsd;

pub fn from_parser<R: std::io::Read>(parser: EventReader<R>) -> Result<Llsd, anyhow::Error> {
    use xml::reader::XmlEvent;
    let mut stack: Vec<Llsd> = Vec::new();
    let mut name_stack: Vec<String> = Vec::new();
    let mut key_stack: Vec<Option<String>> = Vec::new();
    let mut start = false;
    let mut end = false;

    for event in parser {
        match event {
            Ok(XmlEvent::StartElement { name, .. }) => {
                name_stack.push(name.local_name.clone());
                if !start {
                    if name.local_name.as_str() != "llsd" {
                        return Err(anyhow::anyhow!(
                            "Error parsing LLSD: expected <llsd> root element, got {}",
                            name.local_name
                        ));
                    }
                    start = true;
                    continue;
                }
                match name.local_name.as_str() {
                    "llsd" => {
                        return Err(anyhow::anyhow!(
                            "Error parsing LLSD: unexpected <llsd> element"
                        ));
                    }
                    "undef" => stack.push(Llsd::Undefined),
                    "boolean" => stack.push(Llsd::Boolean(false)),
                    "string" => stack.push(Llsd::String(String::new())),
                    "uuid" => stack.push(Llsd::Uuid(Default::default())),
                    "uri" => stack.push(Llsd::Uri(Uri::Empty)),
                    "date" => stack.push(Llsd::Date(Default::default())),
                    "binary" => stack.push(Llsd::Binary(Vec::new())),
                    "integer" => stack.push(Llsd::Integer(0)),
                    "real" => stack.push(Llsd::Real(0.0)),
                    "array" => stack.push(Llsd::Array(Vec::new())),
                    "map" => stack.push(Llsd::Map(Default::default())),
                    "key" => {
                        key_stack.push(None);
                    }
                    _ => {
                        return Err(anyhow::anyhow!(
                            "Error parsing LLSD: unexpected element {}",
                            name.local_name
                        ));
                    }
                }
            }
            Ok(XmlEvent::Characters(data)) => {
                if key_stack.last() == Some(&None) {
                    key_stack.pop();
                    key_stack.push(Some(data.clone()));
                } else if let Some(llsd) = stack.last_mut() {
                    match llsd {
                        Llsd::Boolean(_) => match data.as_str() {
                            "true" => *llsd = Llsd::Boolean(true),
                            "false" => *llsd = Llsd::Boolean(false),
                            "1" => *llsd = Llsd::Boolean(true),
                            "0" => *llsd = Llsd::Boolean(false),
                            _ => {
                                return Err(anyhow::anyhow!(
                                    "Error parsing LLSD: expected boolean, got {}",
                                    data
                                ));
                            }
                        },
                        &mut Llsd::String(ref mut s) => s.push_str(data.as_str()),
                        &mut Llsd::Uuid(ref mut u) => *u = Uuid::parse_str(data.as_str())?,
                        &mut Llsd::Uri(ref mut u) => *u = Uri::parse(data.as_str()),
                        &mut Llsd::Date(ref mut d) => {
                            *d = DateTime::parse_from_rfc3339(data.as_str())?.into()
                        }
                        &mut Llsd::Binary(ref mut b) => {
                            *b = BASE64_STANDARD.decode(data.as_bytes())?
                        }
                        &mut Llsd::Integer(ref mut i) => *i = data.parse()?,
                        &mut Llsd::Real(ref mut r) => match data.as_str() {
                            "nan" => *r = f64::NAN,
                            "inf" => *r = f64::INFINITY,
                            "-inf" => *r = f64::NEG_INFINITY,
                            _ => *r = data.parse()?,
                        },
                        _ => {
                            return Err(anyhow::anyhow!(
                                "Error parsing LLSD: unexpected characters {}",
                                data
                            ));
                        }
                    }
                }
            }
            Ok(XmlEvent::EndElement { name }) => {
                if name_stack.pop().as_ref() != Some(&name.local_name) {
                    return Err(anyhow::anyhow!(
                        "Error parsing LLSD: unexpected end element {}",
                        name.local_name
                    ));
                }
                if name.local_name.as_str() == "key" {
                    if key_stack.last().is_none() {
                        return Err(anyhow::anyhow!("Error parsing LLSD: missing key"));
                    }
                } else if name.local_name.as_str() == "llsd" {
                    end = true;
                    break;
                } else if let Some(last) = stack.pop() {
                    match stack.last_mut() {
                        Some(Llsd::Array(parent)) => parent.push(last),
                        Some(Llsd::Map(parent)) => {
                            if let Some(Some(key)) = key_stack.pop() {
                                parent.insert(key.to_string(), last);
                            } else {
                                return Err(anyhow::anyhow!("Error parsing LLSD: missing key"));
                            }
                        }
                        _ => stack.push(last),
                    }
                } else {
                    return Err(anyhow::anyhow!(
                        "Error parsing LLSD: unexpected end element {}",
                        name.local_name
                    ));
                }
            }
            Err(e) => return Err(anyhow::anyhow!("Error parsing LLSD: {}", e)),
            _ => {}
        }
    }
    if !end {
        Err(anyhow::anyhow!(
            "Error parsing LLSD: unexpected end of input"
        ))
    } else if !key_stack.is_empty() {
        Err(anyhow::anyhow!("Error parsing LLSD: missing key"))
    } else if stack.len() > 1 {
        Err(anyhow::anyhow!(
            "Error parsing LLSD: expected 1 value, got {}",
            stack.len()
        ))
    } else {
        Ok(stack.pop().unwrap_or(Llsd::Undefined))
    }
}

pub fn from_str(data: &str) -> Result<Llsd, anyhow::Error> {
    from_parser(EventReader::from_str(data))
}

pub fn from_reader<R: std::io::Read>(reader: R) -> Result<Llsd, anyhow::Error> {
    from_parser(EventReader::new(reader))
}

pub fn from_slice(data: &[u8]) -> Result<Llsd, anyhow::Error> {
    from_parser(EventReader::new(std::io::Cursor::new(data)))
}

fn write_inner<W: Write>(llsd: &Llsd, w: &mut EventWriter<W>) -> Result<(), anyhow::Error> {
    use xml::writer::XmlEvent;
    let tag = |w: &mut EventWriter<W>, tag, text: &str| -> Result<(), anyhow::Error> {
        w.write(XmlEvent::start_element(tag))?;
        if !text.is_empty() {
            w.write(XmlEvent::characters(text))?;
        }
        w.write(XmlEvent::end_element())?;
        Ok(())
    };
    fn f64_to_xml(v: f64) -> String {
        let ss = v.to_string();
        if ss == "NaN" { "nan".to_string() } else { ss }
    }
    match llsd {
        Llsd::Undefined => tag(w, "undef", "")?,
        Llsd::Boolean(b) => tag(w, "boolean", if *b { "1" } else { "0" })?,
        Llsd::String(s) => tag(w, "string", s)?,
        Llsd::Uuid(u) => tag(w, "uuid", u.to_string().as_str())?,
        Llsd::Uri(u) => tag(w, "uri", u.as_str())?,
        Llsd::Date(d) => tag(w, "date", d.to_rfc3339().as_str())?,
        Llsd::Binary(b) => {
            if b.is_empty() {
                tag(w, "binary", "")?;
            } else {
                w.write(XmlEvent::start_element("binary").attr("encoding", "base64"))?;
                w.write(XmlEvent::characters(&BASE64_STANDARD.encode(b)))?;
                w.write(XmlEvent::end_element())?;
            }
        }
        Llsd::Integer(i) => tag(w, "integer", &i.to_string())?,
        Llsd::Real(r) => tag(w, "real", f64_to_xml(*r).as_str())?,
        Llsd::Array(a) => {
            w.write(XmlEvent::start_element("array"))?;
            for v in a {
                write_inner(v, w)?;
            }
            w.write(XmlEvent::end_element())?;
        }
        Llsd::Map(m) => {
            w.write(XmlEvent::start_element("map"))?;
            for (k, v) in m {
                tag(w, "key", k)?;
                write_inner(v, w)?;
            }
            w.write(XmlEvent::end_element())?;
        }
    }
    Ok(())
}

pub fn write<W: Write>(llsd: &Llsd, w: &mut EventWriter<W>) -> Result<(), anyhow::Error> {
    use xml::writer::XmlEvent;
    w.write(XmlEvent::start_element("llsd"))?;
    write_inner(llsd, w)?;
    w.write(XmlEvent::end_element())?;
    Ok(())
}

pub fn to_pretty_string(llsd: &Llsd) -> Result<String, anyhow::Error> {
    let mut buf = Vec::new();
    write(
        llsd,
        &mut EventWriter::new_with_config(
            &mut buf,
            xml::writer::EmitterConfig::new().perform_indent(true),
        ),
    )?;
    Ok(String::from_utf8(buf)?)
}

pub fn to_string(llsd: &Llsd) -> Result<String, anyhow::Error> {
    let mut buf = Vec::new();
    write(llsd, &mut EventWriter::new(&mut buf))?;
    Ok(String::from_utf8(buf)?)
}

pub fn to_request(llsd: &Llsd) -> Result<Vec<u8>, anyhow::Error> {
    let mut buf = Vec::new();
    write(
        llsd,
        &mut EventWriter::new_with_config(
            &mut buf,
            xml::writer::EmitterConfig::new().write_document_declaration(false),
        ),
    )?;
    Ok(buf)
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{TimeZone, Utc};
    use std::collections::HashMap;
    use url::Url;

    fn round_trip(llsd: Llsd) {
        let encoded = to_string(&llsd).expect("Failed to encode");
        let decoded = from_str(&encoded).expect("Failed to decode");
        assert_eq!(llsd, decoded);
    }

    #[test]
    fn undefined() {
        round_trip(Llsd::Undefined);
    }

    #[test]
    fn boolean() {
        round_trip(Llsd::Boolean(true));
        round_trip(Llsd::Boolean(false));
    }

    #[test]
    fn integer() {
        round_trip(Llsd::Integer(42));
    }

    #[test]
    fn real() {
        round_trip(Llsd::Real(13.1415));
    }

    #[test]
    fn string() {
        round_trip(Llsd::String("Hello, LLSD!".to_owned()));
    }

    #[test]
    fn uri() {
        let url = Url::parse("https://example.com/").unwrap();
        round_trip(Llsd::Uri(url.into()));
    }

    #[test]
    fn uuid() {
        let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
        round_trip(Llsd::Uuid(uuid));
    }

    #[test]
    fn date() {
        let dt = Utc.timestamp_opt(1_620_000_000, 0).unwrap();
        round_trip(Llsd::Date(dt));
    }

    #[test]
    fn binary() {
        round_trip(Llsd::Binary(vec![0xde, 0xad, 0xbe, 0xef]));
    }

    #[test]
    fn array() {
        let arr = vec![
            Llsd::Integer(1),
            Llsd::String("two".into()),
            Llsd::Boolean(false),
        ];
        round_trip(Llsd::Array(arr));
    }

    #[test]
    fn map() {
        let mut map = HashMap::new();
        map.insert("answer".into(), Llsd::Integer(42));
        map.insert("pi".into(), Llsd::Real(13.14));
        map.insert("greeting".into(), Llsd::String("hello".into()));
        round_trip(Llsd::Map(map));
    }
}