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
20pub 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 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 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}