objects/
folder_listing.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use bitflags::bitflags;
6use chrono::naive::NaiveDateTime;
7use std::collections::HashSet;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10use std::str::FromStr;
11use xml::attribute::OwnedAttribute;
12use xml::name::OwnedName;
13use xml::reader::{ParserConfig, XmlEvent};
14use xml::writer::{EmitterConfig, XmlEvent as XmlWriteEvent};
15use xml::EventWriter;
16
17use crate::error::Error;
18use crate::{Builder, Parser};
19
20/// Element names
21const FILE_ELEM: &str = "file";
22const FOLDER_ELEM: &str = "folder";
23const PARENT_FOLDER_ELEM: &str = "parent-folder";
24const FOLDER_LISTING_ELEM: &str = "folder-listing";
25
26const VERSION_ATTR: &str = "version";
27const NAME_ATTR: &str = "name";
28const SIZE_ATTR: &str = "size";
29const MODIFIED_ATTR: &str = "modified";
30const CREATED_ATTR: &str = "created";
31const ACCESSED_ATTR: &str = "accessed";
32const USER_PERM_ATTR: &str = "user-perm";
33const GROUP_PERM_ATTR: &str = "group-perm";
34const OTHER_PERM_ATTR: &str = "other-perm";
35const OWNER_ATTR: &str = "owner";
36const GROUP_ATTR: &str = "group";
37const TYPE_ATTR: &str = "type";
38const XML_LANG_ATTR: &str = "xml:lang";
39
40bitflags! {
41    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
42    pub struct Permission : u8 {
43        const READ = 0x1;
44        const WRITE = 0x2;
45        const DELETE = 0x4;
46    }
47}
48
49impl fmt::Display for Permission {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        if self.contains(Self::READ) {
52            write!(f, "R")?;
53        }
54        if self.contains(Self::WRITE) {
55            write!(f, "W")?;
56        }
57        if self.contains(Self::DELETE) {
58            write!(f, "D")?;
59        }
60        Ok(())
61    }
62}
63
64impl FromStr for Permission {
65    type Err = Error;
66    fn from_str(s: &str) -> Result<Self, Self::Err> {
67        let mut perm = Permission::empty();
68        s.chars().try_for_each(|c| {
69            match c {
70                'R' => perm = perm | Permission::READ,
71                'W' => perm = perm | Permission::WRITE,
72                'D' => perm = perm | Permission::DELETE,
73                _ => return Err(Error::InvalidData(s.to_string())),
74            };
75            Ok(())
76        })?;
77        Ok(perm)
78    }
79}
80
81// Naive date time object with format string to use for formatting.
82#[derive(Clone, Debug, PartialEq)]
83pub struct FormattedDateTimeObj(NaiveDateTime, String);
84
85impl FormattedDateTimeObj {
86    /// The ISO 8601 time format used in the Time Header packet.
87    /// The format is YYYYMMDDTHHMMSS where "T" delimits the date from the time. It is assumed that
88    /// the time is the local time, but per OBEX 2.2.5, a suffix of "Z" can be included to indicate
89    /// UTC time.
90    const ISO_8601_UTC_TIME_FORMAT: &'static str = "%Y%m%dT%H%M%SZ";
91    const ISO_8601_TIME_FORMAT: &'static str = "%Y%m%dT%H%M%S";
92
93    fn new(dt: NaiveDateTime) -> Self {
94        Self(dt, Self::ISO_8601_TIME_FORMAT.to_string())
95    }
96    fn new_utc(dt: NaiveDateTime) -> Self {
97        Self(dt, Self::ISO_8601_UTC_TIME_FORMAT.to_string())
98    }
99
100    fn parse_datetime(dt: String) -> Result<Self, Error> {
101        let Ok(datetime) =
102            NaiveDateTime::parse_from_str(dt.as_str(), Self::ISO_8601_UTC_TIME_FORMAT)
103        else {
104            return NaiveDateTime::parse_from_str(dt.as_str(), Self::ISO_8601_TIME_FORMAT)
105                .map(|t| Self::new(t))
106                .map_err(|_| Error::InvalidData(dt));
107        };
108
109        // UTC timestamp.
110        Ok(Self::new_utc(datetime))
111    }
112}
113
114impl fmt::Display for FormattedDateTimeObj {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}", self.0.format(self.1.as_str()))
117    }
118}
119
120#[derive(Clone, Debug)]
121pub enum FolderListingAttribute {
122    Name(String),
123    Size(String),
124    Modified(FormattedDateTimeObj),
125    Created(FormattedDateTimeObj),
126    Accessed(FormattedDateTimeObj),
127    UserPerm(Permission),
128    GroupPerm(Permission),
129    OtherPerm(Permission),
130    Owner(String),
131    Group(String),
132    XmlLang(String),
133    Type(String),
134}
135
136impl Hash for FolderListingAttribute {
137    fn hash<H: Hasher>(&self, state: &mut H) {
138        self.xml_attribute_name().hash(state);
139    }
140}
141
142impl PartialEq for FolderListingAttribute {
143    fn eq(&self, other: &FolderListingAttribute) -> bool {
144        self.xml_attribute_name() == other.xml_attribute_name()
145    }
146}
147
148impl Eq for FolderListingAttribute {}
149
150pub type Attributes = HashSet<FolderListingAttribute>;
151
152impl FolderListingAttribute {
153    fn try_from_xml_attribute(a: &OwnedAttribute) -> Result<Self, Error> {
154        let v = a.value.clone();
155        // For now ignore all attributes that aren't recognized.
156        Ok(match a.name.local_name.as_str() {
157            NAME_ATTR => Self::Name(v),
158            SIZE_ATTR => Self::Size(v),
159            MODIFIED_ATTR => Self::Modified(FormattedDateTimeObj::parse_datetime(v)?),
160            CREATED_ATTR => Self::Created(FormattedDateTimeObj::parse_datetime(v)?),
161            ACCESSED_ATTR => Self::Accessed(FormattedDateTimeObj::parse_datetime(v)?),
162            USER_PERM_ATTR => Self::UserPerm(str::parse(v.as_str())?),
163            GROUP_PERM_ATTR => Self::GroupPerm(str::parse(v.as_str())?),
164            OTHER_PERM_ATTR => Self::OtherPerm(str::parse(v.as_str())?),
165            OWNER_ATTR => Self::Owner(v),
166            GROUP_ATTR => Self::Group(v),
167            XML_LANG_ATTR => Self::XmlLang(v),
168            TYPE_ATTR => Self::Type(v),
169            _ => return Err(Error::InvalidData(format!("{a:?}"))),
170        })
171    }
172
173    const fn xml_attribute_name(&self) -> &'static str {
174        match self {
175            Self::Name(_) => NAME_ATTR,
176            Self::Size(_) => SIZE_ATTR,
177            Self::Modified(_) => MODIFIED_ATTR,
178            Self::Created(_) => CREATED_ATTR,
179            Self::Accessed(_) => ACCESSED_ATTR,
180            Self::UserPerm(_) => USER_PERM_ATTR,
181            Self::GroupPerm(_) => GROUP_PERM_ATTR,
182            Self::OtherPerm(_) => OTHER_PERM_ATTR,
183            Self::Owner(_) => OWNER_ATTR,
184            Self::Group(_) => GROUP_ATTR,
185            Self::XmlLang(_) => XML_LANG_ATTR,
186            Self::Type(_) => TYPE_ATTR,
187        }
188    }
189
190    fn xml_attribute_value(&self) -> String {
191        match self {
192            Self::Name(v)
193            | Self::Size(v)
194            | Self::Owner(v)
195            | Self::Group(v)
196            | Self::XmlLang(v)
197            | Self::Type(v) => v.clone(),
198            Self::Modified(dt) | Self::Created(dt) | Self::Accessed(dt) => dt.to_string(),
199            Self::UserPerm(p) | Self::GroupPerm(p) | Self::OtherPerm(p) => p.to_string(),
200        }
201    }
202}
203
204/// See OBEX v1.4 section 9.1.1.2.2 for attributes for File elements.
205#[derive(Clone, Debug, PartialEq)]
206pub struct File {
207    data: Option<String>,
208    attributes: Attributes,
209}
210
211impl File {
212    fn write<W: std::io::prelude::Write>(&self, writer: &mut EventWriter<W>) -> Result<(), Error> {
213        // Build the file XML element.
214        let mut builder = XmlWriteEvent::start_element(FILE_ELEM);
215        let attrs: Vec<(&str, String)> = self
216            .attributes
217            .iter()
218            .map(|a| (a.xml_attribute_name(), a.xml_attribute_value()))
219            .collect();
220        for a in &attrs {
221            builder = builder.attr(a.0, &a.1);
222        }
223        writer.write(builder)?;
224
225        if let Some(data) = &self.data {
226            writer.write(data.as_str())?;
227        }
228
229        // Write the end element.
230        writer.write(XmlWriteEvent::end_element()).map_err(Into::into)
231    }
232
233    fn validate(&self) -> Result<(), Error> {
234        // Name is a required field.
235        if !self.attributes.contains(&FolderListingAttribute::Name("".to_string())) {
236            return Err(Error::MissingData(NAME_ATTR.to_string()));
237        }
238        Ok(())
239    }
240}
241
242impl TryFrom<XmlEvent> for File {
243    type Error = Error;
244    fn try_from(src: XmlEvent) -> Result<Self, Error> {
245        let XmlEvent::StartElement { ref name, ref attributes, .. } = src else {
246            return Err(Error::InvalidData(format!("{:?}", src)));
247        };
248        if name.local_name.as_str() != FILE_ELEM {
249            return Err(Error::InvalidData(name.local_name.clone()));
250        }
251        let mut attrs = HashSet::new();
252        for a in attributes {
253            if !attrs.insert(FolderListingAttribute::try_from_xml_attribute(a)?) {
254                return Err(Error::InvalidData(format!("duplicate \"{}\"", a.name.local_name)));
255            }
256        }
257
258        let file = File { data: None, attributes: attrs };
259        file.validate()?;
260        Ok(file)
261    }
262}
263
264/// See OBEX v1.4 section 9.1.1.2.2 for attributes for Folder elements.
265#[derive(Clone, Debug, PartialEq)]
266pub struct Folder {
267    data: Option<String>,
268    attributes: Attributes,
269}
270
271impl Folder {
272    fn write<W: std::io::prelude::Write>(&self, writer: &mut EventWriter<W>) -> Result<(), Error> {
273        // Build the file XML element.
274        let mut builder = XmlWriteEvent::start_element(FOLDER_ELEM);
275        let attrs: Vec<(&str, String)> = self
276            .attributes
277            .iter()
278            .map(|a| (a.xml_attribute_name(), a.xml_attribute_value()))
279            .collect();
280        for a in &attrs {
281            builder = builder.attr(a.0, a.1.as_str());
282        }
283        writer.write(builder)?;
284
285        if let Some(data) = &self.data {
286            writer.write(data.as_str())?;
287        }
288
289        // Write the end element.
290        Ok(writer.write(XmlWriteEvent::end_element())?)
291    }
292
293    fn validate(&self) -> Result<(), Error> {
294        // Name is a required field.
295        if !self.attributes.contains(&FolderListingAttribute::Name("".to_string())) {
296            return Err(Error::MissingData(NAME_ATTR.to_string()));
297        }
298        // Type field only applies to File elements.
299        if self.attributes.contains(&FolderListingAttribute::Type("".to_string())) {
300            return Err(Error::InvalidData(TYPE_ATTR.to_string()));
301        }
302        Ok(())
303    }
304}
305
306impl TryFrom<XmlEvent> for Folder {
307    type Error = Error;
308    fn try_from(src: XmlEvent) -> Result<Self, Error> {
309        let XmlEvent::StartElement { ref name, ref attributes, .. } = src else {
310            return Err(Error::InvalidData(format!("{:?}", src)));
311        };
312        if name.local_name.as_str() != FOLDER_ELEM {
313            return Err(Error::InvalidData(name.local_name.clone()));
314        }
315        let mut attrs = HashSet::new();
316        attributes.iter().try_for_each(|a| {
317            if !attrs.insert(FolderListingAttribute::try_from_xml_attribute(a)?) {
318                return Err(Error::InvalidData(format!("duplicate \"{}\"", a.name.local_name)));
319            }
320            Ok(())
321        })?;
322
323        let folder = Folder { data: None, attributes: attrs };
324        folder.validate()?;
325        Ok(folder)
326    }
327}
328
329enum ParsedXmlEvent {
330    DocumentStart,
331    FolderListingElement,
332    ParentFolderElement,
333    FolderElement(Folder),
334    FileElement(File),
335}
336
337/// FolderListing struct represents the list of the objects in the current folder.
338/// See OBEX v1.4 section 9.1.4 for XML Document Definition for OBEX folder listing.
339/// Version field is not stated explicitly since we only support version 1.0.
340#[derive(Clone, Debug, PartialEq)]
341pub struct FolderListing {
342    // Whether or current folder has a parent folder.
343    parent_folder: bool,
344    files: Vec<File>,
345    folders: Vec<Folder>,
346}
347
348impl FolderListing {
349    const DEFAULT_VERSION: &'static str = "1.0";
350
351    fn new_empty() -> Self {
352        Self { parent_folder: false, files: Vec::new(), folders: Vec::new() }
353    }
354
355    // Given the XML StartElement, checks whether or not it is a valid folder
356    // listing element.
357    fn validate_folder_listing_element(element: XmlEvent) -> Result<(), Error> {
358        let XmlEvent::StartElement { ref name, ref attributes, .. } = element else {
359            return Err(Error::InvalidData(format!("{:?}", element)));
360        };
361
362        if name.local_name != FOLDER_LISTING_ELEM {
363            return Err(Error::InvalidData(name.local_name.clone()));
364        }
365        let default_version: OwnedAttribute = OwnedAttribute::new(
366            OwnedName { local_name: VERSION_ATTR.to_string(), namespace: None, prefix: None },
367            FolderListing::DEFAULT_VERSION,
368        );
369        // If the version attribute was missing, assume 1.0.
370        let version = &attributes
371            .iter()
372            .find(|a| a.name.local_name == VERSION_ATTR)
373            .unwrap_or(&default_version)
374            .value;
375        if version != FolderListing::DEFAULT_VERSION {
376            return Err(Error::UnsupportedVersion);
377        }
378        Ok(())
379    }
380}
381
382impl Parser for FolderListing {
383    type Error = Error;
384
385    /// Parses FolderListing from raw bytes of XML data.
386    fn parse<R: std::io::prelude::Read>(buf: R) -> Result<Self, Self::Error> {
387        let mut reader = ParserConfig::new()
388            .ignore_comments(true)
389            .whitespace_to_characters(true)
390            .cdata_to_characters(true)
391            .trim_whitespace(true)
392            .create_reader(buf);
393        let mut prev = Vec::new();
394
395        // Process start of document.
396        match reader.next() {
397            Ok(XmlEvent::StartDocument { .. }) => {
398                prev.push(ParsedXmlEvent::DocumentStart);
399            }
400            Ok(element) => return Err(Error::InvalidData(format!("{:?}", element))),
401            Err(e) => return Err(Error::ReadXml(e)),
402        };
403
404        // Process start of folder listing element.
405        let xml_event = reader.next()?;
406        let _ = Self::validate_folder_listing_element(xml_event)?;
407
408        prev.push(ParsedXmlEvent::FolderListingElement);
409        let mut folder_listing = Self::new_empty();
410
411        // Process remaining elements elements.
412        let mut finished_document = false;
413        let mut finished_folder_listing = false;
414        while !finished_document {
415            // Could be either end of folder listing element,
416            let e = reader.next()?;
417            let invalid_elem_err = Err(Error::InvalidData(format!("{:?}", e)));
418            match e {
419                XmlEvent::StartElement { ref name, .. } => {
420                    match name.local_name.as_str() {
421                        PARENT_FOLDER_ELEM => prev.push(ParsedXmlEvent::ParentFolderElement),
422                        FOLDER_ELEM => prev.push(ParsedXmlEvent::FolderElement(e.try_into()?)),
423                        FILE_ELEM => prev.push(ParsedXmlEvent::FileElement(e.try_into()?)),
424                        other_value => return Err(Error::InvalidData(other_value.to_string())),
425                    };
426                }
427                XmlEvent::EndElement { ref name } => {
428                    let Some(parsed_elem) = prev.pop() else {
429                        return invalid_elem_err;
430                    };
431                    match name.local_name.as_str() {
432                        FOLDER_LISTING_ELEM => {
433                            let ParsedXmlEvent::FolderListingElement = parsed_elem else {
434                                return Err(Error::MissingData(format!(
435                                    "closing {FOLDER_LISTING_ELEM}"
436                                )));
437                            };
438                            finished_folder_listing = true;
439                        }
440                        PARENT_FOLDER_ELEM => {
441                            let ParsedXmlEvent::ParentFolderElement = parsed_elem else {
442                                return Err(Error::MissingData(format!(
443                                    "closing {PARENT_FOLDER_ELEM}"
444                                )));
445                            };
446                            folder_listing.parent_folder = true;
447                        }
448                        FOLDER_ELEM => {
449                            let ParsedXmlEvent::FolderElement(f) = parsed_elem else {
450                                return Err(Error::MissingData(format!("closing {FOLDER_ELEM}")));
451                            };
452                            folder_listing.folders.push(f);
453                        }
454                        FILE_ELEM => {
455                            let ParsedXmlEvent::FileElement(f) = parsed_elem else {
456                                return Err(Error::MissingData(format!("closing {FILE_ELEM}")));
457                            };
458                            folder_listing.files.push(f);
459                        }
460                        _ => return invalid_elem_err,
461                    };
462                }
463                XmlEvent::Characters(data) => {
464                    let err = Err(Error::InvalidData(data.clone()));
465                    if let Some(mut event) = prev.pop() {
466                        match &mut event {
467                            ParsedXmlEvent::FolderElement(ref mut f) => f.data = Some(data),
468                            ParsedXmlEvent::FileElement(ref mut f) => f.data = Some(data),
469                            _ => return err,
470                        };
471                        prev.push(event);
472                    } else {
473                        return err;
474                    }
475                }
476                XmlEvent::EndDocument => {
477                    if !finished_folder_listing {
478                        return Err(Error::MissingData(format!("closing {FOLDER_LISTING_ELEM}")));
479                    }
480                    finished_document = true;
481                }
482                _ => return invalid_elem_err,
483            }
484        }
485        Ok(folder_listing)
486    }
487}
488
489impl Builder for FolderListing {
490    type Error = Error;
491
492    // Returns the MIME type of the raw bytes of data.
493    fn mime_type(&self) -> String {
494        "application/xml".to_string()
495    }
496
497    /// Builds self into raw bytes of the specific Document Type.
498    fn build<W: std::io::Write>(&self, buf: W) -> Result<(), Self::Error> {
499        let mut w = EmitterConfig::new()
500            .write_document_declaration(true)
501            .perform_indent(true)
502            .create_writer(buf);
503
504        // Begin `folder-listing` element.
505        let folder_listing = XmlWriteEvent::start_element(FOLDER_LISTING_ELEM)
506            .attr(VERSION_ATTR, Self::DEFAULT_VERSION);
507        w.write(folder_listing)?;
508
509        if self.parent_folder {
510            w.write(XmlWriteEvent::start_element(PARENT_FOLDER_ELEM))?;
511            w.write(XmlWriteEvent::end_element())?;
512        }
513        self.folders.iter().try_for_each(|f| f.write(&mut w))?;
514        self.files.iter().try_for_each(|f| f.write(&mut w))?;
515
516        // End `folder-listing` element.
517        w.write(XmlWriteEvent::end_element())?;
518        Ok(())
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525    use chrono::{NaiveDate, NaiveTime};
526
527    use std::fs;
528    use std::io::Cursor;
529
530    #[fuchsia::test]
531    fn parse_empty_folder_listing_success() {
532        const EMPTY_FOLDER_LISTING_TEST_FILE: &str = "/pkg/data/sample_folder_listing_1.xml";
533        let bytes = fs::read(EMPTY_FOLDER_LISTING_TEST_FILE).expect("should be ok");
534        let folder_listing = FolderListing::parse(Cursor::new(bytes)).expect("should be ok");
535        assert_eq!(folder_listing, FolderListing::new_empty());
536    }
537
538    #[fuchsia::test]
539    fn parse_simple_folder_listing_success() {
540        const FOLDER_LISTING_TEST_FILE: &str = "/pkg/data/sample_folder_listing_2.xml";
541        let bytes = fs::read(FOLDER_LISTING_TEST_FILE).expect("should be ok");
542        let folder_listing = FolderListing::parse(Cursor::new(bytes)).expect("should be ok");
543        assert_eq!(
544            folder_listing,
545            FolderListing {
546                parent_folder: true,
547                files: vec![
548                    File {
549                        data: Some("Jumar Handling Guide".to_string()),
550                        attributes: HashSet::from([
551                            FolderListingAttribute::Name("Jumar.txt".to_string()),
552                            FolderListingAttribute::Size("6672".to_string()),
553                        ]),
554                    },
555                    File {
556                        data: Some("OBEX Specification v1.0".to_string()),
557                        attributes: HashSet::from([
558                            FolderListingAttribute::Name("Obex.doc".to_string()),
559                            FolderListingAttribute::Type("application/msword".to_string()),
560                        ]),
561                    },
562                ],
563                folders: vec![
564                    Folder {
565                        data: None,
566                        attributes: HashSet::from([FolderListingAttribute::Name(
567                            "System".to_string()
568                        ),])
569                    },
570                    Folder {
571                        data: None,
572                        attributes: HashSet::from([FolderListingAttribute::Name(
573                            "IR Inbox".to_string()
574                        ),])
575                    },
576                ],
577            }
578        );
579    }
580
581    #[fuchsia::test]
582    fn parse_detailed_folder_listing_success() {
583        const DETAILED_FOLDER_LISTING_TEST_FILE: &str = "/pkg/data/sample_folder_listing_3.xml";
584        let bytes = fs::read(DETAILED_FOLDER_LISTING_TEST_FILE).expect("should be ok");
585        let folder_listing = FolderListing::parse(Cursor::new(bytes)).expect("should be ok");
586        assert_eq!(
587            folder_listing,
588            FolderListing {
589                parent_folder: true,
590                files: vec![
591                    File {
592                        data: None,
593                        attributes: HashSet::from([
594                            FolderListingAttribute::Name("Jumar.txt".to_string()),
595                            FolderListingAttribute::Created(FormattedDateTimeObj(
596                                NaiveDateTime::new(
597                                    NaiveDate::from_ymd_opt(1997, 12, 09).unwrap(),
598                                    NaiveTime::from_hms_opt(09, 03, 00).unwrap(),
599                                ),
600                                FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
601                            )),
602                            FolderListingAttribute::Size("6672".to_string()),
603                            FolderListingAttribute::Modified(FormattedDateTimeObj(
604                                NaiveDateTime::new(
605                                    NaiveDate::from_ymd_opt(1997, 12, 22).unwrap(),
606                                    NaiveTime::from_hms_opt(16, 41, 00).unwrap(),
607                                ),
608                                FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
609                            )),
610                            FolderListingAttribute::UserPerm(Permission::READ | Permission::WRITE),
611                        ]),
612                    },
613                    File {
614                        data: None,
615                        attributes: HashSet::from([
616                            FolderListingAttribute::Name("Obex.doc".to_string()),
617                            FolderListingAttribute::Created(FormattedDateTimeObj(
618                                NaiveDateTime::new(
619                                    NaiveDate::from_ymd_opt(1997, 01, 22).unwrap(),
620                                    NaiveTime::from_hms_opt(10, 23, 00).unwrap(),
621                                ),
622                                FormattedDateTimeObj::ISO_8601_UTC_TIME_FORMAT.to_string(),
623                            )),
624                            FolderListingAttribute::Size("41042".to_string()),
625                            FolderListingAttribute::Type("application/msword".to_string()),
626                            FolderListingAttribute::Modified(FormattedDateTimeObj(
627                                NaiveDateTime::new(
628                                    NaiveDate::from_ymd_opt(1997, 01, 22).unwrap(),
629                                    NaiveTime::from_hms_opt(10, 23, 00).unwrap(),
630                                ),
631                                FormattedDateTimeObj::ISO_8601_UTC_TIME_FORMAT.to_string(),
632                            )),
633                        ]),
634                    },
635                ],
636                folders: vec![
637                    Folder {
638                        data: None,
639                        attributes: HashSet::from([
640                            FolderListingAttribute::Name("System".to_string()),
641                            FolderListingAttribute::Created(FormattedDateTimeObj(
642                                NaiveDateTime::new(
643                                    NaiveDate::from_ymd_opt(1996, 11, 03).unwrap(),
644                                    NaiveTime::from_hms_opt(14, 15, 00).unwrap(),
645                                ),
646                                FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
647                            )),
648                        ]),
649                    },
650                    Folder {
651                        data: None,
652                        attributes: HashSet::from([
653                            FolderListingAttribute::Name("IR Inbox".to_string()),
654                            FolderListingAttribute::Created(FormattedDateTimeObj(
655                                NaiveDateTime::new(
656                                    NaiveDate::from_ymd_opt(1995, 03, 30).unwrap(),
657                                    NaiveTime::from_hms_opt(10, 50, 00).unwrap(),
658                                ),
659                                FormattedDateTimeObj::ISO_8601_UTC_TIME_FORMAT.to_string(),
660                            )),
661                        ]),
662                    },
663                ],
664            }
665        );
666    }
667
668    #[fuchsia::test]
669    fn parse_folder_listing_fail() {
670        let bad_sample_xml_files = vec![
671            "/pkg/data/bad_sample.xml",
672            "/pkg/data/bad_sample_folder_listing_1.xml",
673            "/pkg/data/bad_sample_folder_listing_2.xml",
674            "/pkg/data/bad_sample_folder_listing_3.xml",
675            "/pkg/data/bad_sample_folder_listing_4.xml",
676            "/pkg/data/bad_sample_folder_listing_5.xml",
677            "/pkg/data/bad_sample_folder_listing_6.xml",
678            "/pkg/data/bad_sample_folder_listing_7.xml",
679            "/pkg/data/bad_sample_folder_listing_8.xml",
680        ];
681
682        bad_sample_xml_files.iter().for_each(|f| {
683            let bytes = fs::read(f).expect("should be ok");
684            let _ = FolderListing::parse(Cursor::new(bytes)).expect_err("should have failed");
685        });
686    }
687
688    #[fuchsia::test]
689    fn build_empty_folder_listing_success() {
690        // Empty folder listing example.
691        let empty_folder_listing = FolderListing::new_empty();
692        let mut buf = Vec::new();
693        assert_eq!(empty_folder_listing.mime_type(), "application/xml");
694        empty_folder_listing.build(&mut buf).expect("should have succeeded");
695        assert_eq!(
696            empty_folder_listing,
697            FolderListing::parse(Cursor::new(buf)).expect("should be valid xml")
698        );
699    }
700
701    #[fuchsia::test]
702    fn build_simple_folder_listing_success() {
703        let folder_listing = FolderListing {
704            parent_folder: true,
705            files: vec![File {
706                data: Some("Jumar Handling Guide".to_string()),
707                attributes: HashSet::from([
708                    FolderListingAttribute::Name("Jumar.txt".to_string()),
709                    FolderListingAttribute::Size("6672".to_string()),
710                ]),
711            }],
712            folders: vec![Folder {
713                data: None,
714                attributes: HashSet::from([FolderListingAttribute::Name("System".to_string())]),
715            }],
716        };
717        let mut buf = Vec::new();
718        folder_listing.build(&mut buf).expect("should have succeeded");
719        assert_eq!(
720            folder_listing,
721            FolderListing::parse(Cursor::new(buf)).expect("should be valid xml")
722        );
723    }
724
725    #[fuchsia::test]
726    fn build_detailed_folder_listing_success() {
727        let detailed_folder_listing = FolderListing {
728            parent_folder: true,
729            files: vec![File {
730                data: None,
731                attributes: HashSet::from([
732                    FolderListingAttribute::Name("Jumar.txt".to_string()),
733                    FolderListingAttribute::Created(FormattedDateTimeObj(
734                        NaiveDateTime::new(
735                            NaiveDate::from_ymd_opt(1997, 12, 09).unwrap(),
736                            NaiveTime::from_hms_opt(09, 03, 00).unwrap(),
737                        ),
738                        FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
739                    )),
740                    FolderListingAttribute::Size("6672".to_string()),
741                    FolderListingAttribute::Modified(FormattedDateTimeObj(
742                        NaiveDateTime::new(
743                            NaiveDate::from_ymd_opt(1997, 12, 22).unwrap(),
744                            NaiveTime::from_hms_opt(16, 41, 00).unwrap(),
745                        ),
746                        FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
747                    )),
748                    FolderListingAttribute::UserPerm(Permission::READ | Permission::WRITE),
749                ]),
750            }],
751            folders: vec![Folder {
752                data: None,
753                attributes: HashSet::from([
754                    FolderListingAttribute::Name("System".to_string()),
755                    FolderListingAttribute::Created(FormattedDateTimeObj(
756                        NaiveDateTime::new(
757                            NaiveDate::from_ymd_opt(1996, 11, 03).unwrap(),
758                            NaiveTime::from_hms_opt(14, 15, 00).unwrap(),
759                        ),
760                        FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
761                    )),
762                ]),
763            }],
764        };
765        let mut buf = Vec::new();
766        assert_eq!(detailed_folder_listing.mime_type(), "application/xml");
767        detailed_folder_listing.build(&mut buf).expect("should have succeeded");
768
769        assert_eq!(
770            detailed_folder_listing,
771            FolderListing::parse(Cursor::new(buf)).expect("should be valid xml")
772        );
773    }
774}