Skip to main content

pgn_reader/
reader.rs

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/// Build a [`Reader`] with custom settings.
17#[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    /// Create a [`ReaderBuilder`] with default settings based on the PGN
26    /// standard.
27    pub fn new(reader: R) -> Self {
28        ReaderBuilder {
29            reader,
30            tag_line_bytes: 255,
31            movetext_token_bytes: 255,
32        }
33    }
34
35    /// Configure the buffer to support *at least* the given tag line length.
36    ///
37    /// Defaults to `255` bytes.
38    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    /// Configure the buffer to support *at least* the given comment length.
44    ///
45    /// Longer comments will be split into one or more
46    /// calls to [`Visitor::partial_comment()`] followed by a final call to
47    /// [`Visitor::comment()`].
48    ///
49    /// Defaults to `255` bytes.
50    pub fn set_supported_comment_length(mut self, bytes: usize) -> Self {
51        self.movetext_token_bytes = max(255, bytes) + 2; // Plus '{' and '}'
52        self
53    }
54
55    /// Finalize and create a [`Reader`].
56    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/// Reads a PGN and calls [`Visitor`] methods.
72///
73/// Buffers the underlying reader with an appropriate strategy, so it's *not*
74/// recommended to add an additional layer of buffering like
75/// [`BufReader`](std::io::BufReader).
76#[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    /// Create a reader with default settings based on the PGN standard.
88    pub fn new(reader: R) -> Reader<R> {
89        ReaderBuilder::new(reader).finish()
90    }
91
92    /// Build a reader with custom settings.
93    ///
94    /// ```no_run
95    /// use std::fs::File;
96    /// use pgn_reader::Reader;
97    ///
98    /// let reader = Reader::build(File::open("example.pgn")?)
99    ///     .set_supported_tag_line_length(1000)
100    ///     .set_supported_comment_length(4000)
101    ///     .finish();
102    /// # Ok::<_, std::io::Error>(())
103    /// ```
104    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        // Optimization: Skip a move number following in the buffer, if any.
222        // It might consume nothing, a partial move number, or the entire
223        // move number.
224        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                        // Skip escaped character.
311                        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                // Fast path for the common case, equivalent to skip_ket().
334                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                            // Consume at least the right brace, and (as an optimization only)
406                            // some spaces.
407                            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                            // Castling notation with zeros.
468                            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                    // ½-½
522                    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                            // Do not consume the first or second line break.
663                            break;
664                        }
665                        Some(b'\r') if self.buffer.data().get(2).copied() == Some(b'\n') => {
666                            // Do not consume the first or second line break.
667                            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    /// Read a single game, if any, and return the result produced by the
695    /// visitor.
696    ///
697    /// Returns `Ok(None)` if the underlying reader is empty.
698    ///
699    /// # Errors
700    ///
701    /// * I/O error from the underlying reader.
702    /// * Irrecoverable parser errors.
703    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    /// Returns whether the reader has another game to parse, but does not
738    /// actually parse it.
739    ///
740    /// # Errors
741    ///
742    /// * I/O error from the underlying reader.
743    /// * Irrecoverable parser errors (while trying to read previous
744    ///   unfinished game to completion).
745    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    /// Skip a single game, if any.
751    ///
752    /// Returns `Ok(true)` if a game found and skipped.
753    ///
754    /// # Errors
755    ///
756    /// * I/O error from the underlying reader.
757    /// * Irrecoverable parser errors.
758    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    /// Iterate over all games, yielding the visitor outputs.
766    #[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    /// Visit all games, ignoring the visitor outputs.
775    ///
776    /// # Errors
777    ///
778    /// * I/O error from the underlying reader.
779    /// * Irrecoverable parser errors.
780    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    /// The currently buffered bytes.
786    pub fn buffer(&self) -> &[u8] {
787        self.buffer.data()
788    }
789
790    /// Discard the remaining bytes in the buffer ([`Reader::buffer()`]) and
791    /// get the underlying reader.
792    pub fn into_inner(self) -> R {
793        self.reader
794    }
795}
796
797/// Iterator returned by [`Reader::read_games()`].
798#[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
887// Helper function for handling comments that don't fit in the movetext token
888// buffer. When we encounter these, we'll call `Vistor::comment` several times
889// with roughly buffer-sized chunks, but we want to avoid putting chunk
890// boundaries in the middle of a multibyte UTF-8 sequence. We don't assume or
891// require that the PGN is UTF-8 encoded at all. To prevent an infinite loop,
892// we avoid ever turning a non-empty slice into an empty one.
893fn 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, // ASCII
919        0xC2..=0xDF => 2, // 2-byte lead (exclude C0/C1 overlong)
920        0xE0..=0xEF => 3, // 3-byte lead
921        0xF0..=0xF4 => 4, // 4-byte lead (UTF-8 valid range)
922        _ => 0,           // continuation byte or invalid lead
923    }
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        // This repeats a four-byte sequence 257 times, and the buffer is 255+2
1096        // bytes. Since 4 is relatively prime to 257, this ensures that we test
1097        // splitting the sequence no matter where the beginning of the comment
1098        // occurs.
1099        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}