lrcat-extractor 0.7.0

Extract data from Adobe Lightroomâ„¢ catalogs.
Documentation
/*
 This Source Code Form is subject to the terms of the Mozilla Public
 License, v. 2.0. If a copy of the MPL was not distributed with this
 file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

//! lron stands for Lightroom Object Notation, specific to Lightroom
//! that is found throughout the catalog database to store arbitrary
//! but structured data.
//!
//! lron looks like plist (before XML) or JSON, but doesn't match
//! either syntax.
//!
//! Note: I couldn't figure out what this format was called, so I
//! couldn't reuse an existing parser. If you have a better idea,
//! please, let me know.
//!
//! Note2: The crate [`agprefs`](https://crates.io/crates/agprefs)
//! call it `agprefs`.
//!
//! It has the form
//! ```json
//! name = {
//!   object = {
//!     x = 1.3,
//!     string = "some text",
//!   },
//! }
//! ```
//!
//! The text is parsed using peg.
//!
//! You obtain the expression from the text by the following:
//! ```
//! use lrcat::lron;
//!
//! let lron_text = "name = {}"; // load the text in the string
//!
//! if let Ok(object) = lron::Value::from_string(lron_text) {
//!     // do your stuff with it
//! }
//! ```

/// A key/value pair.
#[derive(Clone, Debug, PartialEq)]
pub struct Pair {
    pub key: String,
    pub value: Value,
}

/// Lron Value
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
    Dict(Vec<Value>),
    Pair(Box<Pair>),
    Str(String),
    ZStr(String),
    Int(i64),
    Float(f64),
    Bool(bool),
}

impl Value {
    /// Return the dict. Currently it's still pairs.
    pub fn as_dict(&self) -> Option<&[Value]> {
        if let Self::Dict(dict) = self {
            Some(dict.as_slice())
        } else {
            None
        }
    }

    /// Return the pair.
    pub fn as_pair(&self) -> Option<&Pair> {
        if let Self::Pair(p) = self {
            Some(p.as_ref())
        } else {
            None
        }
    }

    /// Return an str.
    pub fn as_str(&self) -> Option<&str> {
        match self {
            Self::Str(s) | Self::ZStr(s) => Some(s.as_str()),
            _ => None,
        }
    }

    /// Return the bool.
    pub fn as_bool(&self) -> Option<bool> {
        if let Self::Bool(b) = self {
            Some(*b)
        } else {
            None
        }
    }

    /// Try to convert the value as a float of type T. This is
    /// because number are untyped in Lron, and the parser will manage
    /// float or int.  Instead of having a generic Number type, it's
    /// better this way.
    pub fn as_float<T>(&self) -> Option<T>
    where
        T: std::convert::From<f64>,
    {
        match *self {
            Self::Int(i) => Some((i as f64).into()),
            Self::Float(f) => Some(f.into()),
            _ => None,
        }
    }

    /// Try to convert the value as an integer of type T.
    pub fn as_int<T>(&self) -> Option<T>
    where
        T: std::convert::From<i64>,
    {
        match *self {
            Self::Int(i) => Some(i.into()),
            Self::Float(f) => Some((f as i64).into()),
            _ => None,
        }
    }

    /// Create a value from a string
    pub fn from_string(s: &str) -> Result<Value> {
        lron::root(s)
    }
}

/// Alias result type for parsing a Lron object.
type Result<T> = std::result::Result<T, peg::error::ParseError<peg::str::LineCol>>;

// lron stand for Lightroom Object Notation
// Some sort of JSON specific to Lightroom
//
// lron data syntax is defined in this PEG grammar.
peg::parser! {grammar lron() for str {

use std::str::FromStr;

pub rule root() -> Value
        = key:identifier() _() "=" _() value:array() _()
    { Value::Pair(Box::new(Pair{key, value: Value::Dict(value)})) }

rule array() -> Vec<Value>
        = "{" _() v:(value() ** (_() "," _())) _()(",")? _() "}" { v }

rule bracket_key() -> String
        = key:string_literal()
    { key.to_owned() } /
        key:int()
    { key.to_string() }

rule pair() -> Pair
        = key:identifier() _() "=" _() value:value()
    { Pair { key, value } } /
        "[" key:bracket_key() "]" _() "=" _() value:value()
    { Pair { key, value } }

rule value() -> Value
        = a:array() { Value::Dict(a) } /
        p:pair() { Value::Pair(Box::new(p)) } /
        i:int() { Value::Int(i) } /
        b:bool() { Value::Bool(b) } /
        f:float() { Value::Float(f) } /
        s:string_literal() { Value::Str(s) } /
        a:array() { Value::Dict(a) } /
        z:zstr() { Value::ZStr(z) }

rule int() -> i64
        = n:$("-"? ['0'..='9']+) !"." {? i64::from_str(n).or(Err("out of range for i32")) } / expected!("integer")

rule bool() -> bool
        = "true" { true } / "false" { false }

rule float() -> f64
        = f:$("-"? ['0'..='9']+ "." ['0'..='9']+) {? f64::from_str(f).or(Err("invalid float")) } / expected!("floating point")

rule identifier() -> String
        = s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_']+) { s.to_owned() } / expected!("identifier")

// String escape, either literal EOL or quotes.
rule escape() -> &'static str
        = "\\\"" { "\"" } / "\\\n" { "\n" }

// String literal can be escaped.
rule string_literal() -> String
        = "\"" s:((escape() / $(!['"'][_]))*) "\"" { s.join("") }

rule zstr() -> String
        = "ZSTR" _() s:string_literal() { s }

rule _() = quiet!{[' ' | '\r' | '\n' | '\t']*}

}}

#[cfg(test)]
mod tests {
    use super::Value;

    #[test]
    fn test_long_int() {
        const DATA: &str = include_str!("../data/volumeinfo.lron");
        let _ = Value::from_string(DATA).expect("Couldn't parse data");
    }

    #[test]
    fn test_int_key() {
        const DATA: &str = include_str!("../data/metadataThatTriggersRepublish.lron");
        let v = Value::from_string(DATA).expect("Couldn't parse data");
        if let Some(p) = v.as_pair()
            && let Some(dict) = p.value.as_dict()
        {
            if let Some(p) = dict[0].as_pair() {
                assert_eq!(&p.key, "800348");
            } else {
                unreachable!();
            }
        } else {
            unreachable!();
        }
    }

    #[test]
    fn test_parser() {
        const DATA: &str = include_str!("../data/test_lron");
        let r = Value::from_string(DATA);

        assert!(r.is_ok());
        let o = r.unwrap();

        assert!(matches!(o, Value::Pair(_)));
        if let Some(p) = o.as_pair() {
            assert_eq!(p.key, "s");
            assert!(matches!(p.value, Value::Dict(_)));

            if let Some(d) = p.value.as_dict() {
                assert_eq!(d.len(), 2);
                assert!(matches!(d[0], Value::Dict(_)));
                if let Some(d) = d[0].as_dict() {
                    assert_eq!(d.len(), 5);
                    assert!(matches!(d[0], Value::Pair(_)));
                    assert!(matches!(d[1], Value::Pair(_)));
                    assert!(matches!(d[2], Value::Pair(_)));
                    assert!(matches!(d[3], Value::Pair(_)));
                    assert!(matches!(d[4], Value::Pair(_)));
                    if let Some(p) = d[4].as_pair() {
                        assert_eq!(p.key, "someOther");
                        if let Some(value) = p.value.as_str() {
                            let r2 = Value::from_string(value);
                            assert!(r2.is_ok());
                        }
                        assert_eq!(
                            p.value.as_str(),
                            Some(
                                "anObject = {\n\
                                 key = \"lr\",\n\
                                 }\n"
                            )
                        );
                    }
                }
                assert!(matches!(d[1], Value::Pair(_)));
                if let Some(p) = d[1].as_pair() {
                    assert_eq!(p.key, "combine");
                    assert_eq!(p.value.as_str(), Some("intersect"));
                }
            }
        } else {
            unreachable!();
        }
    }
}