serde_llsd/de/
notation.rs

1//! #  de/notation -- de-serialize LLSD, "notation" form.
2//!
3//!  Library for serializing and de-serializing data in
4//!  Linden Lab Structured Data format.
5//!
6//!  Format documentation is at http://wiki.secondlife.com/wiki/LLSD
7//!
8//!  Notation format.
9//!  Similar to JSON, but not compatible
10//!
11//! Notation format comes in two forms - bytes, and UTF-8 characters.
12//! UTF-8 format is always valid UTF-8 strings, and can be encapsulated
13//! inside XML if desired. This format is used inside SL/OS for "gltf material overrides".
14//!
15//! Byte string form is binary bytes, and cannot be encapsulated inside XML.
16//! It can contain raw binary fields of the form b(NN)"rawbytes".
17//! and raw strings of the form s(NN)"rawstring".
18//! This form is used inside SL/OS for script uploads. We think.
19//
20//  Animats
21//  June, 2023.
22//  License: LGPL.
23//
24use crate::LLSDValue;
25use anyhow::{anyhow, Error};
26use std::collections::HashMap;
27use core::iter::{Peekable};
28use core::str::{Chars, Bytes};
29use uuid::{Uuid};
30use chrono::DateTime;
31use base64::Engine;
32
33//
34//  Constants
35//
36/// Notation LLSD prefix
37pub const LLSDNOTATIONPREFIX: &str = "<? llsd/notation ?>\n"; 
38/// Sentinel, must match exactly.
39pub const LLSDNOTATIONSENTINEL: &str = LLSDNOTATIONPREFIX;
40
41/// Exported parse from bytes.
42pub fn from_bytes(b: &[u8]) -> Result<LLSDValue, Error> {
43    LLSDStreamBytes::parse(b)
44}
45
46/// Exported parse from str.
47pub fn from_str(s: &str) -> Result<LLSDValue, Error> {
48    LLSDStreamChars::parse(s)
49}
50
51/// An LLSD stream. May be either a UTF-8 stream or a byte stream.
52/// Generic trait.
53trait LLSDStream<C, S> {
54    /// Get next char/byte
55    fn next(&mut self) -> Option<C>;
56    
57    /// Get next char/byte, result
58    fn next_ok(&mut self) -> Result<C, Error> {
59        if let Some(ch) = self.next() {
60            Ok(ch)
61        } else {
62            Err(anyhow!("Unexpected end of input parsing Notation"))
63        }           
64    }
65    
66    /// Peek at next char/byte
67    fn peek(&mut self) -> Option<&C>;
68    
69    //  Peek at next char, as result
70    fn peek_ok(&mut self) -> Result<&C, Error> {
71        if let Some(ch) = self.peek() {
72            Ok(ch)
73        } else {
74            Err(anyhow!("Unexpected end of input parsing Notation"))
75        }           
76    }
77    
78    /// Convert into char
79    fn into_char(ch: &C) -> char;
80    
81    /// Consume whitespace. Next char will be non-whitespace.
82    //  Need to treat explicit "\n" as whitespace.
83    fn consume_whitespace(&mut self) -> Result<(), Error> {
84        while let Some(ch) = self.peek() {
85            match Self::into_char(ch) {
86                ' ' | '\n' => { let _ = self.next(); },                 // ignore leading white space
87                '\\' => {
88                    let _ = self.next();                                // consume backslash
89                    let ch = Self::into_char(&self.next_ok()?);         // expecting 'n'
90                    if ch != 'n' {                                      // Explicit "\n" is normal white space
91                        return Err(anyhow!("Unexpected escape sequence \"\\{}\" where white space expected.", ch));
92                    }   
93                }
94                _ => break
95            }
96        }
97        Ok(())  
98    }
99    
100    /// Consume expected non-whitespace char
101    fn consume_char(&mut self, expected_ch: char) -> Result<(), Error> {
102        self.consume_whitespace()?;
103        let ch = Self::into_char(&self.next_ok()?);
104        if ch == expected_ch {
105            Ok(())
106        } else {
107            Err(anyhow!("Expected '{}', found '{}'.", expected_ch, ch))
108        }
109    }
110
111    /// Parse "iNNN"
112    fn parse_integer(&mut self) -> Result<LLSDValue, Error> {
113        let mut s = String::with_capacity(20);  // pre-allocate; can still grow
114        //  Accumulate numeric chars.
115        while let Some(ch) = self.peek() {
116            match Self::into_char(ch) {
117                '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'+'|'-' => s.push(Self::into_char(&self.next().unwrap())),
118                 _ => break
119            }
120        }
121        //  Digits accmulated, use standard conversion
122        Ok(LLSDValue::Integer(s.parse::<i32>()?))
123    }
124        /// Parse "rNNN".
125    //  Does "notation" allow exponents?
126    fn parse_real(&mut self) -> Result<LLSDValue, Error> {
127        let mut s = String::with_capacity(20);  // pre-allocate; can still grow
128        //  Accumulate numeric chars.
129        //  This will not accept NaN.
130        while let Some(ch) = self.peek() {
131            match Self::into_char(ch) {
132                '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'+'|'-'|'.' => s.push(Self::into_char(&self.next().unwrap())),
133                 _ => break
134            }
135        }
136        //  Digits accmulated, use standard conversion
137        Ok(LLSDValue::Real(s.parse::<f64>()?))
138    }
139    
140    /// Parse Boolean
141    fn parse_boolean(&mut self, first_char: char) -> Result<LLSDValue, Error> {
142        //  Accumulate next word
143        let mut s = String::with_capacity(4);
144        s.push(first_char);     // we already had the first character.        
145        loop {              
146            if let Some(ch) = self.peek() {
147                if Self::into_char(ch).is_alphabetic() {
148                    s.push(Self::into_char(&self.next().unwrap()));
149                    continue
150                }
151            }
152            break;
153        }
154        //  Check for all the allowed Boolean forms.
155        match s.as_str() {
156            "f" | "F" | "false" | "FALSE" => Ok(LLSDValue::Boolean(false)),
157            "t" | "T" | "true" | "TRUE" => Ok(LLSDValue::Boolean(true)),
158            _ => Err(anyhow!("Parsing Boolean, got {}", s)) 
159        }
160    }
161    /// Parse string. "ABC" or 'ABC', with '\' as escape.
162    /// Allowed escapes are \\, \", \', and \n
163    /// Does not parse the numeric count prefix form.
164    fn parse_quoted_string(&mut self, delim: char) -> Result<String, Error> {
165        self.consume_whitespace()?;
166        let mut s = String::with_capacity(128);             // allocate reasonably large size for typical string.
167        loop {
168            let ch = Self::into_char(&self.next_ok()?);     // next char, must be present
169            if ch == delim { break }                        // end of string
170            if ch == '\\' {                                 // escape
171                let ch = Self::into_char(&self.next_ok()?); // next char, must be present
172                match ch {
173                    '\\' | '\'' | '\"' => s.push(ch),       // escapable characters
174                    'n' => s.push('\n'),                    // backslash n becomes newline
175                    _ => { return Err(anyhow!("Unexpected escape sequence \"\\{}\" within quoted string.", ch)); }
176                }
177            } else {
178                s.push(ch)
179            }
180        }
181        String::shrink_to_fit(&mut s);                      // release wasted space
182        Ok(s)
183    }   
184    /// Parse date string per RFC 1339.
185    fn parse_date(&mut self) -> Result<LLSDValue, Error> {
186        if let Some(delim) = self.next() {
187            if Self::into_char(&delim) == '"' || Self::into_char(&delim) == '\'' {
188                let s = self.parse_quoted_string(Self::into_char(&delim))?;
189                let naive_date =  DateTime::parse_from_rfc3339(&s)?; // parse date per RFC 3339.
190                Ok(LLSDValue::Date(naive_date.timestamp())) // seconds since UNIX epoch.
191            } else {
192                Err(anyhow!("URI did not begin with '\"'"))
193            }
194        } else {
195            Err(anyhow!("URI at end of file."))
196        }
197    }
198    
199    /// Parse URI string per rfc 1738
200    fn parse_uri(&mut self) -> Result<LLSDValue, Error> {
201        if let Some(delim) = self.next() {
202            if Self::into_char(&delim) == '"' || Self::into_char(&delim) == '\'' {
203                let s = self.parse_quoted_string(Self::into_char(&delim))?;
204                Ok(LLSDValue::URI(urlencoding::decode(&s)?.to_string()))
205            } else {
206                Err(anyhow!("URI did not begin with '\"'"))
207            }
208        } else {
209            Err(anyhow!("URI at end of file."))
210        }
211    }    
212    /// Parse UUID. No quotes
213    fn parse_uuid(&mut self) -> Result<LLSDValue, Error> {
214        const UUID_LEN: usize = "c69b29b1-8944-58ae-a7c5-2ca7b23e22fb".len();   // just to get the length of a standard format UUID.
215        let mut s = String::with_capacity(UUID_LEN);
216        for _ in 0..UUID_LEN {
217            s.push(Self::into_char(&(self.next().ok_or(anyhow!("EOF parsing UUID"))?)));
218        }
219        Ok(LLSDValue::UUID(Uuid::parse_str(&s)?))
220    }
221
222    /// Parse "{ 'key' : value, 'key' : value ... }
223    fn parse_map(&mut self) -> Result<LLSDValue, Error> {
224        let mut kvmap = HashMap::new();                         // building map
225        loop {
226            self.consume_whitespace()?;
227            let key =  {
228                let ch = Self::into_char(&self.next_ok()?);
229                match ch {
230                    '}' => { break } // end of map, may be empty.
231                    '\'' | '"' => self.parse_quoted_string(ch)?, 
232                    _ => { return Err(anyhow!("Map key began with {} instead of quote.", ch)); }
233                }
234            };
235            self.consume_char(':')?;
236            let value = self.parse_value()?;           // value of key:value
237            kvmap.insert(key, value);
238            //  Check for comma indicating more items.
239            self.consume_whitespace()?;
240            if Self::into_char(self.peek_ok()?) == ',' {
241                let _ = self.next();    // consume comma, continue with next field
242            }
243        }
244        Ok(LLSDValue::Map(kvmap))
245    }
246        
247    /// Parse "[ value, value ... ]"
248    /// At this point, the '[' has been consumed.
249    /// At successful return, the ending ']' has been consumed.
250    fn parse_array(&mut self) -> Result<LLSDValue, Error> {
251        let mut array_items = Vec::new();
252        //  Accumulate array elements.
253        loop {
254            //  Check for end of items
255            self.consume_whitespace()?;
256            let ch = Self::into_char(self.peek_ok()?);
257            if ch == ']' {
258                let _ = self.next(); break;    // end of array, may be empty.
259            }
260            array_items.push(self.parse_value()?);          // parse next value
261            //  Check for comma indicating more items.
262            self.consume_whitespace()?;
263            if Self::into_char(self.peek_ok()?) == ',' {
264                let _ = self.next();    // consume comma, continue with next field
265            }           
266        }
267        Ok(LLSDValue::Array(array_items))               // return array
268    }
269    
270    fn parse_binary(&mut self) -> Result<LLSDValue, Error>; // passed down to next level
271    
272    fn parse_sized_string(&mut self) -> Result<LLSDValue, Error>; // passed down to next level
273        
274    
275    /// Parse one value - real, integer, map, etc. Recursive.
276    /// This is the top level of the parser
277    fn parse_value(&mut self) -> Result<LLSDValue, Error> {
278        self.consume_whitespace()?;                      // ignore leading white space
279        let ch = Self::into_char(&self.next_ok()?);
280        match ch {
281            '!' => { Ok(LLSDValue::Undefined) }         // "Undefined" as a value
282            '0' => { Ok(LLSDValue::Boolean(false)) }    // false
283            '1' => { Ok(LLSDValue::Boolean(true)) }     // true
284            'f' | 'F' => { self.parse_boolean(ch) }     // false, all alpha forms
285            't' | 'T' => { self.parse_boolean(ch) }     // true, all alpha forms
286            '{' => { self.parse_map() }                 // map
287            '[' => { self.parse_array() }               // array
288            'i' => { self.parse_integer() }             // integer
289            'r' => { self.parse_real() }                // real
290            'd' => { self.parse_date() }                // date
291            'u' => { self.parse_uuid() }                // UUID
292            'l' => { self.parse_uri() }                 // URI
293            'b' => { self.parse_binary() }              // binary
294            's' => { self.parse_sized_string() }        // string with explicit size
295            '"' => { Ok(LLSDValue::String(self.parse_quoted_string(ch)?)) }  // string, double quoted
296            '\'' => { Ok(LLSDValue::String(self.parse_quoted_string(ch)?)) }  // string, double quoted
297            //  ***MORE*** add cases for UUID, URL, date, and binary.
298            _ => { Err(anyhow!("Unexpected character: {:?}", ch)) } // error
299        }
300    }
301}
302
303/// Stream, composed of UTF-8 chars.
304struct LLSDStreamChars<'a> {
305    /// Stream is composed of peekable UTF-8 chars
306    cursor: Peekable<Chars<'a>>,
307}
308
309impl LLSDStream<char, Peekable<Chars<'_>>> for LLSDStreamChars<'_> {
310    /// Get next UTF-8 char.
311    fn next(&mut self) -> Option<char> {
312        self.cursor.next()
313    }
314    /// Peek at next UTF-8 char.
315    fn peek(&mut self) -> Option<&char> {
316        self.cursor.peek()
317    }
318    /// Into char, which is a null conversion
319    fn into_char(ch: &char) -> char {
320        *ch
321    }  
322    
323    /// Won't work.
324    fn parse_binary(&mut self) -> Result<LLSDValue, Error> {
325        Err(anyhow!("Byte-counted binary data inside UTF-8 won't work."))
326    }
327    
328    /// Won't work.
329    fn parse_sized_string(&mut self) -> Result<LLSDValue, Error> {
330        Err(anyhow!("Byte-counted string data inside UTF-8 won't work."))
331    }
332}
333
334impl LLSDStreamChars<'_> {
335    /// Parse LLSD string expressed in notation format into an LLSDObject tree. No header.
336    /// Strng form
337    pub fn parse(notation_str: &str) -> Result<LLSDValue, Error> {
338        let mut stream = LLSDStreamChars { cursor: notation_str.chars().peekable() };
339        match stream.parse_value() {
340            Ok(v) => Ok(v),
341            Err(e) => {
342                //  Useful error message
343                let s = beginning_to_iterator(notation_str, &stream.cursor);
344                Err(anyhow!("LLSD notation string parse error: {:?}. Parse got this far: {}", e, s))
345            }
346        }
347    }
348}
349
350/// Stream, composed of raw bytes.
351struct LLSDStreamBytes<'a> {
352    /// Stream is composed of peekable bytes.
353    cursor: Peekable<std::slice::Iter<'a, u8>>,
354}
355
356impl LLSDStream<u8, Peekable<Bytes<'_>>> for LLSDStreamBytes<'_> {
357    /// Get next byte.
358    fn next(&mut self) -> Option<u8> {
359        self.cursor.next().copied()
360    }
361    /// Peek at next byte.
362    fn peek(&mut self) -> Option<&u8> {
363        self.cursor.peek().copied()
364    }
365    /// Into char, which is a real conversion to a UTF-8 char.
366    fn into_char(ch: &u8) -> char {
367        (*ch).into()
368    }
369    
370    /// Parse binary value.
371    /// Format is b16"value" or b64"value" or b(cnt)"value".
372    /// Putting text in this format is just wrong, yet the LL example does it.
373    /// This conversion may fail for non-ASCII input.
374    //
375    //  The LL parser for this is at
376    //  https://github.com/secondlife/viewer/blob/ec4135da63a3f3877222fba4ecb59b15650371fe/indra/llcommon/llsdserialize.cpp#L789
377    //  That reads N bytes from the input as a byte stream. We only do this for byte streams, not Strings.
378    //
379    fn parse_binary(&mut self) -> Result<LLSDValue, Error> {
380        if let Some(ch) = self.peek() {
381            match Self::into_char(ch) {
382                '(' => {
383                    let cnt = self.parse_number_in_parentheses()?;
384                    self.consume_char('"')?;
385                    let s = self.next_chunk(cnt)?;
386                    self.consume_char('"')?;     // count must be correct or this will fail.
387                    Ok(LLSDValue::Binary(s))     // not sure about this
388                }                 
389                '1' => {
390                    self.consume_char('1')?;
391                    self.consume_char('6')?;          // base 16
392                    self.consume_char('"')?;          // begin quote
393                    let mut s = self.parse_quoted_string('"')?;
394                    s.retain(|c| !c.is_whitespace());
395                    Ok(LLSDValue::Binary(hex::decode(s)?))
396                }
397                '6' => {
398                    self.consume_char('6')?;
399                    self.consume_char('4')?;
400                    self.consume_char('"')?;          // begin quote
401                    let mut s = self.parse_quoted_string('"')?;
402                    s.retain(|c| !c.is_whitespace());
403                    println!("Base 64 decode input: \"{}\"", s);    // ***TEMP***
404                    let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
405                    Ok(LLSDValue::Binary(bytes))
406                }
407                _ => Err(anyhow!("Binary value started with {} instead of (, 1, or 6", ch))   
408            } 
409        } else {
410            Err(anyhow!("Binary value started with EOF"))   
411        }
412    }
413    
414    /// Parse sized string.
415    /// Format is s(NNN)"string"
416    fn parse_sized_string(&mut self) -> Result<LLSDValue, Error> {
417        let cnt = self.parse_number_in_parentheses()?;
418        //  At this point, we are supposed to have a quoted string of ASCII characters.
419        //  If this can be validy converted as UTF-8, it will be accepted.
420        self.consume_char('"')?;
421        let s = self.next_chunk(cnt)?;
422        self.consume_char('"')?;
423        Ok(LLSDValue::String(String::from_utf8(s)?))
424    }
425}
426
427impl LLSDStreamBytes<'_> {
428    /// Parse LLSD string expressed in notation format into an LLSDObject tree. No header.
429    /// Bytes form.
430    pub fn parse(notation_bytes: &[u8]) -> Result<LLSDValue, Error> {
431        let mut stream = LLSDStreamBytes { cursor: notation_bytes.iter().peekable() };
432        stream.parse_value()
433    }
434
435    /// Parse (NNN), which is used for length information.
436    fn parse_number_in_parentheses(&mut self) -> Result<usize, Error> {
437        self.consume_char('(')?;
438        let val = self.parse_integer()?;
439        self.consume_char(')')?;   
440        if let LLSDValue::Integer(v) = val {
441            Ok(v as usize)
442        } else {
443            panic!("Integer parse did not return an integer.");
444        }
445    }
446    
447    /// Read chunk of N bytes.
448    fn next_chunk(&mut self, cnt: usize) -> Result<Vec<u8>, Error> {
449        let mut s = Vec::with_capacity(cnt);
450        //  next_chunk, for getting N chars, doesn't work yet.
451        for _ in 0..cnt {
452            s.push(self.next_ok()?);
453        }
454        Ok(s)
455    }
456
457}
458
459//  Utility functions
460/// Extract the part of a string from the beginning to an iterator.
461fn beginning_to_iterator<'a>(orig: &'a str, pos: &Peekable<Chars>) -> &'a str {
462    let suffix: String = pos.clone().collect();
463    println!("Suffix: {}", suffix);
464    if let Some(s) = orig.strip_suffix(&suffix) {
465        s
466    } else {
467        orig
468    }
469}
470
471#[test]
472/// Unit tests
473fn notationparse1() {
474    let s1 = "\"ABC☺DEF\"".to_string();  // string, including quotes, with emoji.
475    let mut stream1 = LLSDStreamChars { cursor: s1.chars().peekable() };
476    stream1.consume_char('"').unwrap(); // leading quote
477    let v1 = stream1.parse_quoted_string('"').unwrap();
478    assert_eq!(v1, "ABC☺DEF");
479}
480
481#[test]
482fn notationparse2() {
483    //  Linden Lab documented test data from wiki. Compatibility test use only.
484    const TESTNOTATION2: &str = r#"
485[
486  {'destination':l"http://secondlife.com"}, 
487  {'version':i1}, 
488  {
489    'agent_id':u3c115e51-04f4-523c-9fa6-98aff1034730, 
490    'session_id':u2c585cec-038c-40b0-b42e-a25ebab4d132, 
491    'circuit_code':i1075, 
492    'first_name':'Phoenix', 
493    'last_name':'Linden',
494    'position':[r70.9247,r254.378,r38.7304], 
495    'look_at':[r-0.043753,r-0.999042,r0], 
496    'granters':[ua2e76fcd-9360-4f6d-a924-000000000003],
497    'attachment_data':
498    [
499      {
500        'attachment_point':i2,
501        'item_id':ud6852c11-a74e-309a-0462-50533f1ef9b3,
502        'asset_id':uc69b29b1-8944-58ae-a7c5-2ca7b23e22fb
503      },
504      {
505        'attachment_point':i10, 
506        'item_id':uff852c22-a74e-309a-0462-50533f1ef900,
507        'asset_id':u5868dd20-c25a-47bd-8b4c-dedc99ef9479
508      }
509    ]
510  }
511]
512"#;
513    let parsed_s = LLSDStreamChars::parse(TESTNOTATION2);
514    println!("Parse of string form {}: \n{:#?}", TESTNOTATION2, parsed_s);
515    let parsed_b = LLSDStreamBytes::parse(TESTNOTATION2.as_bytes());
516    println!("Parse of byte form: {:#?}", parsed_b);
517    assert_eq!(parsed_s.unwrap(), parsed_b.unwrap());
518}
519
520#[test]
521fn notationparse3() {
522    //  Linden Lab documented test data from wiki. Compatibility test use only.
523    const TESTNOTATION3: &str = r#"
524[
525  {
526    'creation-date':d"2007-03-15T18:30:18Z", 
527    'creator-id':u3c115e51-04f4-523c-9fa6-98aff1034730
528  },
529  s(10)"0123456789",
530  "Where's the beef?",
531  'Over here.',  
532  b(158)"default
533{
534    state_entry()
535    {
536        llSay(0, "Hello, Avatar!");
537    }
538
539    touch_start(integer total_number)
540    {
541        llSay(0, "Touched.");
542    }
543}",
544  b64"AABAAAAAAAAAAAIAAAA//wAAP/8AAADgAAAA5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
545AABkAAAAZAAAAAAAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAABQAAAAEAAAAQAAAAAAAA
546AAUAAAAFAAAAABAAAAAAAAAAPgAAAAQAAAAFAGNbXgAAAABgSGVsbG8sIEF2YXRhciEAZgAAAABc
547XgAAAAhwEQjRABeVAAAABQBjW14AAAAAYFRvdWNoZWQuAGYAAAAAXF4AAAAIcBEI0QAXAZUAAEAA
548AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 
549]
550"#;
551    let parsed_b = from_bytes(TESTNOTATION3.as_bytes()).unwrap();
552    println!("Parse of byte form: {:#?}", parsed_b);
553    let parsed_b = from_str(TESTNOTATION3);
554    assert!(parsed_b.is_err());             // not allowed to have b(158) or s(10) in string mode.
555    println!("Parse of string form: {:#?}", parsed_b);
556}
557
558#[test]
559fn notationparse4() {
560    //  This is a "material override".
561    const TESTNOTATION4: &str = r#"
562        {'gltf_json':['{\"asset\":{\"version\":\"2.0\"},\"images\":[{\"uri\":\"5748decc-f629-461c-9a36-a35a221fe21f\"},
563            {\"uri\":\"5748decc-f629-461c-9a36-a35a221fe21f\"}],\"materials\":[{\"occlusionTexture\":{\"index\":1},\"pbrMetallicRoughness\":{\"metallicRoughnessTexture\":{\"index\":0},\"roughnessFactor\":0.20000000298023224}}],\"textures\":[{\"source\":0},
564            {\"source\":1}]}\\n'],'local_id':i8893800,'object_id':u6ac43d70-80eb-e526-ec91-110b4116293e,'region_handle_x':i342016,'region_handle_y':i343552,'sides':[i0]}"
565"#;
566    let parsed_b = from_bytes(TESTNOTATION4.as_bytes());
567    println!("Parse of byte form: {:#?}", parsed_b);
568    let local_id = *parsed_b.unwrap().as_map().unwrap().get("local_id").unwrap().as_integer().unwrap();
569    assert_eq!(local_id, 8893800); // validate local ID
570    let parsed_b = from_str(TESTNOTATION4);
571    println!("Parse of str form: {:#?}", parsed_b);
572    let local_id = *parsed_b.unwrap().as_map().unwrap().get("local_id").unwrap().as_integer().unwrap();
573    assert_eq!(local_id, 8893800); // validate local ID
574}
575
576#[test]
577fn notationparse5() {
578    //  Parse with error. Note unwanted backslash in "images".
579    const TESTNOTATION5: &str = r#"
580        {'gltf_json':['{\"asset\":{\"version\":\"2.0\"},\"im\ages\":[{\"uri\":\"5748decc-f629-461c-9a36-a35a221fe21f\"},
581            {\"uri\":\"5748decc-f629-461c-9a36-a35a221fe21f\"}],\"materials\":[{\"occlusionTexture\":{\"index\":1},\"pbrMetallicRoughness\":{\"metallicRoughnessTexture\":{\"index\":0},\"roughnessFactor\":0.20000000298023224}}],\"textures\":[{\"source\":0},
582            {\"source\":1}]}\\n'],'local_id':i8893800,'object_id':u6ac43d70-80eb-e526-ec91-110b4116293e,'region_handle_x':i342016,'region_handle_y':i343552,'sides':[i0]}"
583"#;
584    println!("Test notationparse5");
585    let parsed_b = from_str(TESTNOTATION5);
586    println!("Parse of byte form: {:#?}", parsed_b);
587    assert!(parsed_b.is_err());
588}