1use 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
20const 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#[derive(Clone, Debug, PartialEq)]
83pub struct FormattedDateTimeObj(NaiveDateTime, String);
84
85impl FormattedDateTimeObj {
86 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 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 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#[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 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 writer.write(XmlWriteEvent::end_element()).map_err(Into::into)
231 }
232
233 fn validate(&self) -> Result<(), Error> {
234 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#[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 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 Ok(writer.write(XmlWriteEvent::end_element())?)
291 }
292
293 fn validate(&self) -> Result<(), Error> {
294 if !self.attributes.contains(&FolderListingAttribute::Name("".to_string())) {
296 return Err(Error::MissingData(NAME_ATTR.to_string()));
297 }
298 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#[derive(Clone, Debug, PartialEq)]
341pub struct FolderListing {
342 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 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 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 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 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 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 let mut finished_document = false;
416 let mut finished_folder_listing = false;
417 while !finished_document {
418 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 fn mime_type(&self) -> String {
498 "application/xml".to_string()
499 }
500
501 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 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 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 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}