Skip to main content

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::EventWriter;
12use xml::attribute::OwnedAttribute;
13use xml::name::OwnedName;
14use xml::reader::{ParserConfig, XmlEvent};
15use xml::writer::{EmitterConfig, XmlEvent as XmlWriteEvent};
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 mut xml_event = reader.next()?;
406        while matches!(xml_event, XmlEvent::Doctype { .. } | XmlEvent::Comment(_)) {
407            xml_event = reader.next()?;
408        }
409        let _ = Self::validate_folder_listing_element(xml_event)?;
410
411        prev.push(ParsedXmlEvent::FolderListingElement);
412        let mut folder_listing = Self::new_empty();
413
414        // Process remaining elements elements.
415        let mut finished_document = false;
416        let mut finished_folder_listing = false;
417        while !finished_document {
418            // Could be either end of folder listing element,
419            let e = reader.next()?;
420            let invalid_elem_err = Err(Error::InvalidData(format!("{:?}", e)));
421            match e {
422                XmlEvent::StartElement { ref name, .. } => {
423                    match name.local_name.as_str() {
424                        PARENT_FOLDER_ELEM => prev.push(ParsedXmlEvent::ParentFolderElement),
425                        FOLDER_ELEM => prev.push(ParsedXmlEvent::FolderElement(e.try_into()?)),
426                        FILE_ELEM => prev.push(ParsedXmlEvent::FileElement(e.try_into()?)),
427                        other_value => return Err(Error::InvalidData(other_value.to_string())),
428                    };
429                }
430                XmlEvent::EndElement { ref name } => {
431                    let Some(parsed_elem) = prev.pop() else {
432                        return invalid_elem_err;
433                    };
434                    match name.local_name.as_str() {
435                        FOLDER_LISTING_ELEM => {
436                            let ParsedXmlEvent::FolderListingElement = parsed_elem else {
437                                return Err(Error::MissingData(format!(
438                                    "closing {FOLDER_LISTING_ELEM}"
439                                )));
440                            };
441                            finished_folder_listing = true;
442                        }
443                        PARENT_FOLDER_ELEM => {
444                            let ParsedXmlEvent::ParentFolderElement = parsed_elem else {
445                                return Err(Error::MissingData(format!(
446                                    "closing {PARENT_FOLDER_ELEM}"
447                                )));
448                            };
449                            folder_listing.parent_folder = true;
450                        }
451                        FOLDER_ELEM => {
452                            let ParsedXmlEvent::FolderElement(f) = parsed_elem else {
453                                return Err(Error::MissingData(format!("closing {FOLDER_ELEM}")));
454                            };
455                            folder_listing.folders.push(f);
456                        }
457                        FILE_ELEM => {
458                            let ParsedXmlEvent::FileElement(f) = parsed_elem else {
459                                return Err(Error::MissingData(format!("closing {FILE_ELEM}")));
460                            };
461                            folder_listing.files.push(f);
462                        }
463                        _ => return invalid_elem_err,
464                    };
465                }
466                XmlEvent::Characters(data) => {
467                    let err = Err(Error::InvalidData(data.clone()));
468                    if let Some(mut event) = prev.pop() {
469                        match &mut event {
470                            ParsedXmlEvent::FolderElement(f) => f.data = Some(data),
471                            ParsedXmlEvent::FileElement(f) => f.data = Some(data),
472                            _ => return err,
473                        };
474                        prev.push(event);
475                    } else {
476                        return err;
477                    }
478                }
479                XmlEvent::EndDocument => {
480                    if !finished_folder_listing {
481                        return Err(Error::MissingData(format!("closing {FOLDER_LISTING_ELEM}")));
482                    }
483                    finished_document = true;
484                }
485                XmlEvent::Doctype { .. } | XmlEvent::Comment(_) => {}
486                _ => return invalid_elem_err,
487            }
488        }
489        Ok(folder_listing)
490    }
491}
492
493impl Builder for FolderListing {
494    type Error = Error;
495
496    // Returns the MIME type of the raw bytes of data.
497    fn mime_type(&self) -> String {
498        "application/xml".to_string()
499    }
500
501    /// Builds self into raw bytes of the specific Document Type.
502    fn build<W: std::io::Write>(&self, buf: W) -> Result<(), Self::Error> {
503        let mut w = EmitterConfig::new()
504            .write_document_declaration(true)
505            .perform_indent(true)
506            .create_writer(buf);
507
508        // Begin `folder-listing` element.
509        let folder_listing = XmlWriteEvent::start_element(FOLDER_LISTING_ELEM)
510            .attr(VERSION_ATTR, Self::DEFAULT_VERSION);
511        w.write(folder_listing)?;
512
513        if self.parent_folder {
514            w.write(XmlWriteEvent::start_element(PARENT_FOLDER_ELEM))?;
515            w.write(XmlWriteEvent::end_element())?;
516        }
517        self.folders.iter().try_for_each(|f| f.write(&mut w))?;
518        self.files.iter().try_for_each(|f| f.write(&mut w))?;
519
520        // End `folder-listing` element.
521        w.write(XmlWriteEvent::end_element())?;
522        Ok(())
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use chrono::{NaiveDate, NaiveTime};
530
531    use std::fs;
532    use std::io::Cursor;
533
534    #[fuchsia::test]
535    fn parse_empty_folder_listing_success() {
536        const EMPTY_FOLDER_LISTING_TEST_FILE: &str = "/pkg/data/sample_folder_listing_1.xml";
537        let bytes = fs::read(EMPTY_FOLDER_LISTING_TEST_FILE).expect("should be ok");
538        let folder_listing = FolderListing::parse(Cursor::new(bytes)).expect("should be ok");
539        assert_eq!(folder_listing, FolderListing::new_empty());
540    }
541
542    #[fuchsia::test]
543    fn parse_simple_folder_listing_success() {
544        const FOLDER_LISTING_TEST_FILE: &str = "/pkg/data/sample_folder_listing_2.xml";
545        let bytes = fs::read(FOLDER_LISTING_TEST_FILE).expect("should be ok");
546        let folder_listing = FolderListing::parse(Cursor::new(bytes)).expect("should be ok");
547        assert_eq!(
548            folder_listing,
549            FolderListing {
550                parent_folder: true,
551                files: vec![
552                    File {
553                        data: Some("Jumar Handling Guide".to_string()),
554                        attributes: HashSet::from([
555                            FolderListingAttribute::Name("Jumar.txt".to_string()),
556                            FolderListingAttribute::Size("6672".to_string()),
557                        ]),
558                    },
559                    File {
560                        data: Some("OBEX Specification v1.0".to_string()),
561                        attributes: HashSet::from([
562                            FolderListingAttribute::Name("Obex.doc".to_string()),
563                            FolderListingAttribute::Type("application/msword".to_string()),
564                        ]),
565                    },
566                ],
567                folders: vec![
568                    Folder {
569                        data: None,
570                        attributes: HashSet::from([FolderListingAttribute::Name(
571                            "System".to_string()
572                        ),])
573                    },
574                    Folder {
575                        data: None,
576                        attributes: HashSet::from([FolderListingAttribute::Name(
577                            "IR Inbox".to_string()
578                        ),])
579                    },
580                ],
581            }
582        );
583    }
584
585    #[fuchsia::test]
586    fn parse_detailed_folder_listing_success() {
587        const DETAILED_FOLDER_LISTING_TEST_FILE: &str = "/pkg/data/sample_folder_listing_3.xml";
588        let bytes = fs::read(DETAILED_FOLDER_LISTING_TEST_FILE).expect("should be ok");
589        let folder_listing = FolderListing::parse(Cursor::new(bytes)).expect("should be ok");
590        assert_eq!(
591            folder_listing,
592            FolderListing {
593                parent_folder: true,
594                files: vec![
595                    File {
596                        data: None,
597                        attributes: HashSet::from([
598                            FolderListingAttribute::Name("Jumar.txt".to_string()),
599                            FolderListingAttribute::Created(FormattedDateTimeObj(
600                                NaiveDateTime::new(
601                                    NaiveDate::from_ymd_opt(1997, 12, 09).unwrap(),
602                                    NaiveTime::from_hms_opt(09, 03, 00).unwrap(),
603                                ),
604                                FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
605                            )),
606                            FolderListingAttribute::Size("6672".to_string()),
607                            FolderListingAttribute::Modified(FormattedDateTimeObj(
608                                NaiveDateTime::new(
609                                    NaiveDate::from_ymd_opt(1997, 12, 22).unwrap(),
610                                    NaiveTime::from_hms_opt(16, 41, 00).unwrap(),
611                                ),
612                                FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
613                            )),
614                            FolderListingAttribute::UserPerm(Permission::READ | Permission::WRITE),
615                        ]),
616                    },
617                    File {
618                        data: None,
619                        attributes: HashSet::from([
620                            FolderListingAttribute::Name("Obex.doc".to_string()),
621                            FolderListingAttribute::Created(FormattedDateTimeObj(
622                                NaiveDateTime::new(
623                                    NaiveDate::from_ymd_opt(1997, 01, 22).unwrap(),
624                                    NaiveTime::from_hms_opt(10, 23, 00).unwrap(),
625                                ),
626                                FormattedDateTimeObj::ISO_8601_UTC_TIME_FORMAT.to_string(),
627                            )),
628                            FolderListingAttribute::Size("41042".to_string()),
629                            FolderListingAttribute::Type("application/msword".to_string()),
630                            FolderListingAttribute::Modified(FormattedDateTimeObj(
631                                NaiveDateTime::new(
632                                    NaiveDate::from_ymd_opt(1997, 01, 22).unwrap(),
633                                    NaiveTime::from_hms_opt(10, 23, 00).unwrap(),
634                                ),
635                                FormattedDateTimeObj::ISO_8601_UTC_TIME_FORMAT.to_string(),
636                            )),
637                        ]),
638                    },
639                ],
640                folders: vec![
641                    Folder {
642                        data: None,
643                        attributes: HashSet::from([
644                            FolderListingAttribute::Name("System".to_string()),
645                            FolderListingAttribute::Created(FormattedDateTimeObj(
646                                NaiveDateTime::new(
647                                    NaiveDate::from_ymd_opt(1996, 11, 03).unwrap(),
648                                    NaiveTime::from_hms_opt(14, 15, 00).unwrap(),
649                                ),
650                                FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
651                            )),
652                        ]),
653                    },
654                    Folder {
655                        data: None,
656                        attributes: HashSet::from([
657                            FolderListingAttribute::Name("IR Inbox".to_string()),
658                            FolderListingAttribute::Created(FormattedDateTimeObj(
659                                NaiveDateTime::new(
660                                    NaiveDate::from_ymd_opt(1995, 03, 30).unwrap(),
661                                    NaiveTime::from_hms_opt(10, 50, 00).unwrap(),
662                                ),
663                                FormattedDateTimeObj::ISO_8601_UTC_TIME_FORMAT.to_string(),
664                            )),
665                        ]),
666                    },
667                ],
668            }
669        );
670    }
671
672    #[fuchsia::test]
673    fn parse_folder_listing_fail() {
674        let bad_sample_xml_files = vec![
675            "/pkg/data/bad_sample.xml",
676            "/pkg/data/bad_sample_folder_listing_1.xml",
677            "/pkg/data/bad_sample_folder_listing_2.xml",
678            "/pkg/data/bad_sample_folder_listing_3.xml",
679            "/pkg/data/bad_sample_folder_listing_4.xml",
680            "/pkg/data/bad_sample_folder_listing_5.xml",
681            "/pkg/data/bad_sample_folder_listing_6.xml",
682            "/pkg/data/bad_sample_folder_listing_7.xml",
683            "/pkg/data/bad_sample_folder_listing_8.xml",
684        ];
685
686        bad_sample_xml_files.iter().for_each(|f| {
687            let bytes = fs::read(f).expect("should be ok");
688            let _ = FolderListing::parse(Cursor::new(bytes)).expect_err("should have failed");
689        });
690    }
691
692    #[fuchsia::test]
693    fn build_empty_folder_listing_success() {
694        // Empty folder listing example.
695        let empty_folder_listing = FolderListing::new_empty();
696        let mut buf = Vec::new();
697        assert_eq!(empty_folder_listing.mime_type(), "application/xml");
698        empty_folder_listing.build(&mut buf).expect("should have succeeded");
699        assert_eq!(
700            empty_folder_listing,
701            FolderListing::parse(Cursor::new(buf)).expect("should be valid xml")
702        );
703    }
704
705    #[fuchsia::test]
706    fn build_simple_folder_listing_success() {
707        let folder_listing = FolderListing {
708            parent_folder: true,
709            files: vec![File {
710                data: Some("Jumar Handling Guide".to_string()),
711                attributes: HashSet::from([
712                    FolderListingAttribute::Name("Jumar.txt".to_string()),
713                    FolderListingAttribute::Size("6672".to_string()),
714                ]),
715            }],
716            folders: vec![Folder {
717                data: None,
718                attributes: HashSet::from([FolderListingAttribute::Name("System".to_string())]),
719            }],
720        };
721        let mut buf = Vec::new();
722        folder_listing.build(&mut buf).expect("should have succeeded");
723        assert_eq!(
724            folder_listing,
725            FolderListing::parse(Cursor::new(buf)).expect("should be valid xml")
726        );
727    }
728
729    #[fuchsia::test]
730    fn build_detailed_folder_listing_success() {
731        let detailed_folder_listing = FolderListing {
732            parent_folder: true,
733            files: vec![File {
734                data: None,
735                attributes: HashSet::from([
736                    FolderListingAttribute::Name("Jumar.txt".to_string()),
737                    FolderListingAttribute::Created(FormattedDateTimeObj(
738                        NaiveDateTime::new(
739                            NaiveDate::from_ymd_opt(1997, 12, 09).unwrap(),
740                            NaiveTime::from_hms_opt(09, 03, 00).unwrap(),
741                        ),
742                        FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
743                    )),
744                    FolderListingAttribute::Size("6672".to_string()),
745                    FolderListingAttribute::Modified(FormattedDateTimeObj(
746                        NaiveDateTime::new(
747                            NaiveDate::from_ymd_opt(1997, 12, 22).unwrap(),
748                            NaiveTime::from_hms_opt(16, 41, 00).unwrap(),
749                        ),
750                        FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
751                    )),
752                    FolderListingAttribute::UserPerm(Permission::READ | Permission::WRITE),
753                ]),
754            }],
755            folders: vec![Folder {
756                data: None,
757                attributes: HashSet::from([
758                    FolderListingAttribute::Name("System".to_string()),
759                    FolderListingAttribute::Created(FormattedDateTimeObj(
760                        NaiveDateTime::new(
761                            NaiveDate::from_ymd_opt(1996, 11, 03).unwrap(),
762                            NaiveTime::from_hms_opt(14, 15, 00).unwrap(),
763                        ),
764                        FormattedDateTimeObj::ISO_8601_TIME_FORMAT.to_string(),
765                    )),
766                ]),
767            }],
768        };
769        let mut buf = Vec::new();
770        assert_eq!(detailed_folder_listing.mime_type(), "application/xml");
771        detailed_folder_listing.build(&mut buf).expect("should have succeeded");
772
773        assert_eq!(
774            detailed_folder_listing,
775            FolderListing::parse(Cursor::new(buf)).expect("should be valid xml")
776        );
777    }
778}