burpsuite_kit/http_history/
items.rs

1use core::{
2    iter::Iterator,
3    num::ParseIntError,
4    str::{self, ParseBoolError},
5};
6use std::{collections::HashSet, io::BufRead};
7
8use chrono::NaiveDateTime;
9use http::{
10    uri::{InvalidUri, Scheme},
11    Method, StatusCode,
12};
13use quick_xml::{
14    events::{attributes::Attribute, Event},
15    Error, Reader,
16};
17
18use super::item::{Item, Tag as ItemTag, TAG_SET as ITEM_TAG_SET};
19
20//
21pub struct Items<R>
22where
23    R: BufRead,
24{
25    pub attr: ItemsAttr,
26
27    reader: Reader<R>,
28    buf: Vec<u8>,
29    state: State,
30    item: Item,
31    processed_item_tags: HashSet<ItemTag>,
32    is_eof: bool,
33}
34
35pub struct ItemsAttr {
36    pub burp_version: String,
37    pub export_time: NaiveDateTime,
38}
39
40#[derive(PartialEq, Debug)]
41enum State {
42    Idle,
43    WaitTag,
44    WaitTagValue(ItemTag),
45}
46
47#[derive(thiserror::Error, Debug)]
48pub enum ItemsParseError {
49    #[error("XmlError {0:?}")]
50    XmlError(Error),
51    #[error("UnknownTag {0:?}")]
52    UnknownTag(Vec<u8>),
53    #[error("UnexpectedEof")]
54    UnexpectedEof,
55    #[error("AttrMissing {0}")]
56    AttrMissing(String),
57    #[error("AttrInvalid {0} {1}")]
58    AttrInvalid(String, String),
59}
60
61impl<R> Items<R>
62where
63    R: BufRead,
64{
65    pub fn from_reader(reader: R) -> Result<Self, ItemsParseError> {
66        let mut reader = Reader::from_reader(reader);
67
68        let mut buf = Vec::new();
69        let attr = loop {
70            match reader.read_event_into(&mut buf) {
71                Ok(Event::Start(e)) => match e.name().as_ref() {
72                    b"items" => {
73                        let attrs: Vec<Attribute<'_>> =
74                            e.attributes().filter_map(|ret| ret.ok()).collect();
75
76                        let burp_version = attrs
77                            .iter()
78                            .find(|a| a.key.as_ref() == b"burpVersion")
79                            .map(|x| x.value.clone())
80                            .ok_or_else(|| {
81                                ItemsParseError::AttrMissing("burpVersion".to_owned())
82                            })?;
83
84                        let burp_version = str::from_utf8(burp_version.as_ref())
85                            .map(|x| x.to_owned())
86                            .map_err(|err| {
87                                ItemsParseError::AttrInvalid(
88                                    "burpVersion".to_owned(),
89                                    err.to_string(),
90                                )
91                            })?;
92
93                        let export_time = attrs
94                            .iter()
95                            .find(|a| a.key.as_ref() == b"exportTime")
96                            .map(|x| x.value.clone())
97                            .ok_or_else(|| ItemsParseError::AttrMissing("exportTime".to_owned()))?;
98
99                        let export_time = NaiveDateTime::parse_from_str(
100                            str::from_utf8(export_time.as_ref()).map_err(|err| {
101                                ItemsParseError::AttrInvalid(
102                                    "exportTime".to_owned(),
103                                    err.to_string(),
104                                )
105                            })?,
106                            "%a %b %d %T %Z %Y",
107                        )
108                        .map_err(|err| {
109                            ItemsParseError::AttrInvalid("exportTime".to_owned(), err.to_string())
110                        })?;
111
112                        break ItemsAttr {
113                            burp_version,
114                            export_time,
115                        };
116                    }
117                    _ => {
118                        return Err(ItemsParseError::UnknownTag(
119                            e.name().into_inner().to_owned(),
120                        ))
121                    }
122                },
123                Err(err) => return Err(ItemsParseError::XmlError(err)),
124                Ok(Event::Eof) => return Err(ItemsParseError::UnexpectedEof),
125                _ => {}
126            }
127
128            buf.clear();
129        };
130
131        Ok(Self {
132            attr,
133            reader,
134            buf,
135            state: State::Idle,
136            item: Default::default(),
137            processed_item_tags: HashSet::new(),
138            is_eof: false,
139        })
140    }
141}
142
143#[derive(thiserror::Error, Debug)]
144pub enum ItemParseError {
145    #[error("XmlError {0:?}")]
146    XmlError(#[from] Error),
147    #[error("UnknownTag {0:?}")]
148    UnknownTag(Vec<u8>),
149    #[error("Unexpected")]
150    UnexpectedEof,
151    #[error("StateMismatch {0}")]
152    StateMismatch(String),
153    #[error("SomeTagsMissing {0:?}")]
154    SomeTagsMissing(HashSet<ItemTag>),
155    #[error("DuplicateTag {0:?}")]
156    DuplicateTag(ItemTag),
157    #[error("TagAttrMissing {0:?} {1}")]
158    TagAttrMissing(ItemTag, String),
159    #[error("TagAttrInvalid {0:?} {1} {2}")]
160    TagAttrInvalid(ItemTag, String, String),
161    #[error("TagValueMissing {0:?}")]
162    TagValueMissing(ItemTag),
163    #[error("TagValueInvalid {0:?} {1}")]
164    TagValueInvalid(ItemTag, String),
165}
166
167impl<R> Items<R>
168where
169    R: BufRead,
170{
171    fn item(&mut self) -> Result<Item, ItemParseError> {
172        loop {
173            match self.reader.read_event_into(&mut self.buf) {
174                Ok(Event::Start(e)) => match e.name().as_ref() {
175                    b"item" => {
176                        if State::Idle != self.state {
177                            return Err(ItemParseError::StateMismatch(format!(
178                                "expect {:?} but current {:?}",
179                                State::Idle,
180                                self.state
181                            )));
182                        }
183
184                        self.item = Default::default();
185                        self.state = State::WaitTag;
186                    }
187                    _ => {
188                        if let Ok(tag) = ItemTag::try_from(e.name().as_ref()) {
189                            match self.state {
190                                State::Idle => {
191                                    return Err(ItemParseError::StateMismatch(format!(
192                                        "expect not {:?}",
193                                        self.state
194                                    )));
195                                }
196                                State::WaitTag => {
197                                    if self.processed_item_tags.contains(&tag) {
198                                        return Err(ItemParseError::DuplicateTag(tag));
199                                    }
200                                    match tag {
201                                        ItemTag::Host => {
202                                            let attrs: Vec<Attribute<'_>> =
203                                                e.attributes().filter_map(|ret| ret.ok()).collect();
204
205                                            let ip = attrs
206                                                .iter()
207                                                .find(|a| a.key.as_ref() == b"ip")
208                                                .map(|x| x.value.clone())
209                                                .ok_or_else(|| {
210                                                    ItemParseError::TagAttrMissing(
211                                                        tag.to_owned(),
212                                                        "ip".to_owned(),
213                                                    )
214                                                })?;
215
216                                            self.item.host.0.ip = ip.into_owned();
217                                        }
218                                        ItemTag::Request => {
219                                            let attrs: Vec<Attribute<'_>> =
220                                                e.attributes().filter_map(|ret| ret.ok()).collect();
221
222                                            let base64 = attrs
223                                                .iter()
224                                                .find(|a| a.key.as_ref() == b"base64")
225                                                .map(|x| x.value.clone())
226                                                .ok_or_else(|| {
227                                                    ItemParseError::TagAttrMissing(
228                                                        tag.to_owned(),
229                                                        "base64".to_owned(),
230                                                    )
231                                                })?;
232
233                                            let base64 =
234                                                str::from_utf8(base64.as_ref()).map_err(|err| {
235                                                    ItemParseError::TagAttrInvalid(
236                                                        tag.to_owned(),
237                                                        "base64".to_owned(),
238                                                        err.to_string(),
239                                                    )
240                                                })?;
241
242                                            let base64: bool =
243                                                base64.parse().map_err(|err: ParseBoolError| {
244                                                    ItemParseError::TagAttrInvalid(
245                                                        tag.to_owned(),
246                                                        "base64".to_owned(),
247                                                        err.to_string(),
248                                                    )
249                                                })?;
250
251                                            self.item.request.0.base64 = base64;
252                                        }
253                                        ItemTag::Response => {
254                                            let attrs: Vec<Attribute<'_>> =
255                                                e.attributes().filter_map(|ret| ret.ok()).collect();
256
257                                            let base64 = attrs
258                                                .iter()
259                                                .find(|a| a.key.as_ref() == b"base64")
260                                                .map(|x| x.value.clone())
261                                                .ok_or_else(|| {
262                                                    ItemParseError::TagAttrMissing(
263                                                        tag.to_owned(),
264                                                        "base64".to_owned(),
265                                                    )
266                                                })?;
267
268                                            let base64 =
269                                                str::from_utf8(base64.as_ref()).map_err(|err| {
270                                                    ItemParseError::TagAttrInvalid(
271                                                        tag.to_owned(),
272                                                        "base64".to_owned(),
273                                                        err.to_string(),
274                                                    )
275                                                })?;
276
277                                            let base64: bool =
278                                                base64.parse().map_err(|err: ParseBoolError| {
279                                                    ItemParseError::TagAttrInvalid(
280                                                        tag.to_owned(),
281                                                        "base64".to_owned(),
282                                                        err.to_string(),
283                                                    )
284                                                })?;
285
286                                            self.item.response.0.base64 = base64;
287                                        }
288                                        _ => {}
289                                    }
290
291                                    self.state = State::WaitTagValue(tag)
292                                }
293                                State::WaitTagValue(_) => {
294                                    return Err(ItemParseError::StateMismatch(format!(
295                                        "expect not {:?}",
296                                        self.state
297                                    )));
298                                }
299                            }
300                        } else {
301                            return Err(ItemParseError::UnknownTag(
302                                e.name().into_inner().to_owned(),
303                            ));
304                        }
305                    }
306                },
307                Ok(Event::End(e)) => match e.name().as_ref() {
308                    b"items" => {}
309                    b"item" => {
310                        let unprocessed_item_tags = ITEM_TAG_SET
311                            .difference(&self.processed_item_tags)
312                            .collect::<HashSet<_>>();
313
314                        if !unprocessed_item_tags.is_empty() {
315                            return Err(ItemParseError::SomeTagsMissing(
316                                unprocessed_item_tags
317                                    .into_iter()
318                                    .map(|x| x.to_owned())
319                                    .collect(),
320                            ));
321                        }
322
323                        self.state = State::Idle;
324                        self.processed_item_tags.clear();
325
326                        return Ok(self.item.to_owned());
327                    }
328                    _ => {
329                        if let Ok(tag) = ItemTag::try_from(e.name().as_ref()) {
330                            match self.state {
331                                State::Idle => {
332                                    return Err(ItemParseError::StateMismatch(format!(
333                                        "expect not {:?}",
334                                        self.state
335                                    )));
336                                }
337                                State::WaitTag => {
338                                    return Err(ItemParseError::StateMismatch(format!(
339                                        "expect not {:?}",
340                                        self.state
341                                    )));
342                                }
343                                State::WaitTagValue(ref wait_tag) => {
344                                    #[allow(clippy::collapsible_else_if)]
345                                    if wait_tag == &tag {
346                                        if self.processed_item_tags.contains(&tag) {
347                                            self.state = State::WaitTag;
348                                        } else {
349                                            // TODO,
350                                            if tag != ItemTag::Comment {
351                                                return Err(ItemParseError::TagValueMissing(
352                                                    wait_tag.to_owned(),
353                                                ));
354                                            }
355                                        }
356                                    } else {
357                                        return Err(ItemParseError::StateMismatch(format!(
358                                            "expect {:?} but current {:?}",
359                                            State::WaitTagValue(tag),
360                                            self.state
361                                        )));
362                                    }
363                                }
364                            }
365                        } else {
366                            return Err(ItemParseError::UnknownTag(
367                                e.name().into_inner().to_owned(),
368                            ));
369                        }
370                    }
371                },
372                Ok(Event::Text(e)) => match self.state {
373                    State::Idle => {}
374                    State::WaitTag => {}
375                    State::WaitTagValue(ref tag) => {
376                        let bytes = e.as_ref();
377                        match tag {
378                            ItemTag::Time => {
379                                let time = NaiveDateTime::parse_from_str(
380                                    str::from_utf8(bytes).map_err(|err| {
381                                        ItemParseError::TagValueInvalid(
382                                            tag.to_owned(),
383                                            err.to_string(),
384                                        )
385                                    })?,
386                                    "%a %b %d %T %Z %Y",
387                                )
388                                .map_err(|err| {
389                                    ItemParseError::TagValueInvalid(tag.to_owned(), err.to_string())
390                                })?;
391
392                                self.item.time = time;
393
394                                self.processed_item_tags.insert(tag.to_owned());
395                            }
396                            ItemTag::Host => {
397                                self.item.host.1 =
398                                    String::from_utf8(bytes.to_vec()).map_err(|err| {
399                                        ItemParseError::TagValueInvalid(
400                                            tag.to_owned(),
401                                            err.to_string(),
402                                        )
403                                    })?;
404
405                                self.processed_item_tags.insert(tag.to_owned());
406                            }
407                            ItemTag::Port => {
408                                let port: u16 = str::from_utf8(bytes)
409                                    .map_err(|err| {
410                                        ItemParseError::TagValueInvalid(
411                                            tag.to_owned(),
412                                            err.to_string(),
413                                        )
414                                    })?
415                                    .parse()
416                                    .map_err(|err: ParseIntError| {
417                                        ItemParseError::TagValueInvalid(
418                                            tag.to_owned(),
419                                            err.to_string(),
420                                        )
421                                    })?;
422
423                                self.item.port = port;
424
425                                self.processed_item_tags.insert(tag.to_owned());
426                            }
427                            ItemTag::Protocol => {
428                                let protocol: Scheme = str::from_utf8(bytes)
429                                    .map_err(|err| {
430                                        ItemParseError::TagValueInvalid(
431                                            tag.to_owned(),
432                                            err.to_string(),
433                                        )
434                                    })?
435                                    .parse()
436                                    .map_err(|err: InvalidUri| {
437                                        ItemParseError::TagValueInvalid(
438                                            tag.to_owned(),
439                                            err.to_string(),
440                                        )
441                                    })?;
442
443                                self.item.protocol = protocol;
444
445                                self.processed_item_tags.insert(tag.to_owned());
446                            }
447                            ItemTag::Extension => {
448                                self.item.extension = if bytes == b"null" {
449                                    None
450                                } else {
451                                    Some(String::from_utf8(bytes.to_vec()).map_err(|err| {
452                                        ItemParseError::TagValueInvalid(
453                                            tag.to_owned(),
454                                            err.to_string(),
455                                        )
456                                    })?)
457                                };
458
459                                self.processed_item_tags.insert(tag.to_owned());
460                            }
461                            ItemTag::Status => {
462                                let status = StatusCode::from_bytes(bytes).map_err(|err| {
463                                    ItemParseError::TagValueInvalid(tag.to_owned(), err.to_string())
464                                })?;
465
466                                self.item.status = status;
467
468                                self.processed_item_tags.insert(tag.to_owned());
469                            }
470                            ItemTag::ResponseLength => {
471                                let response_length: u32 = str::from_utf8(bytes)
472                                    .map_err(|err| {
473                                        ItemParseError::TagValueInvalid(
474                                            tag.to_owned(),
475                                            err.to_string(),
476                                        )
477                                    })?
478                                    .parse()
479                                    .map_err(|err: ParseIntError| {
480                                        ItemParseError::TagValueInvalid(
481                                            tag.to_owned(),
482                                            err.to_string(),
483                                        )
484                                    })?;
485
486                                self.item.response_length = response_length;
487
488                                self.processed_item_tags.insert(tag.to_owned());
489                            }
490                            ItemTag::Mimetype => {
491                                self.item.mimetype =
492                                    String::from_utf8(bytes.to_vec()).map_err(|err| {
493                                        ItemParseError::TagValueInvalid(
494                                            tag.to_owned(),
495                                            err.to_string(),
496                                        )
497                                    })?;
498
499                                self.processed_item_tags.insert(tag.to_owned());
500                            }
501                            ItemTag::Comment => {
502                                // TODO, why comment is b"\r\n  "
503                                self.item.comment = if bytes.is_empty() || bytes == b"\r\n  " {
504                                    None
505                                } else {
506                                    Some(String::from_utf8(bytes.to_vec()).map_err(|err| {
507                                        ItemParseError::TagValueInvalid(
508                                            tag.to_owned(),
509                                            err.to_string(),
510                                        )
511                                    })?)
512                                };
513
514                                self.processed_item_tags.insert(tag.to_owned());
515                            }
516                            _ => {}
517                        }
518                    }
519                },
520                Ok(Event::CData(e)) => match self.state {
521                    State::Idle => {}
522                    State::WaitTag => {}
523                    State::WaitTagValue(ref tag) => {
524                        let bytes_text = e.escape()?;
525                        let bytes = bytes_text.as_ref();
526                        match tag {
527                            ItemTag::Request => {
528                                self.item.request.1 = bytes.to_vec();
529
530                                self.processed_item_tags.insert(tag.to_owned());
531                            }
532                            ItemTag::Response => {
533                                self.item.response.1 = bytes.to_vec();
534
535                                self.processed_item_tags.insert(tag.to_owned());
536                            }
537                            ItemTag::Url => {
538                                let url = String::from_utf8(bytes.to_vec()).map_err(|err| {
539                                    ItemParseError::TagValueInvalid(tag.to_owned(), err.to_string())
540                                })?;
541
542                                self.item.url = url;
543
544                                self.processed_item_tags.insert(tag.to_owned());
545                            }
546                            ItemTag::Method => {
547                                let method = Method::from_bytes(bytes).map_err(|err| {
548                                    ItemParseError::TagValueInvalid(tag.to_owned(), err.to_string())
549                                })?;
550
551                                self.item.method = method;
552
553                                self.processed_item_tags.insert(tag.to_owned());
554                            }
555                            ItemTag::Path => {
556                                let path = String::from_utf8(bytes.to_vec()).map_err(|err| {
557                                    ItemParseError::TagValueInvalid(tag.to_owned(), err.to_string())
558                                })?;
559
560                                self.item.path = path;
561                                self.processed_item_tags.insert(tag.to_owned());
562                            }
563                            _ => {}
564                        }
565                    }
566                },
567                Err(err) => return Err(ItemParseError::XmlError(err)),
568                Ok(Event::Eof) => return Err(ItemParseError::UnexpectedEof),
569                _ => {}
570            }
571
572            self.buf.clear();
573        }
574    }
575}
576
577impl<R> Iterator for Items<R>
578where
579    R: BufRead,
580{
581    type Item = Result<Item, ItemParseError>;
582
583    fn next(&mut self) -> Option<Self::Item> {
584        match self.item() {
585            Ok(item) => Some(Ok(item)),
586            Err(err) => match err {
587                ItemParseError::UnexpectedEof => {
588                    if self.state == State::Idle {
589                        None
590                    } else {
591                        if self.is_eof {
592                            return None;
593                        }
594                        self.is_eof = true;
595                        Some(Err(err))
596                    }
597                }
598                _ => Some(Err(err)),
599            },
600        }
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    use std::{error, fs::File, io::BufReader};
609
610    use chrono::NaiveDate;
611
612    #[test]
613    fn test_v_1_7_36() -> Result<(), Box<dyn error::Error>> {
614        let file = File::open("tests/http_history_files/burpsuite_community_v1.7.36.xml").unwrap();
615        let buf_reader = BufReader::new(file);
616        let mut items = Items::from_reader(buf_reader)?;
617
618        assert_eq!(items.attr.burp_version, "1.7.36");
619        assert_eq!(
620            items.attr.export_time,
621            NaiveDate::from_ymd_opt(2021, 1, 6)
622                .expect("")
623                .and_hms_opt(11, 27, 54)
624                .expect("")
625        );
626
627        match items.next() {
628            Some(Ok(item)) => {
629                assert_eq!(
630                    item.time,
631                    NaiveDate::from_ymd_opt(2021, 1, 6)
632                        .expect("")
633                        .and_hms_opt(11, 26, 17)
634                        .expect("")
635                );
636                assert_eq!(item.url, "http://httpbin.org/get?foo=bar");
637                assert_eq!(item.host.0.ip, b"184.72.216.47");
638                assert_eq!(item.host.1, "httpbin.org");
639                assert_eq!(item.port, 80);
640                assert_eq!(item.protocol, Scheme::HTTP);
641                assert_eq!(item.method, Method::GET);
642                assert_eq!(item.path, "/get?foo=bar");
643                assert_eq!(item.extension, None);
644                assert!(item.request.0.base64);
645                assert!(item.request.1.starts_with(b"R0VUIC"));
646                assert!(item.request.1.ends_with(b"UNCg0K"));
647                assert_eq!(item.status, 200);
648                assert_eq!(item.response_length, 508);
649                assert_eq!(item.mimetype, "JSON");
650                assert!(item.response.0.base64);
651                assert!(item.response.1.starts_with(b"SFRUUC"));
652                assert!(item.response.1.ends_with(b"p9Cg=="));
653                assert_eq!(item.comment, None);
654            }
655            Some(Err(err)) => {
656                eprintln!("{err}");
657                panic!("{err}");
658            }
659            None => panic!(),
660        }
661
662        match items.next() {
663            Some(Ok(item)) => {
664                assert_eq!(
665                    item.time,
666                    NaiveDate::from_ymd_opt(2021, 1, 6)
667                        .expect("")
668                        .and_hms_opt(11, 27, 9)
669                        .expect("")
670                );
671                assert_eq!(item.url, "https://httpbin.org/post");
672                assert_eq!(item.host.0.ip, b"54.164.234.192");
673                assert_eq!(item.host.1, "httpbin.org");
674                assert_eq!(item.port, 443);
675                assert_eq!(item.protocol, Scheme::HTTPS);
676                assert_eq!(item.method, Method::POST);
677                assert_eq!(item.path, "/post");
678                assert_eq!(item.extension, None);
679                assert!(item.request.0.base64);
680                assert!(item.request.1.starts_with(b"UE9TVC"));
681                assert!(item.request.1.ends_with(b"0Ke30="));
682                assert_eq!(item.status, 200);
683                assert_eq!(item.response_length, 614);
684                assert_eq!(item.mimetype, "JSON");
685                assert!(item.response.0.base64);
686                assert!(item.response.1.starts_with(b"SFRUUC"));
687                assert!(item.response.1.ends_with(b"IKfQo="));
688                assert_eq!(item.comment, None);
689            }
690            Some(Err(err)) => {
691                eprintln!("{err}");
692                panic!("{err}");
693            }
694            None => panic!(),
695        }
696
697        assert!(items.next().is_none());
698
699        Ok(())
700    }
701
702    #[test]
703    fn test_v_2020_12_1() -> Result<(), Box<dyn error::Error>> {
704        let file =
705            File::open("tests/http_history_files/burpsuite_community_v2020.12.1.xml").unwrap();
706        let buf_reader = BufReader::new(file);
707        let mut items = Items::from_reader(buf_reader)?;
708
709        assert_eq!(items.attr.burp_version, "2020.12.1");
710        assert_eq!(
711            items.attr.export_time,
712            NaiveDate::from_ymd_opt(2021, 1, 6)
713                .expect("")
714                .and_hms_opt(11, 36, 18)
715                .expect("")
716        );
717
718        match items.next() {
719            Some(Ok(item)) => {
720                assert_eq!(
721                    item.time,
722                    NaiveDate::from_ymd_opt(2021, 1, 6)
723                        .expect("")
724                        .and_hms_opt(11, 36, 3)
725                        .expect("")
726                );
727                assert_eq!(item.url, "http://httpbin.org/get?foo=bar");
728                assert_eq!(item.host.0.ip, b"184.72.216.47");
729                assert_eq!(item.host.1, "httpbin.org");
730                assert_eq!(item.port, 80);
731                assert_eq!(item.protocol, Scheme::HTTP);
732                assert_eq!(item.method, Method::GET);
733                assert_eq!(item.path, "/get?foo=bar");
734                assert_eq!(item.extension, None);
735                assert!(item.request.0.base64);
736                assert!(item.request.1.starts_with(b"R0VUIC"));
737                assert!(item.request.1.ends_with(b"UNCg0K"));
738                assert_eq!(item.status, 200);
739                assert_eq!(item.response_length, 508);
740                assert_eq!(item.mimetype, "JSON");
741                assert!(item.response.0.base64);
742                assert!(item.response.1.starts_with(b"SFRUUC"));
743                assert!(item.response.1.ends_with(b"p9Cg=="));
744                assert_eq!(item.comment, None);
745            }
746            Some(Err(err)) => {
747                eprintln!("{err}");
748                panic!("{err}");
749            }
750            None => panic!(),
751        }
752
753        match items.next() {
754            Some(Ok(item)) => {
755                assert_eq!(
756                    item.time,
757                    NaiveDate::from_ymd_opt(2021, 1, 6)
758                        .expect("")
759                        .and_hms_opt(11, 36, 6)
760                        .expect("")
761                );
762                assert_eq!(item.url, "https://httpbin.org/post");
763                assert_eq!(item.host.0.ip, b"184.72.216.47");
764                assert_eq!(item.host.1, "httpbin.org");
765                assert_eq!(item.port, 443);
766                assert_eq!(item.protocol, Scheme::HTTPS);
767                assert_eq!(item.method, Method::POST);
768                assert_eq!(item.path, "/post");
769                assert_eq!(item.extension, None);
770                assert!(item.request.0.base64);
771                assert!(item.request.1.starts_with(b"UE9TVC"));
772                assert!(item.request.1.ends_with(b"0Ke30="));
773                assert_eq!(item.status, 200);
774                assert_eq!(item.response_length, 614);
775                assert_eq!(item.mimetype, "JSON");
776                assert!(item.response.0.base64);
777                assert!(item.response.1.starts_with(b"SFRUUC"));
778                assert!(item.response.1.ends_with(b"IKfQo="));
779                assert_eq!(item.comment, None);
780            }
781            Some(Err(err)) => {
782                eprintln!("{err}");
783                panic!("{err}");
784            }
785            None => panic!(),
786        }
787
788        assert!(items.next().is_none());
789
790        Ok(())
791    }
792
793    #[test]
794    fn test_v_2021_3_2() -> Result<(), Box<dyn error::Error>> {
795        let file =
796            File::open("tests/http_history_files/burpsuite_community_v2021.3.2.xml").unwrap();
797        let buf_reader = BufReader::new(file);
798        let mut items = Items::from_reader(buf_reader)?;
799
800        assert_eq!(items.attr.burp_version, "2021.3.2");
801        assert_eq!(
802            items.attr.export_time,
803            NaiveDate::from_ymd_opt(2021, 3, 31)
804                .expect("")
805                .and_hms_opt(13, 7, 44)
806                .expect("")
807        );
808
809        match items.next() {
810            Some(Ok(item)) => {
811                assert_eq!(
812                    item.time,
813                    NaiveDate::from_ymd_opt(2021, 3, 31)
814                        .expect("")
815                        .and_hms_opt(13, 6, 6)
816                        .expect("")
817                );
818                assert_eq!(item.url, "http://httpbin.org/get?foo=bar");
819                assert_eq!(item.host.0.ip, b"34.199.75.4");
820                assert_eq!(item.host.1, "httpbin.org");
821                assert_eq!(item.port, 80);
822                assert_eq!(item.protocol, Scheme::HTTP);
823                assert_eq!(item.method, Method::GET);
824                assert_eq!(item.path, "/get?foo=bar");
825                assert_eq!(item.extension, None);
826                assert!(item.request.0.base64);
827                assert!(item.request.1.starts_with(b"R0VUIC"));
828                assert!(item.request.1.ends_with(b"UNCg0K"));
829                assert_eq!(item.status, 200);
830                assert_eq!(item.response_length, 508);
831                assert_eq!(item.mimetype, "JSON");
832                assert!(item.response.0.base64);
833                assert!(item.response.1.starts_with(b"SFRUUC"));
834                assert!(item.response.1.ends_with(b"p9Cg=="));
835                assert_eq!(item.comment, None);
836            }
837            Some(Err(err)) => {
838                eprintln!("{err}");
839                panic!("{err}");
840            }
841            None => panic!(),
842        }
843
844        match items.next() {
845            Some(Ok(item)) => {
846                assert_eq!(
847                    item.time,
848                    NaiveDate::from_ymd_opt(2021, 3, 31)
849                        .expect("")
850                        .and_hms_opt(13, 6, 13)
851                        .expect("")
852                );
853                assert_eq!(item.url, "https://httpbin.org/post");
854                assert_eq!(item.host.0.ip, b"34.199.75.4");
855                assert_eq!(item.host.1, "httpbin.org");
856                assert_eq!(item.port, 443);
857                assert_eq!(item.protocol, Scheme::HTTPS);
858                assert_eq!(item.method, Method::POST);
859                assert_eq!(item.path, "/post");
860                assert_eq!(item.extension, None);
861                assert!(item.request.0.base64);
862                assert!(item.request.1.starts_with(b"UE9TVC"));
863                assert!(item.request.1.ends_with(b"oNCnt9"));
864                assert_eq!(item.status, 200);
865                assert_eq!(item.response_length, 593);
866                assert_eq!(item.mimetype, "JSON");
867                assert!(item.response.0.base64);
868                assert!(item.response.1.starts_with(b"SFRUUC"));
869                assert!(item.response.1.ends_with(b"IKfQo="));
870                assert_eq!(item.comment, None);
871            }
872            Some(Err(err)) => {
873                eprintln!("{err}");
874                panic!("{err}");
875            }
876            None => panic!(),
877        }
878
879        assert!(items.next().is_none());
880
881        Ok(())
882    }
883}