1use crate::parse::keyword::Keyword;
8use crate::types::misc::FromBytes;
9use crate::util::PeekableIterator;
10use crate::{Error, NetdocErrorKind as EK, Pos, Result};
11use base64ct::{Base64, Encoding};
12use itertools::Itertools;
13use std::cell::{Ref, RefCell};
14use std::iter::Peekable;
15use std::str::FromStr;
16use tor_error::internal;
17
18pub(crate) mod object {
20 pub(crate) const BEGIN_STR: &str = "-----BEGIN ";
22 pub(crate) const END_STR: &str = "-----END ";
24 pub(crate) const TAG_END: &str = "-----";
26 pub(crate) const BASE64_PEM_MAX_LINE: usize = 64;
28}
29
30pub(crate) fn is_sp(c: char) -> bool {
33 c == ' ' || c == '\t'
34}
35fn b64check(s: &str) -> Result<()> {
40 for b in s.bytes() {
41 match b {
42 b'=' => (),
43 b'a'..=b'z' => (),
44 b'A'..=b'Z' => (),
45 b'0'..=b'9' => (),
46 b'/' | b'+' => (),
47 _ => {
48 return Err(EK::BadObjectBase64.at_pos(Pos::at(s)));
49 }
50 };
51 }
52 Ok(())
53}
54
55#[derive(Clone, Copy, Debug)]
63pub(crate) struct Object<'a> {
64 tag: &'a str,
66 data: &'a str,
69 endline: &'a str,
73}
74
75#[derive(Clone, Debug)]
83pub(crate) struct Item<'a, K: Keyword> {
84 kwd: K,
86 kwd_str: &'a str,
89 args: &'a str,
93 split_args: RefCell<Option<Vec<&'a str>>>,
96 object: Option<Object<'a>>,
99}
100
101#[derive(Debug)]
105struct NetDocReaderBase<'a, K: Keyword> {
106 s: &'a str,
108 off: usize,
110 _k: std::marker::PhantomData<K>,
112}
113
114impl<'a, K: Keyword> NetDocReaderBase<'a, K> {
115 fn new(s: &'a str) -> Result<Self> {
117 Ok(NetDocReaderBase {
118 s: validate_utf_8_rules(s)?,
119 off: 0,
120 _k: std::marker::PhantomData,
121 })
122 }
123 fn pos(&self, pos: usize) -> Pos {
125 Pos::from_offset(self.s, pos)
126 }
127 fn advance(&mut self, n: usize) -> Result<()> {
132 if n > self.remaining() {
133 return Err(
134 Error::from(internal!("tried to advance past end of document"))
135 .at_pos(Pos::from_offset(self.s, self.off)),
136 );
137 }
138 self.off += n;
139 Ok(())
140 }
141 fn remaining(&self) -> usize {
143 self.s.len() - self.off
144 }
145
146 #[allow(clippy::string_slice)] fn starts_with(&self, s: &str) -> bool {
149 self.s[self.off..].starts_with(s)
150 }
151 #[allow(clippy::string_slice)] fn line(&mut self) -> Result<&'a str> {
155 let remainder = &self.s[self.off..];
156 if let Some(nl_pos) = remainder.find('\n') {
157 self.advance(nl_pos + 1)?;
158 let line = &remainder[..nl_pos];
159
160 Ok(line)
163 } else {
164 self.advance(remainder.len())?; Err(EK::TruncatedLine.at_pos(self.pos(self.s.len())))
166 }
167 }
168
169 #[allow(clippy::string_slice)] fn kwdline(&mut self) -> Result<(&'a str, &'a str)> {
174 let pos = self.off;
175 let line = self.line()?;
176 if line.is_empty() {
177 return Err(EK::EmptyLine.at_pos(self.pos(pos)));
178 }
179 let (line, anno_ok) = if let Some(rem) = line.strip_prefix("opt ") {
180 (rem, false)
181 } else {
182 (line, true)
183 };
184 let mut parts_iter = line.splitn(2, [' ', '\t']);
185 let kwd = match parts_iter.next() {
186 Some(k) => k,
187 None => return Err(EK::MissingKeyword.at_pos(self.pos(pos))),
190 };
191 if !keyword_ok(kwd, anno_ok) {
192 return Err(EK::BadKeyword.at_pos(self.pos(pos)));
193 }
194 let args = match parts_iter.next() {
197 Some(a) => a,
198 None => &kwd[kwd.len()..],
200 };
201 Ok((kwd, args))
202 }
203
204 #[allow(clippy::string_slice)] fn object(&mut self) -> Result<Option<Object<'a>>> {
211 use object::*;
212
213 let pos = self.off;
214 if !self.starts_with(BEGIN_STR) {
215 return Ok(None);
216 }
217 let line = self.line()?;
218 if !line.ends_with(TAG_END) {
219 return Err(EK::BadObjectBeginTag.at_pos(self.pos(pos)));
220 }
221 let tag = &line[BEGIN_STR.len()..(line.len() - TAG_END.len())];
222 if !tag_keywords_ok(tag) {
223 return Err(EK::BadObjectBeginTag.at_pos(self.pos(pos)));
224 }
225 let datapos = self.off;
226 let (endlinepos, endline) = loop {
227 let p = self.off;
228 let line = self.line()?;
229 if line.starts_with(END_STR) {
230 break (p, line);
231 }
232 b64check(line).map_err(|e| e.within(self.s))?;
237 };
238 let data = &self.s[datapos..endlinepos];
239 if !endline.ends_with(TAG_END) {
240 return Err(EK::BadObjectEndTag.at_pos(self.pos(endlinepos)));
241 }
242 let endtag = &endline[END_STR.len()..(endline.len() - TAG_END.len())];
243 if endtag != tag {
244 return Err(EK::BadObjectMismatchedTag.at_pos(self.pos(endlinepos)));
245 }
246 Ok(Some(Object { tag, data, endline }))
247 }
248
249 fn item(&mut self) -> Result<Option<Item<'a, K>>> {
257 if self.remaining() == 0 {
258 return Ok(None);
259 }
260 let (kwd_str, args) = self.kwdline()?;
261 let object = self.object()?;
262 let split_args = RefCell::new(None);
263 let kwd = K::from_str(kwd_str);
264 Ok(Some(Item {
265 kwd,
266 kwd_str,
267 args,
268 split_args,
269 object,
270 }))
271 }
272}
273
274#[allow(clippy::string_slice)] fn keyword_ok(mut s: &str, anno_ok: bool) -> bool {
279 fn kwd_char_ok(c: char) -> bool {
281 matches!(c,'A'..='Z' | 'a'..='z' |'0'..='9' | '-')
282 }
283
284 if s.is_empty() {
285 return false;
286 }
287 if anno_ok && s.starts_with('@') {
288 s = &s[1..];
289 }
290 if s.starts_with('-') {
291 return false;
292 }
293 s.chars().all(kwd_char_ok)
294}
295
296pub(crate) fn tag_keywords_ok(s: &str) -> bool {
298 s.split(' ').all(|w| keyword_ok(w, false))
299}
300
301impl<'a, K: Keyword> Iterator for NetDocReaderBase<'a, K> {
303 type Item = Result<Item<'a, K>>;
304 fn next(&mut self) -> Option<Self::Item> {
305 self.item().transpose()
306 }
307}
308
309pub(crate) fn base64_decode_multiline(s: &str) -> std::result::Result<Vec<u8>, base64ct::Error> {
312 let mut s = s.to_string();
314 s.retain(|ch| ch != '\n');
315 let v = Base64::decode_vec(&s)?;
316 Ok(v)
317}
318
319impl<'a, K: Keyword> Item<'a, K> {
320 pub(crate) fn kwd(&self) -> K {
322 self.kwd
323 }
324 pub(crate) fn kwd_str(&self) -> &'a str {
326 self.kwd_str
327 }
328 pub(crate) fn has_kwd_in(&self, ks: &[K]) -> bool {
330 ks.contains(&self.kwd)
331 }
332 pub(crate) fn args_as_str(&self) -> &'a str {
334 self.args
335 }
336 fn args_as_vec(&self) -> Ref<'_, Vec<&'a str>> {
338 if self.split_args.borrow().is_none() {
341 self.split_args.replace(Some(self.args().collect()));
342 }
343 Ref::map(self.split_args.borrow(), |opt| match opt {
344 Some(v) => v,
345 None => panic!(),
346 })
347 }
348 pub(crate) fn args(&self) -> impl Iterator<Item = &'a str> + use<'a, K> {
350 self.args.split(is_sp).filter(|s| !s.is_empty())
351 }
352 pub(crate) fn arg(&self, idx: usize) -> Option<&'a str> {
354 self.args_as_vec().get(idx).copied()
355 }
356 pub(crate) fn required_arg(&self, idx: usize) -> Result<&'a str> {
358 self.arg(idx)
359 .ok_or_else(|| EK::MissingArgument.at_pos(Pos::at(self.args)))
360 }
361 pub(crate) fn parse_optional_arg<V: FromStr>(&self, idx: usize) -> Result<Option<V>>
366 where
367 Error: From<V::Err>,
368 {
369 match self.arg(idx) {
370 None => Ok(None),
371 Some(s) => match s.parse() {
372 Ok(r) => Ok(Some(r)),
373 Err(e) => {
374 let e: Error = e.into();
375 Err(e.or_at_pos(Pos::at(s)))
376 }
377 },
378 }
379 }
380 pub(crate) fn parse_arg<V: FromStr>(&self, idx: usize) -> Result<V>
385 where
386 Error: From<V::Err>,
387 {
388 match self.parse_optional_arg(idx) {
389 Ok(Some(v)) => Ok(v),
390 Ok(None) => Err(EK::MissingArgument.at_pos(self.arg_pos(idx))),
391 Err(e) => Err(e),
392 }
393 }
394 pub(crate) fn n_args(&self) -> usize {
396 self.args().count()
397 }
398 pub(crate) fn has_obj(&self) -> bool {
400 self.object.is_some()
401 }
402 pub(crate) fn obj_tag(&self) -> Option<&'a str> {
404 self.object.map(|o| o.tag)
405 }
406 pub(crate) fn obj_raw(&self) -> Result<Option<(&'a str, Vec<u8>)>> {
410 match self.object {
411 None => Ok(None),
412 Some(obj) => {
413 let decoded = base64_decode_multiline(obj.data)
414 .map_err(|_| EK::BadObjectBase64.at_pos(Pos::at(obj.data)))?;
415 Ok(Some((obj.tag, decoded)))
416 }
417 }
418 }
419 pub(crate) fn obj(&self, want_tag: &str) -> Result<Vec<u8>> {
422 match self.obj_raw()? {
423 None => Err(EK::MissingObject
424 .with_msg(self.kwd.to_str())
425 .at_pos(self.end_pos())),
426 Some((tag, decoded)) => {
427 if tag != want_tag {
428 Err(EK::WrongObject.at_pos(Pos::at(tag)))
429 } else {
430 Ok(decoded)
431 }
432 }
433 }
434 }
435 pub(crate) fn parse_obj<V: FromBytes>(&self, want_tag: &str) -> Result<V> {
438 let bytes = self.obj(want_tag)?;
439 #[allow(clippy::unwrap_used)]
442 let p = Pos::at(self.object.unwrap().data);
443 V::from_vec(bytes, p).map_err(|e| e.at_pos(p))
444 }
445 pub(crate) fn pos(&self) -> Pos {
450 Pos::at(self.kwd_str)
451 }
452 pub(crate) fn offset_in(&self, s: &str) -> Option<usize> {
456 crate::util::str::str_offset(s, self.kwd_str)
457 }
458 pub(crate) fn arg_pos(&self, n: usize) -> Pos {
463 let args = self.args_as_vec();
464 if n < args.len() {
465 Pos::at(args[n])
466 } else {
467 self.last_arg_end_pos()
468 }
469 }
470 fn last_arg_end_pos(&self) -> Pos {
473 Pos::at_end_of(self.args)
474 }
475 pub(crate) fn end_pos(&self) -> Pos {
478 match self.object {
479 Some(o) => Pos::at_end_of(o.endline),
480 None => self.last_arg_end_pos(),
481 }
482 }
483 pub(crate) fn offset_after(&self, s: &str) -> Option<usize> {
486 self.end_pos().offset_within(s).map(|nl_pos| nl_pos + 1)
487 }
488
489 #[allow(dead_code)] pub(crate) fn text_within<'b>(&self, s: &'b str) -> Option<&'b str> {
493 let start = self.pos().offset_within(s)?;
494 let end = self.end_pos().offset_within(s)?;
495 s.get(start..=end)
496 }
497}
498
499pub(crate) struct MaybeItem<'a, 'b, K: Keyword>(Option<&'a Item<'b, K>>);
503
504impl<'a, 'b, K: Keyword> MaybeItem<'a, 'b, K> {
506 pub(crate) fn pos(&self) -> Pos {
508 match self.0 {
509 Some(item) => item.pos(),
510 None => Pos::None,
511 }
512 }
513 pub(crate) fn from_option(opt: Option<&'a Item<'b, K>>) -> Self {
515 MaybeItem(opt)
516 }
517
518 pub(crate) fn parse_arg<V: FromStr>(&self, idx: usize) -> Result<Option<V>>
522 where
523 Error: From<V::Err>,
524 {
525 match self.0 {
526 Some(item) => match item.parse_arg(idx) {
527 Ok(v) => Ok(Some(v)),
528 Err(e) => Err(e.or_at_pos(self.pos())),
529 },
530 None => Ok(None),
531 }
532 }
533 pub(crate) fn args_as_str(&self) -> Option<&str> {
535 self.0.map(|item| item.args_as_str())
536 }
537 pub(crate) fn parse_args_as_str<V: FromStr>(&self) -> Result<Option<V>>
540 where
541 Error: From<V::Err>,
542 {
543 match self.0 {
544 Some(item) => match item.args_as_str().parse::<V>() {
545 Ok(v) => Ok(Some(v)),
546 Err(e) => {
547 let e: Error = e.into();
548 Err(e.or_at_pos(self.pos()))
549 }
550 },
551 None => Ok(None),
552 }
553 }
554}
555
556pub(crate) trait ItemResult<K: Keyword> {
559 fn is_ok_with_annotation(&self) -> bool;
561 fn is_ok_with_non_annotation(&self) -> bool;
563 fn is_ok_with_kwd(&self, k: K) -> bool {
565 self.is_ok_with_kwd_in(&[k])
566 }
567 fn is_ok_with_kwd_in(&self, ks: &[K]) -> bool;
569 fn is_ok_with_kwd_not_in(&self, ks: &[K]) -> bool;
571 fn is_empty_line(&self) -> bool;
573}
574
575impl<'a, K: Keyword> ItemResult<K> for Result<Item<'a, K>> {
576 fn is_ok_with_annotation(&self) -> bool {
577 match self {
578 Ok(item) => item.kwd().is_annotation(),
579 Err(_) => false,
580 }
581 }
582 fn is_ok_with_non_annotation(&self) -> bool {
583 match self {
584 Ok(item) => !item.kwd().is_annotation(),
585 Err(_) => false,
586 }
587 }
588 fn is_ok_with_kwd_in(&self, ks: &[K]) -> bool {
589 match self {
590 Ok(item) => item.has_kwd_in(ks),
591 Err(_) => false,
592 }
593 }
594 fn is_ok_with_kwd_not_in(&self, ks: &[K]) -> bool {
595 match self {
596 Ok(item) => !item.has_kwd_in(ks),
597 Err(_) => false,
598 }
599 }
600 fn is_empty_line(&self) -> bool {
601 matches!(
602 self,
603 Err(e) if e.netdoc_error_kind() == crate::err::NetdocErrorKind::EmptyLine
604 )
605 }
606}
607
608#[derive(Debug)]
612pub(crate) struct NetDocReader<'a, K: Keyword> {
613 s: &'a str,
617 tokens: Peekable<NetDocReaderBase<'a, K>>,
619}
620
621impl<'a, K: Keyword> NetDocReader<'a, K> {
622 pub(crate) fn new(s: &'a str) -> Result<Self> {
624 Ok(NetDocReader {
625 s,
626 tokens: NetDocReaderBase::new(s)?.peekable(),
627 })
628 }
629 pub(crate) fn str(&self) -> &'a str {
631 self.s
632 }
633 pub(crate) fn pause_at<'f, 'r, F>(
637 &mut self,
638 mut f: F,
639 ) -> itertools::PeekingTakeWhile<
640 '_,
641 Self,
642 impl FnMut(&Result<Item<'a, K>>) -> bool + 'f + use<'a, 'f, F, K>,
643 >
644 where
645 'f: 'r,
646 F: FnMut(&Result<Item<'a, K>>) -> bool + 'f,
647 K: 'f,
648 {
649 self.peeking_take_while(move |i| !f(i))
650 }
651
652 #[allow(clippy::wrong_self_convention)]
656 #[allow(dead_code)] pub(crate) fn is_exhausted(&mut self) -> bool {
658 self.peek().is_none()
659 }
660
661 pub(crate) fn should_be_exhausted(&mut self) -> Result<()> {
663 match self.peek() {
664 None => Ok(()),
665 Some(Ok(t)) => Err(EK::UnexpectedToken
666 .with_msg(t.kwd().to_str())
667 .at_pos(t.pos())),
668 Some(Err(e)) => Err(e.clone()),
669 }
670 }
671
672 pub(crate) fn should_be_exhausted_but_for_empty_lines(&mut self) -> Result<()> {
677 use crate::err::NetdocErrorKind as K;
678 while let Some(Err(e)) = self.peek() {
679 if e.netdoc_error_kind() == K::EmptyLine {
680 let _ignore = self.next();
681 } else {
682 break;
683 }
684 }
685 self.should_be_exhausted()
686 }
687
688 pub(crate) fn pos(&mut self) -> Pos {
691 match self.tokens.peek() {
692 Some(Ok(tok)) => tok.pos(),
693 Some(Err(e)) => e.pos(),
694 None => Pos::at_end_of(self.s),
695 }
696 }
697}
698
699impl<'a, K: Keyword> Iterator for NetDocReader<'a, K> {
700 type Item = Result<Item<'a, K>>;
701 fn next(&mut self) -> Option<Self::Item> {
702 self.tokens.next()
703 }
704}
705
706impl<'a, K: Keyword> PeekableIterator for NetDocReader<'a, K> {
707 fn peek(&mut self) -> Option<&Self::Item> {
708 self.tokens.peek()
709 }
710}
711
712impl<'a, K: Keyword> itertools::PeekingNext for NetDocReader<'a, K> {
713 fn peeking_next<F>(&mut self, f: F) -> Option<Self::Item>
714 where
715 F: FnOnce(&Self::Item) -> bool,
716 {
717 if f(self.peek()?) { self.next() } else { None }
718 }
719}
720
721fn validate_utf_8_rules(s: &str) -> Result<&str> {
729 let first_char = s.chars().next();
731 if [Some('\u{feff}'), Some('\u{fffe}')].contains(&first_char) {
732 return Err(EK::BomMarkerFound.at_pos(Pos::at(s)));
733 }
734 if let Some(nul_pos) = memchr::memchr(0, s.as_bytes()) {
736 return Err(EK::NulFound.at_pos(Pos::from_byte(nul_pos)));
737 }
738 Ok(s)
739}
740
741#[cfg(test)]
742mod test {
743 #![allow(clippy::bool_assert_comparison)]
745 #![allow(clippy::clone_on_copy)]
746 #![allow(clippy::dbg_macro)]
747 #![allow(clippy::mixed_attributes_style)]
748 #![allow(clippy::print_stderr)]
749 #![allow(clippy::print_stdout)]
750 #![allow(clippy::single_char_pattern)]
751 #![allow(clippy::unwrap_used)]
752 #![allow(clippy::unchecked_time_subtraction)]
753 #![allow(clippy::useless_vec)]
754 #![allow(clippy::needless_pass_by_value)]
755 #![allow(clippy::string_slice)] #![allow(clippy::cognitive_complexity)]
758 use super::*;
759 use crate::parse::macros::test::Fruit;
760 use crate::{NetdocErrorKind as EK, Pos, Result};
761
762 #[test]
763 fn read_simple() {
764 use Fruit::*;
765
766 let s = "\
767@tasty very much so
768opt apple 77
769banana 60
770cherry 6
771-----BEGIN CHERRY SYNOPSIS-----
7728J+NkvCfjZLwn42S8J+NkvCfjZLwn42S
773-----END CHERRY SYNOPSIS-----
774plum hello there
775";
776 let mut r: NetDocReader<'_, Fruit> = NetDocReader::new(s).unwrap();
777
778 assert_eq!(r.str(), s);
779 assert!(r.should_be_exhausted().is_err()); let toks: Result<Vec<_>> = r.by_ref().collect();
782 assert!(r.should_be_exhausted().is_ok());
783
784 let toks = toks.unwrap();
785 assert_eq!(toks.len(), 5);
786 assert_eq!(toks[0].kwd(), ANN_TASTY);
787 assert_eq!(toks[0].n_args(), 3);
788 assert_eq!(toks[0].args_as_str(), "very much so");
789 assert_eq!(toks[0].arg(1), Some("much"));
790 {
791 let a: Vec<_> = toks[0].args().collect();
792 assert_eq!(a, vec!["very", "much", "so"]);
793 }
794 assert!(toks[0].parse_arg::<usize>(0).is_err());
795 assert!(toks[0].parse_arg::<usize>(10).is_err());
796 assert!(!toks[0].has_obj());
797 assert_eq!(toks[0].obj_tag(), None);
798
799 assert_eq!(toks[2].pos().within(s), Pos::from_line(3, 1));
800 assert_eq!(toks[2].arg_pos(0).within(s), Pos::from_line(3, 8));
801 assert_eq!(toks[2].last_arg_end_pos().within(s), Pos::from_line(3, 10));
802 assert_eq!(toks[2].end_pos().within(s), Pos::from_line(3, 10));
803
804 assert_eq!(toks[3].kwd(), STONEFRUIT);
805 assert_eq!(toks[3].kwd_str(), "cherry"); assert_eq!(toks[3].n_args(), 1);
807 assert_eq!(toks[3].required_arg(0), Ok("6"));
808 assert_eq!(toks[3].parse_arg::<usize>(0), Ok(6));
809 assert_eq!(toks[3].parse_optional_arg::<usize>(0), Ok(Some(6)));
810 assert_eq!(toks[3].parse_optional_arg::<usize>(3), Ok(None));
811 assert!(toks[3].has_obj());
812 assert_eq!(toks[3].obj_tag(), Some("CHERRY SYNOPSIS"));
813 assert_eq!(
814 &toks[3].obj("CHERRY SYNOPSIS").unwrap()[..],
815 "🍒🍒🍒🍒🍒🍒".as_bytes()
816 );
817 assert!(toks[3].obj("PLUOT SYNOPSIS").is_err());
818 assert_eq!(toks[3].end_pos().within(s), Pos::from_line(7, 30));
820 }
821
822 #[test]
823 fn test_badtoks() {
824 use Fruit::*;
825
826 let s = "\
827-foobar 9090
828apple 3.14159
829$hello
830unrecognized 127.0.0.1 foo
831plum
832-----BEGIN WHATEVER-----
8338J+NkvCfjZLwn42S8J+NkvCfjZLwn42S
834-----END SOMETHING ELSE-----
835orange
836orange
837-----BEGIN WHATEVER-----
838not! base64!
839-----END WHATEVER-----
840guava paste
841opt @annotation
842orange
843-----BEGIN LOBSTER
8448J+NkvCfjZLwn42S8J+NkvCfjZLwn42S
845-----END SOMETHING ELSE-----
846orange
847-----BEGIN !!!!!!-----
8488J+NkvCfjZLwn42S8J+NkvCfjZLwn42S
849-----END !!!!!!-----
850cherry
851-----BEGIN CHERRY SYNOPSIS-----
8528J+NkvCfjZLwn42S8J+NkvCfjZLwn42S
853-----END CHERRY SYNOPSIS
854
855truncated line";
856
857 let r: NetDocReader<'_, Fruit> = NetDocReader::new(s).unwrap();
858 let toks: Vec<_> = r.collect();
859
860 assert!(toks[0].is_err());
861 assert_eq!(
862 toks[0].as_ref().err().unwrap(),
863 &EK::BadKeyword.at_pos(Pos::from_line(1, 1))
864 );
865
866 assert!(toks[1].is_ok());
867 assert!(toks[1].is_ok_with_non_annotation());
868 assert!(!toks[1].is_ok_with_annotation());
869 assert!(toks[1].is_ok_with_kwd_in(&[APPLE, ORANGE]));
870 assert!(toks[1].is_ok_with_kwd_not_in(&[ORANGE, UNRECOGNIZED]));
871 let t = toks[1].as_ref().unwrap();
872 assert_eq!(t.kwd(), APPLE);
873 assert_eq!(t.arg(0), Some("3.14159"));
874
875 assert!(toks[2].is_err());
876 assert!(!toks[2].is_ok_with_non_annotation());
877 assert!(!toks[2].is_ok_with_annotation());
878 assert!(!toks[2].is_ok_with_kwd_in(&[APPLE, ORANGE]));
879 assert!(!toks[2].is_ok_with_kwd_not_in(&[ORANGE, UNRECOGNIZED]));
880 assert_eq!(
881 toks[2].as_ref().err().unwrap(),
882 &EK::BadKeyword.at_pos(Pos::from_line(3, 1))
883 );
884
885 assert!(toks[3].is_ok());
886 let t = toks[3].as_ref().unwrap();
887 assert_eq!(t.kwd(), UNRECOGNIZED);
888 assert_eq!(t.arg(1), Some("foo"));
889
890 assert!(toks[4].is_err());
891 assert_eq!(
892 toks[4].as_ref().err().unwrap(),
893 &EK::BadObjectMismatchedTag.at_pos(Pos::from_line(8, 1))
894 );
895
896 assert!(toks[5].is_ok());
897 let t = toks[5].as_ref().unwrap();
898 assert_eq!(t.kwd(), ORANGE);
899 assert_eq!(t.args_as_str(), "");
900
901 assert!(toks[6].is_err());
904 assert_eq!(
905 toks[6].as_ref().err().unwrap(),
906 &EK::BadObjectBase64.at_pos(Pos::from_line(12, 1))
907 );
908
909 assert!(toks[7].is_err());
910 assert_eq!(
911 toks[7].as_ref().err().unwrap(),
912 &EK::BadKeyword.at_pos(Pos::from_line(13, 1))
913 );
914
915 assert!(toks[8].is_ok());
916 let t = toks[8].as_ref().unwrap();
917 assert_eq!(t.kwd(), GUAVA);
918
919 assert!(toks[9].is_err());
921 assert_eq!(
922 toks[9].as_ref().err().unwrap(),
923 &EK::BadKeyword.at_pos(Pos::from_line(15, 1))
924 );
925
926 assert!(toks[10].is_err());
928 assert_eq!(
929 toks[10].as_ref().err().unwrap(),
930 &EK::BadObjectBeginTag.at_pos(Pos::from_line(17, 1))
931 );
932 assert!(toks[11].is_err());
933 assert_eq!(
934 toks[11].as_ref().err().unwrap(),
935 &EK::BadKeyword.at_pos(Pos::from_line(18, 1))
936 );
937 assert!(toks[12].is_err());
938 assert_eq!(
939 toks[12].as_ref().err().unwrap(),
940 &EK::BadKeyword.at_pos(Pos::from_line(19, 1))
941 );
942
943 assert!(toks[13].is_err());
945 assert_eq!(
946 toks[13].as_ref().err().unwrap(),
947 &EK::BadObjectBeginTag.at_pos(Pos::from_line(21, 1))
948 );
949 assert!(toks[14].is_err());
950 assert_eq!(
951 toks[14].as_ref().err().unwrap(),
952 &EK::BadKeyword.at_pos(Pos::from_line(22, 1))
953 );
954 assert!(toks[15].is_err());
955 assert_eq!(
956 toks[15].as_ref().err().unwrap(),
957 &EK::BadKeyword.at_pos(Pos::from_line(23, 1))
958 );
959
960 assert!(toks[16].is_err());
962 assert_eq!(
963 toks[16].as_ref().err().unwrap(),
964 &EK::BadObjectEndTag.at_pos(Pos::from_line(27, 1))
965 );
966
967 assert!(toks[17].is_err());
968 assert_eq!(
969 toks[17].as_ref().err().unwrap(),
970 &EK::EmptyLine.at_pos(Pos::from_line(28, 1))
971 );
972
973 assert!(toks[18].is_err());
974 assert_eq!(
975 toks[18].as_ref().err().unwrap(),
976 &EK::TruncatedLine.at_pos(Pos::from_line(29, 15))
977 );
978 }
979
980 #[test]
981 fn test_leading_space_forbidden() {
982 let s = " guava space\n";
988 let r: NetDocReader<'_, Fruit> = NetDocReader::new(s).unwrap();
989 let toks: Vec<_> = r.collect();
990
991 assert_eq!(
993 toks[0].as_ref().err().unwrap(),
994 &EK::BadKeyword.at_pos(Pos::from_line(1, 1))
995 );
996
997 let s = "cherry
999-----BEGIN WHATEVER-----
10008J+NkvCfjZLwn42S8J+NkvCfjZLwn42S
1001-----END WHATEVER-----
1002";
1003
1004 let orig_lines = s
1005 .split_terminator('\n')
1006 .map(str::to_string)
1007 .collect::<Vec<_>>();
1008 assert_eq!(orig_lines.len(), 4);
1009 let expected_kinds = [
1010 EK::BadKeyword,
1011 EK::BadKeyword,
1012 EK::BadObjectBase64,
1013 EK::BadObjectBase64,
1014 ];
1015 for pos in 0..orig_lines.len() {
1016 let mut lines = orig_lines.clone();
1017 lines[pos] = format!(" {}", lines[pos]);
1018 let joined = format!("{}\n", lines.join("\n"));
1019
1020 let r: NetDocReader<'_, Fruit> = NetDocReader::new(&joined).unwrap();
1021 let toks: Result<Vec<_>> = r.collect();
1022 assert_eq!(toks.unwrap_err().netdoc_error_kind(), expected_kinds[pos]);
1023 }
1024 }
1025
1026 #[test]
1027 fn test_validate_strings() {
1028 use validate_utf_8_rules as v;
1029 assert_eq!(v(""), Ok(""));
1030 assert_eq!(v("hello world"), Ok("hello world"));
1031 for s in ["\u{feff}", "\u{feff}hello world", "\u{fffe}hello world"] {
1035 let e = v(s).unwrap_err();
1036 assert_eq!(e.netdoc_error_kind(), EK::BomMarkerFound);
1037 assert_eq!(e.pos().offset_within(s), Some(0));
1038 }
1039
1040 for s in [
1041 "\0hello world",
1042 "\0",
1043 "\0\0\0",
1044 "hello\0world",
1045 "hello world\0",
1046 ] {
1047 let e = v(s).unwrap_err();
1048 assert_eq!(e.netdoc_error_kind(), EK::NulFound);
1049 let nul_pos = e.pos().offset_within(s).unwrap();
1050 assert_eq!(s.as_bytes()[nul_pos], 0);
1051 }
1052 }
1053
1054 fn single_fruit(s: &str) -> Item<'_, Fruit> {
1055 NetDocReader::<Fruit>::new(s)
1056 .unwrap()
1057 .next()
1058 .unwrap()
1059 .unwrap()
1060 }
1061
1062 #[test]
1063 fn end_of_item() {
1064 let s = "guava friends 123 \n";
1065 let item = single_fruit(s);
1066 assert_eq!(
1067 item.end_pos().within(s),
1068 Pos::from_byte(s.find('\n').unwrap()).within(s)
1069 );
1070
1071 let s = "cherry
1072-----BEGIN WHATEVER-----
10738J+NkvCfjZLwn42S8J+NkvCfjZLwn42S
1074-----END WHATEVER-----\n";
1075 let item = single_fruit(s);
1076 dbg!(&item);
1077 assert_eq!(
1078 item.end_pos().within(s),
1079 Pos::from_byte(s.rfind('\n').unwrap()).within(s)
1080 );
1081 }
1082}