pgn_reader/
types.rs

1use std::{
2    borrow::Cow,
3    error::Error,
4    fmt,
5    str::{self, FromStr, Utf8Error},
6};
7
8/// Tell the reader to skip over a game or variation.
9#[derive(Clone, Eq, PartialEq, Debug)]
10#[must_use]
11pub struct Skip(pub bool);
12
13/// A numeric annotation glyph like `?`, `!!` or `$42`.
14#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
15pub struct Nag(pub u8);
16
17impl Nag {
18    /// Tries to parse a NAG from ASCII.
19    ///
20    /// # Examples
21    ///
22    /// ```
23    /// use pgn_reader::Nag;
24    ///
25    /// assert_eq!(Nag::from_ascii(b"??"), Ok(Nag(4)));
26    /// assert_eq!(Nag::from_ascii(b"$24"), Ok(Nag(24)));
27    /// ```
28    ///
29    /// # Errors
30    ///
31    /// Returns an [`InvalidNag`] error if the input is neither a known glyph
32    /// (`?!`, `!`, ...) nor a valid numeric annotation (`$0`, ..., `$255`).
33    ///
34    ///
35    /// [`InvalidNag`]: struct.InvalidNag.html
36    pub fn from_ascii(s: &[u8]) -> Result<Nag, InvalidNag> {
37        if s == b"?!" {
38            Ok(Nag::DUBIOUS_MOVE)
39        } else if s == b"?" {
40            Ok(Nag::MISTAKE)
41        } else if s == b"??" {
42            Ok(Nag::BLUNDER)
43        } else if s == b"!" {
44            Ok(Nag::GOOD_MOVE)
45        } else if s == b"!!" {
46            Ok(Nag::BRILLIANT_MOVE)
47        } else if s == b"!?" {
48            Ok(Nag::SPECULATIVE_MOVE)
49        } else if s.len() > 1 && s[0] == b'$' {
50            btoi::btou(&s[1..])
51                .ok()
52                .map(Nag)
53                .ok_or(InvalidNag { _priv: () })
54        } else {
55            Err(InvalidNag { _priv: () })
56        }
57    }
58
59    /// A good move (`!`).
60    pub const GOOD_MOVE: Nag = Nag(1);
61
62    /// A mistake (`?`).
63    pub const MISTAKE: Nag = Nag(2);
64
65    /// A brilliant move (`!!`).
66    pub const BRILLIANT_MOVE: Nag = Nag(3);
67
68    /// A blunder (`??`).
69    pub const BLUNDER: Nag = Nag(4);
70
71    /// A speculative move (`!?`).
72    pub const SPECULATIVE_MOVE: Nag = Nag(5);
73
74    /// A dubious move (`?!`).
75    pub const DUBIOUS_MOVE: Nag = Nag(6);
76}
77
78impl fmt::Display for Nag {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        write!(f, "${}", self.0)
81    }
82}
83
84impl From<u8> for Nag {
85    fn from(nag: u8) -> Nag {
86        Nag(nag)
87    }
88}
89
90/// Error when parsing an invalid NAG.
91#[derive(Clone, Eq, PartialEq)]
92pub struct InvalidNag {
93    _priv: (),
94}
95
96impl fmt::Debug for InvalidNag {
97    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98        f.debug_struct("InvalidNag").finish()
99    }
100}
101
102impl fmt::Display for InvalidNag {
103    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104        "invalid nag".fmt(f)
105    }
106}
107
108impl Error for InvalidNag {
109    fn description(&self) -> &str {
110        "invalid nag"
111    }
112}
113
114impl FromStr for Nag {
115    type Err = InvalidNag;
116
117    fn from_str(s: &str) -> Result<Nag, InvalidNag> {
118        Nag::from_ascii(s.as_bytes())
119    }
120}
121
122/// A tag value.
123///
124/// Provides helper methods for decoding [backslash
125/// escaped](http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm#c7)
126/// values.
127///
128/// > A quote inside a string is represented by the backslash immediately
129/// > followed by a quote. A backslash inside a string is represented by
130/// > two adjacent backslashes.
131#[derive(Clone, Eq, PartialEq)]
132pub struct RawTag<'a>(pub &'a [u8]);
133
134impl<'a> RawTag<'a> {
135    /// Returns the raw byte representation of the tag value.
136    pub fn as_bytes(&self) -> &[u8] {
137        self.0
138    }
139
140    /// Decodes escaped quotes and backslashes into bytes. Allocates only when
141    /// the value actually contains escape sequences.
142    pub fn decode(&self) -> Cow<'a, [u8]> {
143        let mut head = 0;
144        let mut decoded: Vec<u8> = Vec::new();
145        for escape in memchr::memchr_iter(b'\\', self.0) {
146            match self.0.get(escape + 1).cloned() {
147                Some(ch) if ch == b'\\' || ch == b'"' => {
148                    decoded.extend_from_slice(&self.0[head..escape]);
149                    head = escape + 1;
150                }
151                _ => (),
152            }
153        }
154        if head == 0 {
155            Cow::Borrowed(self.0)
156        } else {
157            decoded.extend_from_slice(&self.0[head..]);
158            Cow::Owned(decoded)
159        }
160    }
161
162    /// Tries to decode the tag as UTF-8. This is guaranteed to succeed on
163    /// valid PGNs.
164    ///
165    /// # Errors
166    ///
167    /// Errors if the tag contains an invalid UTF-8 byte sequence.
168    pub fn decode_utf8(&self) -> Result<Cow<'a, str>, Utf8Error> {
169        Ok(match self.decode() {
170            Cow::Borrowed(borrowed) => Cow::Borrowed(str::from_utf8(borrowed)?),
171            Cow::Owned(owned) => Cow::Owned(String::from_utf8(owned).map_err(|e| e.utf8_error())?),
172        })
173    }
174
175    /// Decodes the tag as UTF-8, replacing any invalid byte sequences with
176    /// the placeholder � U+FFFD.
177    pub fn decode_utf8_lossy(&self) -> Cow<'a, str> {
178        match self.decode() {
179            Cow::Borrowed(borrowed) => String::from_utf8_lossy(borrowed),
180            Cow::Owned(owned) => Cow::Owned(String::from_utf8_lossy(&owned).into_owned()),
181        }
182    }
183}
184
185impl<'a> fmt::Debug for RawTag<'a> {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        write!(f, "{:?}", self.decode_utf8_lossy())
188    }
189}
190
191/// A comment, excluding the braces.
192#[derive(Clone, Eq, PartialEq)]
193pub struct RawComment<'a>(pub &'a [u8]);
194
195impl<'a> RawComment<'a> {
196    /// Returns the raw byte representation of the comment.
197    pub fn as_bytes(&self) -> &[u8] {
198        self.0
199    }
200}
201
202impl<'a> fmt::Debug for RawComment<'a> {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "{:?}", String::from_utf8_lossy(self.as_bytes()).as_ref())
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_nag() {
214        assert_eq!(Nag::from_ascii(b"$33"), Ok(Nag(33)));
215    }
216
217    #[test]
218    fn test_raw_tag() {
219        let tag = RawTag(b"Hello world");
220        assert_eq!(tag.decode().as_ref(), b"Hello world");
221
222        let tag = RawTag(b"Hello \\world\\");
223        assert_eq!(tag.decode().as_ref(), b"Hello \\world\\");
224
225        let tag = RawTag(b"\\Hello \\\"world\\\\");
226        assert_eq!(tag.decode().as_ref(), b"\\Hello \"world\\");
227    }
228}