1use std::{
2 cmp::{max, min},
3 convert::Infallible,
4 io::{self, Read, Seek, SeekFrom},
5 mem,
6 ops::ControlFlow,
7};
8
9use shakmaty::{
10 CastlingSide, Color, KnownOutcome, Outcome,
11 san::{San, SanPlus, Suffix},
12};
13
14use crate::{RawTag, Skip, Visitor, buffer::Buffer, comment::RawComment, nag::Nag};
15
16#[derive(Debug, Clone)]
18pub struct ReaderBuilder<R> {
19 reader: R,
20 tag_line_bytes: usize,
21 movetext_token_bytes: usize,
22}
23
24impl<R: Read> ReaderBuilder<R> {
25 pub fn new(reader: R) -> Self {
28 ReaderBuilder {
29 reader,
30 tag_line_bytes: 255,
31 movetext_token_bytes: 255,
32 }
33 }
34
35 pub fn set_supported_tag_line_length(mut self, bytes: usize) -> Self {
39 self.tag_line_bytes = max(255, bytes);
40 self
41 }
42
43 pub fn set_supported_comment_length(mut self, bytes: usize) -> Self {
51 self.movetext_token_bytes = max(255, bytes) + 2; self
53 }
54
55 pub fn finish(self) -> Reader<R> {
57 Reader {
58 reader: self.reader,
59 tag_line_bytes: self.tag_line_bytes,
60 movetext_token_bytes: self.movetext_token_bytes,
61 buffer: Buffer::with_capacity(max(
62 1 << 14,
63 max(self.tag_line_bytes, self.movetext_token_bytes).next_power_of_two() * 2,
64 )),
65 pending_skip_tags: false,
66 pending_skip_movetext: false,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
77pub struct Reader<R> {
78 buffer: Buffer,
79 reader: R,
80 tag_line_bytes: usize,
81 movetext_token_bytes: usize,
82 pending_skip_tags: bool,
83 pending_skip_movetext: bool,
84}
85
86impl<R: Read> Reader<R> {
87 pub fn new(reader: R) -> Reader<R> {
89 ReaderBuilder::new(reader).finish()
90 }
91
92 pub fn build(reader: R) -> ReaderBuilder<R> {
105 ReaderBuilder::new(reader)
106 }
107
108 fn skip_bom(&mut self) -> io::Result<()> {
109 if self
110 .buffer
111 .ensure_bytes(3, &mut self.reader)?
112 .starts_with(b"\xef\xbb\xbf")
113 {
114 self.buffer.consume(3);
115 }
116 Ok(())
117 }
118
119 fn skip_until(&mut self, needle: u8) -> io::Result<()> {
120 while !self.buffer.ensure_bytes(1, &mut self.reader)?.is_empty() {
121 if let Some(pos) = memchr::memchr(needle, self.buffer.data()) {
122 self.buffer.consume(pos);
123 return Ok(());
124 } else {
125 self.buffer.clear();
126 }
127 }
128 Ok(())
129 }
130
131 fn skip_until_after(&mut self, needle: u8) -> io::Result<()> {
132 while !self.buffer.ensure_bytes(1, &mut self.reader)?.is_empty() {
133 if let Some(pos) = memchr::memchr(needle, self.buffer.data()) {
134 self.buffer.consume(pos + 1);
135 return Ok(());
136 } else {
137 self.buffer.clear();
138 }
139 }
140 Ok(())
141 }
142
143 fn skip_whitespace(&mut self, mut is_new_line: bool) -> io::Result<()> {
144 while let &[ch, ..] = self.buffer.ensure_bytes(1, &mut self.reader)? {
145 match ch {
146 b' ' | b'\t' | b'\r' => {
147 self.buffer.bump();
148 }
149 b'\n' => {
150 is_new_line = true;
151 self.buffer.bump();
152 }
153 b'%' if is_new_line => {
154 self.buffer.bump();
155 self.skip_until_after(b'\n')?;
156 }
157 _ => return Ok(()),
158 }
159 }
160 Ok(())
161 }
162
163 fn skip_ket(&mut self) -> io::Result<()> {
164 while let &[ch, ..] = self.buffer.ensure_bytes(1, &mut self.reader)? {
165 match ch {
166 b' ' | b'\t' | b'\r' => {
167 self.buffer.bump();
168 }
169 b']' => {
170 self.buffer.bump();
171 return self.skip_whitespace(false);
172 }
173 b'\n' => {
174 self.buffer.bump();
175 return self.skip_whitespace(true);
176 }
177 _ => {
178 return Ok(());
179 }
180 }
181 }
182 Ok(())
183 }
184
185 fn skip_token(&mut self) -> io::Result<()> {
186 while !self.buffer.ensure_bytes(1, &mut self.reader)?.is_empty() {
187 if let Some(end) = self.find_token_end() {
188 self.buffer.consume(end);
189 break;
190 } else {
191 self.buffer.clear();
192 }
193 }
194 Ok(())
195 }
196
197 fn find_token_end(&self) -> Option<usize> {
198 self.buffer.data().iter().copied().position(is_token_end)
199 }
200
201 fn eat_dash(&mut self) -> bool {
202 for dash in [b"-", "–".as_bytes(), "—".as_bytes()] {
203 if self.eat(dash) {
204 return true;
205 }
206 }
207 false
208 }
209
210 #[inline]
211 fn eat(&mut self, bytes: &[u8]) -> bool {
212 if self.buffer.data().starts_with(bytes) {
213 self.buffer.consume(bytes.len());
214 return true;
215 }
216 false
217 }
218
219 #[inline]
220 fn maybe_skip_move_number(&mut self) {
221 let data = self.buffer.data();
225 let mut n = 0;
226 while let Some(b'0'..=b'9') = data.get(n) {
227 n += 1;
228 }
229 while let Some(b'.' | b' ') = data.get(n) {
230 n += 1;
231 }
232 self.buffer.consume(n);
233 }
234
235 fn skip_tags(&mut self) -> io::Result<()> {
236 struct IgnoreTagsVisitor;
237
238 impl Visitor for IgnoreTagsVisitor {
239 type Tags = ();
240 type Movetext = Infallible;
241 type Output = Infallible;
242
243 fn begin_tags(&mut self) -> ControlFlow<Self::Output, Self::Tags> {
244 ControlFlow::Continue(())
245 }
246
247 fn begin_movetext(
248 &mut self,
249 _tags: Self::Tags,
250 ) -> ControlFlow<Self::Output, Self::Movetext> {
251 unreachable!()
252 }
253
254 fn end_game(&mut self, movetext: Self::Movetext) -> Self::Output {
255 match movetext {}
256 }
257 }
258
259 let _ = self.read_tags(&mut IgnoreTagsVisitor, &mut ())?;
260 Ok(())
261 }
262
263 fn read_tags<V: Visitor>(
264 &mut self,
265 visitor: &mut V,
266 tags: &mut V::Tags,
267 ) -> io::Result<ControlFlow<V::Output>> {
268 while let &[b'[', ..] = self
269 .buffer
270 .ensure_bytes(self.tag_line_bytes, &mut self.reader)?
271 {
272 self.buffer.bump();
273
274 let left_quote = match memchr::memchr3(b'"', b'\n', b']', self.buffer.data()) {
275 Some(left_quote) if self.buffer.data()[left_quote] == b'"' => left_quote,
276 Some(eol) => {
277 self.buffer.consume(eol + 1);
278 self.skip_ket()?;
279 continue;
280 }
281 None => {
282 self.buffer.clear();
283 self.skip_until_after(b'\n')?;
284 return Err(io::Error::new(
285 io::ErrorKind::InvalidData,
286 "unterminated tag",
287 ));
288 }
289 };
290
291 let space = if left_quote > 0 && self.buffer.data()[left_quote - 1] == b' ' {
292 left_quote - 1
293 } else {
294 left_quote
295 };
296
297 let value_start = left_quote + 1;
298 let mut right_quote = value_start;
299 let consumed = loop {
300 match memchr::memchr3(b'\\', b'"', b'\n', &self.buffer.data()[right_quote..]) {
301 Some(delta) if self.buffer.data()[right_quote + delta] == b'"' => {
302 right_quote += delta;
303 break right_quote + 1;
304 }
305 Some(delta) if self.buffer.data()[right_quote + delta] == b'\n' => {
306 right_quote += delta;
307 break right_quote;
308 }
309 Some(delta) => {
310 right_quote = min(right_quote + delta + 2, self.buffer.len());
312 }
313 None => {
314 self.buffer.clear();
315 self.skip_until_after(b'\n')?;
316 return Err(io::Error::new(
317 io::ErrorKind::InvalidData,
318 "unterminated tag",
319 ));
320 }
321 }
322 };
323
324 let cf = visitor.tag(
325 tags,
326 &self.buffer.data()[..space],
327 RawTag(&self.buffer.data()[value_start..right_quote]),
328 );
329
330 self.buffer.consume(consumed);
331
332 if self.buffer.data().starts_with(b"]\n[") {
333 self.buffer.consume(2);
335 } else {
336 self.skip_ket()?;
337 }
338
339 if cf.is_break() {
340 return Ok(cf);
341 }
342 }
343 Ok(ControlFlow::Continue(()))
344 }
345
346 fn skip_movetext(&mut self) -> io::Result<()> {
347 while let &[ch, ..] = self.buffer.ensure_bytes(3, &mut self.reader)? {
348 self.buffer.bump();
349
350 match ch {
351 b'{' => {
352 self.skip_until_after(b'}')?;
353 }
354 b';' => {
355 self.skip_until(b'\n')?;
356 }
357 b'\n' => match self.buffer.peek() {
358 Some(b'%') => {
359 self.buffer.bump();
360 self.skip_until(b'\n')?;
361 }
362 Some(b'\n' | b'[') => break,
363 Some(b'\r') => {
364 self.buffer.bump();
365 if let Some(b'\n') = self.buffer.peek() {
366 break;
367 }
368 }
369 _ => continue,
370 },
371 _ => {
372 if let Some(consumed) = memchr::memchr3(b'\n', b'{', b';', self.buffer.data()) {
373 self.buffer.consume(consumed);
374 } else {
375 self.buffer.clear();
376 }
377 }
378 }
379 }
380
381 Ok(())
382 }
383
384 fn read_movetext<V: Visitor>(
385 &mut self,
386 visitor: &mut V,
387 movetext: &mut V::Movetext,
388 ) -> io::Result<ControlFlow<V::Output>> {
389 while let &[ch, ..] = self
390 .buffer
391 .ensure_bytes(self.movetext_token_bytes, &mut self.reader)?
392 {
393 match ch {
394 b'{' => {
395 self.buffer.bump();
396 loop {
397 if self.buffer.len() == 0 {
398 return Err(io::Error::new(
399 io::ErrorKind::InvalidData,
400 "unterminated comment",
401 ));
402 } else if let Some(right_brace) = memchr::memchr(b'}', self.buffer.data()) {
403 let cf = visitor
404 .comment(movetext, RawComment(&self.buffer.data()[..right_brace]));
405 let mut consumed = right_brace + 1;
408 while self.buffer.data().get(consumed) == Some(&b' ') {
409 consumed += 1;
410 }
411 self.buffer.consume(consumed);
412 if cf.is_break() {
413 return Ok(cf);
414 }
415 break;
416 } else {
417 let trimmed = trim_partial_utf8(self.buffer.data());
418 let cf = visitor.partial_comment(movetext, RawComment(trimmed));
419 if cf.is_break() {
420 self.skip_until_after(b'}')?;
421 return Ok(cf);
422 }
423 self.buffer.consume(trimmed.len());
424 self.buffer
425 .ensure_bytes(self.movetext_token_bytes, &mut self.reader)?;
426 }
427 }
428 }
429 b'\n' => {
430 self.buffer.bump();
431
432 match self.buffer.peek() {
433 Some(b'%') => {
434 self.buffer.bump();
435 self.skip_until(b'\n')?;
436 }
437 Some(b'[' | b'\n') => {
438 break;
439 }
440 Some(b'\r') => {
441 self.buffer.bump();
442 if self.buffer.peek() == Some(b'\n') {
443 break;
444 }
445 }
446 _ => continue,
447 }
448 }
449 b';' => {
450 self.buffer.bump();
451 self.skip_until(b'\n')?;
452 }
453 b'0' => {
454 self.buffer.bump();
455 if self.eat_dash() {
456 if self.eat(b"1") {
457 let cf = visitor.outcome(
458 movetext,
459 Outcome::Known(KnownOutcome::Decisive {
460 winner: Color::Black,
461 }),
462 );
463 if cf.is_break() {
464 return Ok(cf);
465 }
466 } else if self.eat(b"0") {
467 let side = if self.eat_dash() && self.eat(b"0") {
469 CastlingSide::QueenSide
470 } else {
471 CastlingSide::KingSide
472 };
473 let suffix = match self.buffer.peek() {
474 Some(b'+') => {
475 self.buffer.bump();
476 Some(Suffix::Check)
477 }
478 Some(b'#') => {
479 self.buffer.bump();
480 Some(Suffix::Checkmate)
481 }
482 _ => None,
483 };
484 let cf = visitor.san(
485 movetext,
486 SanPlus {
487 san: San::Castle(side),
488 suffix,
489 },
490 );
491 if cf.is_break() {
492 return Ok(cf);
493 }
494 }
495 }
496 }
497 b'1' => {
498 self.buffer.bump();
499 if self.eat_dash() {
500 if self.eat(b"0") {
501 let cf = visitor.outcome(
502 movetext,
503 Outcome::Known(KnownOutcome::Decisive {
504 winner: Color::White,
505 }),
506 );
507 if cf.is_break() {
508 return Ok(cf);
509 }
510 }
511 } else if self.eat(b"/2") && self.eat_dash() && self.eat(b"1/2") {
512 let cf = visitor.outcome(movetext, Outcome::Known(KnownOutcome::Draw));
513 if cf.is_break() {
514 return Ok(cf);
515 }
516 } else {
517 self.maybe_skip_move_number();
518 }
519 }
520 b'\xc2' => {
521 self.buffer.bump();
523 if self.eat(b"\xbd") && self.eat_dash() && self.eat("½".as_bytes()) {
524 let cf = visitor.outcome(movetext, Outcome::Known(KnownOutcome::Draw));
525 if cf.is_break() {
526 return Ok(cf);
527 }
528 }
529 }
530 b'2'..=b'9' => {
531 self.buffer.bump();
532 self.maybe_skip_move_number();
533 }
534 b'(' => {
535 self.buffer.bump();
536 match visitor.begin_variation(movetext) {
537 ControlFlow::Continue(Skip(true)) => self.skip_variation()?,
538 ControlFlow::Continue(Skip(false)) => (),
539 ControlFlow::Break(output) => {
540 return Ok(ControlFlow::Break(output));
541 }
542 }
543 }
544 b')' => {
545 self.buffer.bump();
546 let cf = visitor.end_variation(movetext);
547 if cf.is_break() {
548 return Ok(cf);
549 }
550 }
551 b'$' => {
552 self.buffer.bump();
553 if let Some(token_end) = self.find_token_end() {
554 if let Ok(nag) = btoi::btou(&self.buffer.data()[..token_end]) {
555 let cf = visitor.nag(movetext, Nag(nag));
556 if cf.is_break() {
557 return Ok(cf);
558 }
559 }
560 self.buffer.consume(token_end);
561 } else {
562 self.buffer.clear();
563 self.skip_token()?;
564 }
565 }
566 b'!' => {
567 self.buffer.bump();
568 let cf = match self.buffer.peek() {
569 Some(b'!') => {
570 self.buffer.bump();
571 visitor.nag(movetext, Nag::BRILLIANT_MOVE)
572 }
573 Some(b'?') => {
574 self.buffer.bump();
575 visitor.nag(movetext, Nag::SPECULATIVE_MOVE)
576 }
577 _ => visitor.nag(movetext, Nag::GOOD_MOVE),
578 };
579 if cf.is_break() {
580 return Ok(cf);
581 }
582 }
583 b'?' => {
584 self.buffer.bump();
585 let cf = match self.buffer.peek() {
586 Some(b'!') => {
587 self.buffer.bump();
588 visitor.nag(movetext, Nag::DUBIOUS_MOVE)
589 }
590 Some(b'?') => {
591 self.buffer.bump();
592 visitor.nag(movetext, Nag::BLUNDER)
593 }
594 _ => visitor.nag(movetext, Nag::MISTAKE),
595 };
596 if cf.is_break() {
597 return Ok(cf);
598 }
599 }
600 b'*' => {
601 self.buffer.bump();
602 let cf = visitor.outcome(movetext, Outcome::Unknown);
603 if cf.is_break() {
604 return Ok(cf);
605 }
606 }
607 b' ' | b'\t' | b'\r' | b'.' => {
608 self.buffer.bump();
609 }
610 _ => {
611 if let Ok((san, bytes)) = SanPlus::from_ascii_prefix(self.buffer.data()) {
612 self.buffer.consume(bytes);
613 if self.buffer.peek().is_none_or(is_token_end) {
614 let cf = visitor.san(movetext, san);
615 if cf.is_break() {
616 return Ok(cf);
617 }
618 }
619 } else {
620 self.buffer.bump();
621 self.skip_token()?;
622 }
623 }
624 }
625 }
626
627 Ok(ControlFlow::Continue(()))
628 }
629
630 fn skip_variation(&mut self) -> io::Result<()> {
631 let mut depth = 0usize;
632
633 while let &[ch, ..] = self.buffer.ensure_bytes(3, &mut self.reader)? {
634 match ch {
635 b'(' => {
636 self.buffer.bump();
637 depth += 1;
638 }
639 b')' => {
640 if let Some(d) = depth.checked_sub(1) {
641 self.buffer.bump();
642 depth = d;
643 } else {
644 break;
645 }
646 }
647 b'{' => {
648 self.buffer.bump();
649 self.skip_until_after(b'}')?;
650 }
651 b';' => {
652 self.buffer.bump();
653 self.skip_until(b'\n')?;
654 }
655 b'\n' => {
656 match self.buffer.data().get(1).copied() {
657 Some(b'%') => {
658 self.buffer.consume(2);
659 self.skip_until(b'\n')?;
660 }
661 Some(b'[' | b'\n') => {
662 break;
664 }
665 Some(b'\r') if self.buffer.data().get(2).copied() == Some(b'\n') => {
666 break;
668 }
669 _ => {
670 self.buffer.bump();
671 }
672 }
673 }
674 _ => {
675 self.buffer.bump();
676 }
677 }
678 }
679
680 Ok(())
681 }
682
683 fn before_game(&mut self) -> io::Result<()> {
684 if mem::take(&mut self.pending_skip_tags) {
685 self.skip_tags()?;
686 }
687 if mem::take(&mut self.pending_skip_movetext) {
688 self.skip_movetext()?;
689 }
690 self.skip_bom()?;
691 self.skip_whitespace(true)
692 }
693
694 pub fn read_game<V: Visitor>(&mut self, visitor: &mut V) -> io::Result<Option<V::Output>> {
704 self.before_game()?;
705
706 if self.buffer.ensure_bytes(1, &mut self.reader)?.is_empty() {
707 return Ok(None);
708 }
709
710 let mut tags = match visitor.begin_tags() {
711 ControlFlow::Break(output) => {
712 self.pending_skip_tags = true;
713 self.pending_skip_movetext = true;
714 return Ok(Some(output));
715 }
716 ControlFlow::Continue(tags) => tags,
717 };
718 if let ControlFlow::Break(output) = self.read_tags(visitor, &mut tags)? {
719 self.pending_skip_tags = true;
720 self.pending_skip_movetext = true;
721 return Ok(Some(output));
722 }
723 let mut movetext = match visitor.begin_movetext(tags) {
724 ControlFlow::Break(output) => {
725 self.pending_skip_movetext = true;
726 return Ok(Some(output));
727 }
728 ControlFlow::Continue(movetext) => movetext,
729 };
730 if let ControlFlow::Break(output) = self.read_movetext(visitor, &mut movetext)? {
731 self.pending_skip_movetext = true;
732 return Ok(Some(output));
733 };
734 Ok(Some(visitor.end_game(movetext)))
735 }
736
737 pub fn has_more(&mut self) -> io::Result<bool> {
746 self.before_game()?;
747 Ok(!self.buffer.ensure_bytes(1, &mut self.reader)?.is_empty())
748 }
749
750 pub fn skip_game(&mut self) -> io::Result<bool> {
759 let has_more = self.has_more()?;
760 self.skip_tags()?;
761 self.skip_movetext()?;
762 Ok(has_more)
763 }
764
765 #[must_use = "iterator is lazy"]
767 pub fn read_games<'a, V: Visitor>(&'a mut self, visitor: &'a mut V) -> ReadGames<'a, R, V> {
768 ReadGames {
769 reader: self,
770 visitor,
771 }
772 }
773
774 pub fn visit_all_games<V: Visitor>(&mut self, visitor: &mut V) -> io::Result<()> {
781 while self.read_game(visitor)?.is_some() {}
782 Ok(())
783 }
784
785 pub fn buffer(&self) -> &[u8] {
787 self.buffer.data()
788 }
789
790 pub fn into_inner(self) -> R {
793 self.reader
794 }
795}
796
797#[derive(Debug)]
799#[must_use]
800pub struct ReadGames<'a, R, V> {
801 reader: &'a mut Reader<R>,
802 visitor: &'a mut V,
803}
804
805impl<R: Read, V: Visitor> Iterator for ReadGames<'_, R, V> {
806 type Item = Result<V::Output, io::Error>;
807
808 fn next(&mut self) -> Option<Self::Item> {
809 match self.reader.read_game(self.visitor) {
810 Ok(Some(result)) => Some(Ok(result)),
811 Ok(None) => None,
812 Err(err) => Some(Err(err)),
813 }
814 }
815}
816
817#[inline]
818fn is_token_end(byte: u8) -> bool {
819 static TOKEN_END: [bool; 256] = {
820 let mut table = [false; 256];
821 let mut i = 0;
822 while i < 256 {
823 table[i] = matches!(
824 i as u8,
825 b' ' | b'\t'
826 | b'\n'
827 | b'\r'
828 | b'{'
829 | b'}'
830 | b'('
831 | b')'
832 | b'!'
833 | b'?'
834 | b'$'
835 | b';'
836 | b'.'
837 | b'*'
838 );
839 i += 1;
840 }
841 table
842 };
843
844 TOKEN_END[usize::from(byte)]
845}
846
847impl<R: Seek> Seek for Reader<R> {
848 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
849 let result = if let SeekFrom::Current(offset) = pos {
850 let buffered = self.buffer.len() as i64;
851 if let Some(offset) = offset.checked_sub(buffered) {
852 self.reader.seek(SeekFrom::Current(offset))?
853 } else {
854 self.reader.seek_relative(-buffered)?;
855 self.buffer.clear();
856 self.reader.seek(SeekFrom::Current(offset))?
857 }
858 } else {
859 self.reader.seek(pos)?
860 };
861 self.buffer.clear();
862 Ok(result)
863 }
864
865 fn seek_relative(&mut self, offset: i64) -> io::Result<()> {
866 let buffered = self.buffer.len() as i64;
867 if let Some(offset) = offset.checked_sub(buffered) {
868 self.reader.seek_relative(offset)?;
869 self.buffer.clear();
870 Ok(())
871 } else {
872 self.reader.seek_relative(-buffered)?;
873 self.buffer.clear();
874 self.reader.seek_relative(offset)
875 }
876 }
877
878 fn stream_position(&mut self) -> io::Result<u64> {
879 let buffered = self.buffer.len() as u64;
880 self.reader.stream_position().map(|pos| {
881 pos.checked_sub(buffered)
882 .expect("consistent stream position")
883 })
884 }
885}
886
887fn trim_partial_utf8(buf: &[u8]) -> &[u8] {
894 let scan_start = buf.len().saturating_sub(4);
895 let Some(last_start_byte) = (scan_start..buf.len())
896 .rev()
897 .find(|b| !is_utf8_continuation(buf[*b]))
898 else {
899 return buf;
900 };
901
902 if last_start_byte == 0 || buf.len() - last_start_byte >= utf8_start_width(buf[last_start_byte])
903 {
904 buf
905 } else {
906 &buf[..last_start_byte]
907 }
908}
909
910#[inline]
911fn is_utf8_continuation(b: u8) -> bool {
912 (b & 0b1100_0000) == 0b1000_0000
913}
914
915#[inline]
916fn utf8_start_width(b: u8) -> usize {
917 match b {
918 0x00..=0x7F => 1, 0xC2..=0xDF => 2, 0xE0..=0xEF => 3, 0xF0..=0xF4 => 4, _ => 0, }
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929 use crate::shakmaty::{Role, Square};
930
931 struct _AssertObjectSafe<R>(Box<Reader<R>>);
932
933 #[derive(Debug, Eq, PartialEq)]
934 enum Token {
935 BeginTags,
936 Tag(Vec<u8>, Vec<u8>),
937 BeginMovetext,
938 San(SanPlus),
939 Nag(Nag),
940 Comment(Vec<u8>),
941 BeginVariation,
942 EndVariation,
943 Outcome(Outcome),
944 EndGame,
945 }
946
947 struct CollectTokens;
948
949 impl Visitor for CollectTokens {
950 type Tags = Vec<Token>;
951 type Movetext = Vec<Token>;
952 type Output = Vec<Token>;
953
954 fn begin_tags(&mut self) -> ControlFlow<Self::Output, Self::Tags> {
955 ControlFlow::Continue(vec![Token::BeginTags])
956 }
957
958 fn tag(
959 &mut self,
960 tags: &mut Self::Tags,
961 name: &[u8],
962 value: RawTag<'_>,
963 ) -> ControlFlow<Self::Output> {
964 tags.push(Token::Tag(name.to_owned(), value.decode().into_owned()));
965 ControlFlow::Continue(())
966 }
967
968 fn begin_movetext(
969 &mut self,
970 mut tags: Self::Tags,
971 ) -> ControlFlow<Self::Output, Self::Movetext> {
972 tags.push(Token::BeginMovetext);
973 ControlFlow::Continue(tags)
974 }
975
976 fn san(
977 &mut self,
978 movetext: &mut Self::Movetext,
979 san_plus: SanPlus,
980 ) -> ControlFlow<Self::Output> {
981 movetext.push(Token::San(san_plus));
982 ControlFlow::Continue(())
983 }
984
985 fn nag(&mut self, movetext: &mut Self::Movetext, nag: Nag) -> ControlFlow<Self::Output> {
986 movetext.push(Token::Nag(nag));
987 ControlFlow::Continue(())
988 }
989
990 fn comment(
991 &mut self,
992 movetext: &mut Self::Movetext,
993 comment: RawComment<'_>,
994 ) -> ControlFlow<Self::Output> {
995 movetext.push(Token::Comment(comment.as_bytes().to_owned()));
996 ControlFlow::Continue(())
997 }
998
999 fn begin_variation(
1000 &mut self,
1001 movetext: &mut Self::Movetext,
1002 ) -> ControlFlow<Self::Output, Skip> {
1003 movetext.push(Token::BeginVariation);
1004 ControlFlow::Continue(Skip(false))
1005 }
1006
1007 fn end_variation(&mut self, movetext: &mut Self::Movetext) -> ControlFlow<Self::Output> {
1008 movetext.push(Token::EndVariation);
1009 ControlFlow::Continue(())
1010 }
1011
1012 fn outcome(
1013 &mut self,
1014 movetext: &mut Self::Movetext,
1015 outcome: Outcome,
1016 ) -> ControlFlow<Self::Output> {
1017 movetext.push(Token::Outcome(outcome));
1018 ControlFlow::Continue(())
1019 }
1020
1021 fn end_game(&mut self, mut movetext: Self::Movetext) -> Self::Output {
1022 movetext.push(Token::EndGame);
1023 movetext
1024 }
1025 }
1026
1027 #[test]
1028 fn test_empty() -> io::Result<()> {
1029 let pgn = b"";
1030
1031 assert!(
1032 Reader::new(io::Cursor::new(pgn))
1033 .read_game(&mut CollectTokens)?
1034 .is_none()
1035 );
1036
1037 assert!(!Reader::new(io::Cursor::new(pgn)).skip_game()?);
1038
1039 Ok(())
1040 }
1041
1042 #[test]
1043 fn test_whitespace() -> io::Result<()> {
1044 let pgn = b"\xef\xbb\xbf \n\r\t \n\n ";
1045
1046 assert!(
1047 Reader::new(io::Cursor::new(pgn))
1048 .read_game(&mut CollectTokens)?
1049 .is_none()
1050 );
1051
1052 assert!(!Reader::new(io::Cursor::new(pgn)).skip_game()?);
1053
1054 Ok(())
1055 }
1056
1057 #[test]
1058 fn test_trailing_space() -> io::Result<()> {
1059 let pgn = b"1. e4 1-0\n\n\n\n\n \n";
1060
1061 let mut reader = Reader::new(io::Cursor::new(pgn));
1062 assert!(reader.read_game(&mut CollectTokens)?.is_some());
1063 assert!(reader.read_game(&mut CollectTokens)?.is_none());
1064
1065 let mut reader = Reader::new(io::Cursor::new(pgn));
1066 assert!(reader.skip_game()?);
1067 assert!(!reader.skip_game()?);
1068
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn test_rest_of_line_comment() -> io::Result<()> {
1074 let pgn = b";\n%\n\nX";
1075
1076 let mut reader = Reader::new(io::Cursor::new(pgn));
1077 let mut skip_reader = Reader::new(io::Cursor::new(pgn));
1078
1079 assert!(reader.read_game(&mut CollectTokens)?.is_some());
1080 assert!(skip_reader.skip_game()?);
1081 assert_eq!(reader.stream_position()?, skip_reader.stream_position()?);
1082
1083 assert!(reader.read_game(&mut CollectTokens)?.is_some());
1084 assert!(skip_reader.skip_game()?);
1085 assert_eq!(reader.stream_position()?, skip_reader.stream_position()?);
1086
1087 assert!(reader.read_game(&mut CollectTokens)?.is_none());
1088 assert!(!skip_reader.skip_game()?);
1089
1090 Ok(())
1091 }
1092
1093 #[test]
1094 fn test_long_comment() -> io::Result<()> {
1095 let crabs: String = std::iter::repeat_n('🦀', 257).collect();
1100 let pgn = format!("1. e4 {{{}}}", &crabs).into_bytes();
1101 let mut reader = Reader::new(io::Cursor::new(pgn.as_slice()));
1102 let tokens = reader.read_game(&mut CollectTokens)?.expect("found game");
1103 let mut rebuilt = String::new();
1104 for token in tokens {
1105 if let Token::Comment(bytes) = token {
1106 let s = core::str::from_utf8(bytes.as_slice())
1107 .expect("reader preserves UTF-8 validity");
1108 rebuilt.push_str(s);
1109 }
1110 }
1111 assert_eq!(rebuilt, crabs);
1112 Ok(())
1113 }
1114
1115 #[test]
1116 fn test_tag_movetext_transition() -> io::Result<()> {
1117 let pgn = br#"
1118 [One "1"] [Two "2"]
1119%[Three "%"]
1120[Three "3"]
1121
1122
1123 [Four "4"]*
1124
1125 [Five "5"]"#;
1126
1127 let mut reader = Reader::new(io::Cursor::new(pgn));
1128
1129 let game = reader.read_game(&mut CollectTokens)?.expect("found game");
1130 assert_eq!(
1131 game,
1132 &[
1133 Token::BeginTags,
1134 Token::Tag(b"One".into(), b"1".into()),
1135 Token::Tag(b"Two".into(), b"2".into()),
1136 Token::Tag(b"Three".into(), b"3".into()),
1137 Token::Tag(b"Four".into(), b"4".into()),
1138 Token::BeginMovetext,
1139 Token::Outcome(Outcome::Unknown),
1140 Token::EndGame,
1141 ]
1142 );
1143
1144 let game = reader.read_game(&mut CollectTokens)?.expect("found game");
1145 assert_eq!(
1146 game,
1147 &[
1148 Token::BeginTags,
1149 Token::Tag(b"Five".into(), b"5".into()),
1150 Token::BeginMovetext,
1151 Token::EndGame,
1152 ]
1153 );
1154
1155 Ok(())
1156 }
1157
1158 #[test]
1159 fn test_movetext() -> io::Result<()> {
1160 let pgn =
1161 br#"[White "hello\" "][Black "world"]1.f3! e5$71 2.g4 ?? (-- {}) O-O O-O-O#!?0-1"#;
1162
1163 let game = Reader::new(io::Cursor::new(pgn))
1164 .read_game(&mut CollectTokens)?
1165 .expect("found game");
1166
1167 assert_eq!(
1168 game,
1169 &[
1170 Token::BeginTags,
1171 Token::Tag(b"White".into(), b"hello\" ".into()),
1172 Token::Tag(b"Black".into(), b"world".into()),
1173 Token::BeginMovetext,
1174 Token::San(SanPlus {
1175 san: San::Normal {
1176 role: Role::Pawn,
1177 file: None,
1178 rank: None,
1179 capture: false,
1180 to: Square::F3,
1181 promotion: None
1182 },
1183 suffix: None
1184 }),
1185 Token::Nag(Nag::GOOD_MOVE),
1186 Token::San(SanPlus {
1187 san: San::Normal {
1188 role: Role::Pawn,
1189 file: None,
1190 rank: None,
1191 capture: false,
1192 to: Square::E5,
1193 promotion: None
1194 },
1195 suffix: None
1196 }),
1197 Token::Nag(Nag(71)),
1198 Token::San(SanPlus {
1199 san: San::Normal {
1200 role: Role::Pawn,
1201 file: None,
1202 rank: None,
1203 capture: false,
1204 to: Square::G4,
1205 promotion: None
1206 },
1207 suffix: None
1208 }),
1209 Token::Nag(Nag::BLUNDER),
1210 Token::BeginVariation,
1211 Token::San(SanPlus {
1212 san: San::Null,
1213 suffix: None
1214 }),
1215 Token::Comment(vec![]),
1216 Token::EndVariation,
1217 Token::San(SanPlus {
1218 san: San::Castle(CastlingSide::KingSide),
1219 suffix: None,
1220 }),
1221 Token::San(SanPlus {
1222 san: San::Castle(CastlingSide::QueenSide),
1223 suffix: Some(Suffix::Checkmate),
1224 }),
1225 Token::Nag(Nag::SPECULATIVE_MOVE),
1226 Token::Outcome(Outcome::Known(KnownOutcome::Decisive {
1227 winner: Color::Black
1228 })),
1229 Token::EndGame,
1230 ]
1231 );
1232
1233 Ok(())
1234 }
1235
1236 #[test]
1237 fn test_outcomes() -> io::Result<()> {
1238 let pgn = "1-0 0-1 1/2-1/2 1–0 0–1 ½–½".as_bytes();
1239
1240 let game = Reader::new(io::Cursor::new(pgn))
1241 .read_game(&mut CollectTokens)?
1242 .expect("found game");
1243
1244 assert_eq!(
1245 game,
1246 &[
1247 Token::BeginTags,
1248 Token::BeginMovetext,
1249 Token::Outcome(Outcome::Known(KnownOutcome::Decisive {
1250 winner: Color::White
1251 })),
1252 Token::Outcome(Outcome::Known(KnownOutcome::Decisive {
1253 winner: Color::Black
1254 })),
1255 Token::Outcome(Outcome::Known(KnownOutcome::Draw)),
1256 Token::Outcome(Outcome::Known(KnownOutcome::Decisive {
1257 winner: Color::White
1258 })),
1259 Token::Outcome(Outcome::Known(KnownOutcome::Decisive {
1260 winner: Color::Black
1261 })),
1262 Token::Outcome(Outcome::Known(KnownOutcome::Draw)),
1263 Token::EndGame,
1264 ]
1265 );
1266
1267 Ok(())
1268 }
1269
1270 #[test]
1271 fn test_null_moves() -> io::Result<()> {
1272 let pgn = "-- Z0 --+ Z0+ --# Z0#".as_bytes();
1273
1274 let game = Reader::new(io::Cursor::new(pgn))
1275 .read_game(&mut CollectTokens)?
1276 .expect("found game");
1277
1278 assert_eq!(
1279 game,
1280 &[
1281 Token::BeginTags,
1282 Token::BeginMovetext,
1283 Token::San(SanPlus {
1284 san: San::Null,
1285 suffix: None
1286 }),
1287 Token::San(SanPlus {
1288 san: San::Null,
1289 suffix: None
1290 }),
1291 Token::San(SanPlus {
1292 san: San::Null,
1293 suffix: Some(Suffix::Check),
1294 }),
1295 Token::San(SanPlus {
1296 san: San::Null,
1297 suffix: Some(Suffix::Check),
1298 }),
1299 Token::San(SanPlus {
1300 san: San::Null,
1301 suffix: Some(Suffix::Checkmate),
1302 }),
1303 Token::San(SanPlus {
1304 san: San::Null,
1305 suffix: Some(Suffix::Checkmate),
1306 }),
1307 Token::EndGame,
1308 ]
1309 );
1310
1311 Ok(())
1312 }
1313}