summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2021-11-11 20:29:55 -0300
committerSoniEx2 <endermoneymod@gmail.com>2021-11-11 20:29:55 -0300
commitd4724b4734776d32fb86cd3c932e18fc41b68316 (patch)
treef289b70f1aa1b971d76737d702ea112a67d9c8c2
Start porting Brigadier to Rust
-rw-r--r--.gitignore2
-rw-r--r--COPYRIGHT1
-rw-r--r--Cargo.toml14
-rw-r--r--LICENSE-Mojang21
-rw-r--r--README.md7
-rw-r--r--src/lib.rs10
-rw-r--r--src/strcursor.rs368
-rw-r--r--tests/common/errorfunc.rs89
-rw-r--r--tests/common/errorpanic.rs114
-rw-r--r--tests/common/mod.rs12
-rw-r--r--tests/strcursor.rs592
11 files changed, 1230 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96ef6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/COPYRIGHT b/COPYRIGHT
new file mode 100644
index 0000000..e2d3fb0
--- /dev/null
+++ b/COPYRIGHT
@@ -0,0 +1 @@
+Iosonism is a port of Brigadier (https://github.com/Mojang/brigadier) to Rust.
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..92ff3ba
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "iosonism"
+version = "0.1.0"
+authors = ["SoniEx2 <endermoneymod@gmail.com>"]
+license = "MIT"
+description = "An advanced command parser"
+edition = "2021"
+repository = "https://soniex2.autistic.space/git-repos/iosonism.git"
+readme = "README.md"
+publish = false # for now
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
diff --git a/LICENSE-Mojang b/LICENSE-Mojang
new file mode 100644
index 0000000..88040d8
--- /dev/null
+++ b/LICENSE-Mojang
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..894e6d2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+Iosonism
+========
+
+Iosonism is a command parsing library, not to be confused with an argument
+parsing library. It's basically a port of [Brigadier] to Rust.
+
+[Brigadier]: https://github.com/Mojang/brigadier
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..2900669
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,10 @@
+pub mod strcursor;
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        let result = 2 + 2;
+        assert_eq!(result, 4);
+    }
+}
diff --git a/src/strcursor.rs b/src/strcursor.rs
new file mode 100644
index 0000000..f8b95c6
--- /dev/null
+++ b/src/strcursor.rs
@@ -0,0 +1,368 @@
+// Copyright (c) 2021 Soni L.
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+//! String Cursor (sorta).
+
+use ::std::io::Cursor;
+use ::std::str::FromStr;
+
+/// Built-in `StringReader` errors.
+pub trait ReadError<'a, C: StringReader<'a>>: Sized + std::error::Error {
+    /// Creates an error that indicates an invalid integer was found.
+    fn invalid_integer(context: &C, from: &str) -> Self;
+    /// Creates an error that indicates an integer was expected.
+    fn expected_integer(context: &C) -> Self;
+    /// Creates an error that indicates an invalid float was found.
+    fn invalid_float(context: &C, from: &str) -> Self;
+    /// Creates an error that indicates a float was expected.
+    fn expected_float(context: &C) -> Self;
+    /// Creates an error that indicates an invalid bool was found.
+    fn invalid_bool(context: &C, from: &str) -> Self;
+    /// Creates an error that indicates a bool was expected.
+    fn expected_bool(context: &C) -> Self;
+    /// Creates an error that indicates the start of a quote was expected.
+    fn expected_start_of_quote(context: &C) -> Self;
+    /// Creates an error that indicates the end of a quote was expected.
+    fn expected_end_of_quote(context: &C) -> Self;
+    /// Creates an error that indicates an invalid escape was found.
+    fn invalid_escape(context: &C, from: &str) -> Self;
+    /// Creates an error that indicates a symbol was expected.
+    fn expected_symbol(context: &C, from: &str) -> Self;
+}
+
+/// Extension trait on [`Cursor`]s to help with command parsing.
+///
+/// All `read_*` methods reset the cursor on error.
+///
+/// Note that, compared to Brigadier, this lacks methods such as
+/// `getRemainingLength` (use `get_remaining().len()` or
+/// `remaining_slice().len()`) and `getTotalLength` (use `get_ref().len()`).
+pub trait StringReader<'a>: Sized {
+    /// Returns the part of the string that has been read so far.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    //#[inline]
+    fn get_read(&self) -> &'a str;
+    /// Returns the part of the string that has yet to be read.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    //#[inline]
+    fn get_remaining(&self) -> &'a str;
+    /// Returns whether there's anything left to read.
+    #[inline]
+    fn can_read(&self) -> bool {
+        self.can_read_n(1)
+    }
+    /// Returns whether there's enough left to read, based on the passed length.
+    //#[inline]
+    fn can_read_n(&self, len: usize) -> bool;
+    /// Returns the next char.
+    ///
+    /// # Panics
+    ///
+    /// Panics if there's nothing left to read, or if this cursor is not on an
+    /// UTF-8 character boundary.
+    #[inline]
+    fn peek(&self) -> char {
+        self.peek_n(0)
+    }
+    /// Returns the next nth **byte** (and, if needed, subsequent bytes) as a
+    /// char.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the offset is beyond the boundaries of the buffer, or if the
+    /// offset is not on an UTF-8 character boundary.
+    //#[inline]
+    fn peek_n(&self, offset: usize) -> char;
+
+    /// Advances to the next char.
+    ///
+    /// # Panics
+    ///
+    /// Panics if there's nothing left to read, or if this cursor is not on an
+    /// UTF-8 character boundary.
+    //#[inline]
+    fn skip(&mut self);
+    /// Attempts to read the next char.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    //#[inline]
+    fn read_char(&mut self) -> Option<char>;
+    /// Checks the next char.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    fn expect<E: ReadError<'a, Self>>(&mut self, c: char) -> Result<(), E> {
+        if !self.can_read() || self.peek() != c {
+            // because we want the error constructors to take &str.
+            let mut buf = [0u8; 4];
+            Err(E::expected_symbol(self, c.encode_utf8(&mut buf)))
+        } else {
+            Ok(self.skip())
+        }
+    }
+    /// Skips whitespace.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    fn skip_whitespace(&mut self) {
+        // FIXME figure out if we wanna use the same whitespace rules as
+        // brigadier, because rust uses unicode whereas java uses java rules.
+        while self.can_read() && self.peek().is_whitespace() {
+            self.skip();
+        }
+    }
+
+    /// Reads an integer.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    fn read_integer<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E>
+    where T: FromStr<Err=std::num::ParseIntError>;
+    /// Reads a float.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    fn read_float<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E>
+    where T: FromStr<Err=std::num::ParseFloatError>;
+    /// Reads a bool.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    fn read_bool<E: ReadError<'a, Self>>(&mut self) -> Result<bool, E>;
+    /// Reads an unquoted string.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    // this is a bit of a weird one in that it can't error.
+    fn read_unquoted_str(&mut self) -> &'a str;
+    /// Reads a quoted string.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    fn read_quoted_string<E: ReadError<'a, Self>>(
+        &mut self,
+    ) -> Result<String, E>;
+    /// Reads a quoted or an unquoted string.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this cursor is not on an UTF-8 character boundary.
+    fn read_string<E: ReadError<'a, Self>>(&mut self) -> Result<String, E>;
+}
+
+impl<'a> StringReader<'a> for Cursor<&'a str> {
+    #[inline]
+    fn get_read(&self) -> &'a str {
+        &self.get_ref()[..(self.position() as usize)]
+    }
+    #[inline]
+    fn get_remaining(&self) -> &'a str {
+        &self.get_ref()[(self.position() as usize)..]
+    }
+    #[inline]
+    fn can_read_n(&self, len: usize) -> bool {
+        // NOTE: NOT overflow-aware!
+        self.position() as usize + len <= self.get_ref().len()
+    }
+    #[inline]
+    fn peek_n(&self, offset: usize) -> char {
+        // NOTE: NOT overflow-aware!
+        self.get_ref()[(self.position() as usize + offset)..]
+            .chars().next().unwrap()
+    }
+
+    #[inline]
+    fn skip(&mut self) {
+        self.set_position(self.position() + self.peek().len_utf8() as u64);
+    }
+    #[inline]
+    fn read_char(&mut self) -> Option<char> {
+        let res = self.get_ref()[(self.position() as usize)..].chars().next();
+        if let Some(c) = res {
+            self.set_position(self.position() + c.len_utf8() as u64);
+        }
+        res
+    }
+
+    fn read_integer<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E>
+    where T: FromStr<Err=std::num::ParseIntError> {
+        // see read_unquoted_str for rationale
+        let start = self.position() as usize;
+        let total = self.get_ref().len();
+        let end = total - {
+            self.get_remaining().trim_start_matches(number_chars).len()
+        };
+        self.set_position(end as u64);
+
+        let number = &self.get_ref()[start..end];
+        if number.is_empty() {
+            // don't need to set_position here, we haven't moved
+            Err(E::expected_integer(self))
+        } else {
+            number.parse().map_err(|_| {
+                self.set_position(start as u64);
+                E::invalid_integer(self, number)
+            })
+        }
+    }
+    fn read_float<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E>
+    where T: FromStr<Err=std::num::ParseFloatError> {
+        // see read_unquoted_str for rationale
+        let start = self.position() as usize;
+        let total = self.get_ref().len();
+        let end = total - {
+            self.get_remaining().trim_start_matches(number_chars).len()
+        };
+        self.set_position(end as u64);
+
+        let number = &self.get_ref()[start..end];
+        if number.is_empty() {
+            // don't need to set_position here, we haven't moved
+            Err(E::expected_float(self))
+        } else {
+            number.parse().map_err(|_| {
+                self.set_position(start as u64);
+                E::invalid_float(self, number)
+            })
+        }
+    }
+    fn read_bool<E: ReadError<'a, Self>>(&mut self) -> Result<bool, E> {
+        let pos = self.position();
+        // NOTE: brigadier also allows quoted strings for bools.
+        // we consider that a bug, so we don't.
+        let res = match self.read_unquoted_str() {
+            "true" => Ok(true),
+            "false" => Ok(false),
+            "" => Err(E::expected_bool(self)),
+            value => {
+                self.set_position(pos);
+                Err(E::invalid_bool(self, value))
+            },
+        };
+        res
+    }
+    fn read_unquoted_str(&mut self) -> &'a str {
+        // there's no easy way to grab start matches, so we have to do something
+        // a bit more involved.
+        let start = self.position() as usize;
+        let total = self.get_ref().len();
+        let end = total - {
+            self.get_remaining().trim_start_matches(unquoted_chars).len()
+        };
+        self.set_position(end as u64);
+        &self.get_ref()[start..end]
+    }
+    fn read_quoted_string<E: ReadError<'a, Self>>(
+        &mut self,
+    ) -> Result<String, E> {
+        if !self.can_read() {
+            Ok("".into())
+        } else if quote_chars(self.peek()) {
+            let start = self.position() as usize;
+            let terminator = self.read_char().unwrap();
+            let res = read_string_until(self, terminator);
+            if res.is_err() {
+                self.set_position(start as u64);
+            }
+            res
+        } else {
+            Err(E::expected_start_of_quote(self))
+        }
+    }
+    fn read_string<E: ReadError<'a, Self>>(&mut self) -> Result<String, E> {
+        if !self.can_read() {
+            Ok("".into())
+        } else if quote_chars(self.peek()) {
+            let start = self.position() as usize;
+            let terminator = self.read_char().unwrap();
+            let res = read_string_until(self, terminator);
+            if res.is_err() {
+                self.set_position(start as u64);
+            }
+            res
+        } else {
+            Ok(self.read_unquoted_str().into())
+        }
+    }
+}
+
+fn read_string_until<'a, E: ReadError<'a, Cursor<&'a str>>>(
+    this: &mut Cursor<&'a str>,
+    terminator: char,
+) -> Result<String, E> {
+    let mut result = String::new();
+    let mut escaped = false;
+
+    while let Some(c) = this.read_char() {
+        if escaped {
+            if c == terminator || escape_char(c) {
+                result.push(c);
+                escaped = false;
+            } else {
+                let mut buf = [0u8; 4];
+                // NOTE: brigadier unskips the escape. we don't bother.
+                return Err(E::invalid_escape(this, c.encode_utf8(&mut buf)));
+            }
+        } else if escape_char(c) {
+            escaped = true;
+        } else if c == terminator {
+            return Ok(result);
+        } else {
+            result.push(c);
+        }
+    }
+
+    Err(E::expected_end_of_quote(this))
+}
+
+/// Symbols allowed in unquoted strings.
+#[inline]
+fn unquoted_chars(c: char) -> bool {
+    matches!(
+        c,
+        '0' ..= '9' | 'A' ..= 'Z' | 'a' ..= 'z' | '_' | '-' | '.' | '+',
+    )
+}
+
+/// Symbols allowed in numbers.
+#[inline]
+fn number_chars(c: char) -> bool {
+    matches!(
+        c,
+        '0' ..= '9' | '-' | '.',
+    )
+}
+
+/// Symbols allowed to start/end a quoted string.
+#[inline]
+fn quote_chars(c: char) -> bool {
+    matches!(
+        c,
+        '"' | '\'',
+    )
+}
+
+/// Symbol allowed to escape other symbols.
+#[inline]
+fn escape_char(c: char) -> bool {
+    matches!(
+        c,
+        '\\',
+    )
+}
diff --git a/tests/common/errorfunc.rs b/tests/common/errorfunc.rs
new file mode 100644
index 0000000..99ba641
--- /dev/null
+++ b/tests/common/errorfunc.rs
@@ -0,0 +1,89 @@
+// Copyright (c) 2021 Soni L.
+
+use ::std::marker::PhantomData;
+
+use ::iosonism::strcursor::ReadError;
+use ::iosonism::strcursor::StringReader;
+
+/// An error callback.
+pub trait ErrorFunc<'a, C: StringReader<'a>> {
+    fn call<'b>(context: &C, ty: ErrorType<'b>);
+}
+
+/// An implementation of various Iosonism errors that calls T.
+pub struct ErrorCall<T>(PhantomData<T>);
+
+#[non_exhaustive]
+#[derive(PartialEq, Eq, Debug)]
+pub enum ErrorType<'a> {
+    InvalidInteger(&'a str),
+    ExpectedInteger,
+    InvalidFloat(&'a str),
+    ExpectedFloat,
+    InvalidBool(&'a str),
+    ExpectedBool,
+    ExpectedStartOfQuote,
+    ExpectedEndOfQuote,
+    InvalidEscape(&'a str),
+    ExpectedSymbol(&'a str),
+}
+
+impl<T> ::std::fmt::Display for ErrorCall<T> {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        write!(f, "error!")
+    }
+}
+
+impl<T> ::std::fmt::Debug for ErrorCall<T> {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        write!(f, "ErrorCall")
+    }
+}
+
+impl<T> ::std::error::Error for ErrorCall<T> {
+}
+
+impl<'a, C: StringReader<'a>, T> ReadError<'a, C> for ErrorCall<T>
+where T: ErrorFunc<'a, C> {
+    fn invalid_integer(context: &C, from: &str) -> Self {
+        T::call(context, ErrorType::InvalidInteger(from));
+        Self(PhantomData)
+    }
+    fn expected_integer(context: &C) -> Self {
+        T::call(context, ErrorType::ExpectedInteger);
+        Self(PhantomData)
+    }
+    fn invalid_float(context: &C, from: &str) -> Self {
+        T::call(context, ErrorType::InvalidFloat(from));
+        Self(PhantomData)
+    }
+    fn expected_float(context: &C) -> Self {
+        T::call(context, ErrorType::ExpectedFloat);
+        Self(PhantomData)
+    }
+    fn invalid_bool(context: &C, from: &str) -> Self {
+        T::call(context, ErrorType::InvalidBool(from));
+        Self(PhantomData)
+    }
+    fn expected_bool(context: &C) -> Self {
+        T::call(context, ErrorType::ExpectedBool);
+        Self(PhantomData)
+    }
+    fn expected_start_of_quote(context: &C) -> Self {
+        T::call(context, ErrorType::ExpectedStartOfQuote);
+        Self(PhantomData)
+    }
+    fn expected_end_of_quote(context: &C) -> Self {
+        T::call(context, ErrorType::ExpectedEndOfQuote);
+        Self(PhantomData)
+    }
+    fn invalid_escape(context: &C, from: &str) -> Self {
+        T::call(context, ErrorType::InvalidEscape(from));
+        Self(PhantomData)
+    }
+    fn expected_symbol(context: &C, from: &str) -> Self {
+        T::call(context, ErrorType::ExpectedSymbol(from));
+        Self(PhantomData)
+    }
+}
+
diff --git a/tests/common/errorpanic.rs b/tests/common/errorpanic.rs
new file mode 100644
index 0000000..202c4be
--- /dev/null
+++ b/tests/common/errorpanic.rs
@@ -0,0 +1,114 @@
+// Copyright (c) 2021 Soni L.
+
+use ::iosonism::strcursor::ReadError;
+use ::iosonism::strcursor::StringReader;
+
+/// An implementation of various Iosonism errors that just panics.
+#[derive(Debug)]
+pub enum ErrorPanic {
+    // uninhabitable!
+}
+
+impl ::std::fmt::Display for ErrorPanic {
+    fn fmt(&self, _: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        match *self {
+        }
+    }
+}
+
+impl ::std::error::Error for ErrorPanic {
+}
+
+impl<'a, C: StringReader<'a>> ReadError<'a, C> for ErrorPanic {
+    fn invalid_integer(context: &C, from: &str) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!(
+                "invalid integer: {} at ...{}",
+                from,
+                context.get_remaining(),
+            );
+        } else {
+            panic!("invalid integer: {}", from);
+        }
+    }
+    fn expected_integer(context: &C) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!("expected integer at ...{}", context.get_remaining());
+        } else {
+            panic!("expected integer");
+        }
+    }
+    fn invalid_float(context: &C, from: &str) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!(
+                "invalid float: {} at ...{}",
+                from,
+                context.get_remaining(),
+            );
+        } else {
+            panic!("invalid float: {}", from);
+        }
+    }
+    fn expected_float(context: &C) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!("expected float at ...{}", context.get_remaining());
+        } else {
+            panic!("expected float");
+        }
+    }
+    fn invalid_bool(context: &C, from: &str) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!(
+                "invalid bool: {} at ...{}",
+                from,
+                context.get_remaining(),
+            );
+        } else {
+            panic!("invalid bool: {}", from);
+        }
+    }
+    fn expected_bool(context: &C) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!("expected bool at ...{}", context.get_remaining());
+        } else {
+            panic!("expected bool");
+        }
+    }
+    fn expected_start_of_quote(context: &C) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!("expected start of quote at ...{}", context.get_remaining());
+        } else {
+            panic!("expected start of quote");
+        }
+    }
+    fn expected_end_of_quote(context: &C) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!("expected end of quote at ...{}", context.get_remaining());
+        } else {
+            panic!("expected end of quote");
+        }
+    }
+    fn invalid_escape(context: &C, from: &str) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!(
+                "invalid escape: {} at ...{}",
+                from,
+                context.get_remaining(),
+            );
+        } else {
+            panic!("invalid escape: {}", from);
+        }
+    }
+    fn expected_symbol(context: &C, from: &str) -> Self {
+        if !context.get_remaining().is_empty() {
+            panic!(
+                "expected symbol: {} at ...{}",
+                from,
+                context.get_remaining(),
+            );
+        } else {
+            panic!("expected symbol: {}", from);
+        }
+    }
+}
+
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
new file mode 100644
index 0000000..6760da1
--- /dev/null
+++ b/tests/common/mod.rs
@@ -0,0 +1,12 @@
+// Copyright (c) 2021 Soni L.
+
+// see rationale in tests/*.rs
+#![warn(non_snake_case)]
+
+pub mod errorfunc;
+pub mod errorpanic;
+
+pub use self::errorfunc::ErrorCall;
+pub use self::errorfunc::ErrorFunc;
+pub use self::errorfunc::ErrorType;
+pub use self::errorpanic::ErrorPanic;
diff --git a/tests/strcursor.rs b/tests/strcursor.rs
new file mode 100644
index 0000000..3ec876e
--- /dev/null
+++ b/tests/strcursor.rs
@@ -0,0 +1,592 @@
+// Copyright (c) 2021 Soni L.
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+// because we wanna use double underscore (__) for test names
+#![allow(non_snake_case)]
+
+use ::std::io::Cursor;
+
+use ::iosonism::strcursor::StringReader;
+
+mod common;
+
+use self::common::ErrorCall;
+use self::common::ErrorFunc;
+use self::common::ErrorPanic;
+use self::common::ErrorType;
+
+#[test]
+fn test_can_read() {
+    let mut reader = Cursor::new("abc");
+    assert_eq!(reader.can_read(), true);
+    reader.skip(); // 'a'
+    assert_eq!(reader.can_read(), true);
+    reader.skip(); // 'b'
+    assert_eq!(reader.can_read(), true);
+    reader.skip(); // 'c'
+    assert_eq!(reader.can_read(), false);
+}
+
+#[test]
+fn test_get_remaining_len() {
+    let mut reader = Cursor::new("abc");
+    assert_eq!(reader.get_remaining().len(), 3);
+    reader.set_position(1);
+    assert_eq!(reader.get_remaining().len(), 2);
+    reader.set_position(2);
+    assert_eq!(reader.get_remaining().len(), 1);
+    reader.set_position(3);
+    assert_eq!(reader.get_remaining().len(), 0);
+}
+
+#[test]
+fn test_can_read_n() {
+    let reader = Cursor::new("abc");
+    assert_eq!(reader.can_read_n(1), true);
+    assert_eq!(reader.can_read_n(2), true);
+    assert_eq!(reader.can_read_n(3), true);
+    assert_eq!(reader.can_read_n(4), false);
+    assert_eq!(reader.can_read_n(5), false);
+}
+
+#[test]
+fn test_peek() {
+    let mut reader = Cursor::new("abc");
+    assert_eq!(reader.peek(), 'a');
+    assert_eq!(reader.position(), 0);
+    reader.set_position(2);
+    assert_eq!(reader.peek(), 'c');
+    assert_eq!(reader.position(), 2);
+}
+
+#[test]
+fn test_peek_n() {
+    let mut reader = Cursor::new("abc");
+    assert_eq!(reader.peek_n(0), 'a');
+    assert_eq!(reader.peek_n(2), 'c');
+    assert_eq!(reader.position(), 0);
+    reader.set_position(1);
+    assert_eq!(reader.peek_n(1), 'c');
+    assert_eq!(reader.position(), 1);
+}
+
+#[test]
+fn test_read_char() {
+    let mut reader = Cursor::new("abc");
+    assert_eq!(reader.read_char(), Some('a'));
+    assert_eq!(reader.read_char(), Some('b'));
+    assert_eq!(reader.read_char(), Some('c'));
+    assert_eq!(reader.position(), 3);
+}
+
+#[test]
+fn test_skip() {
+    let mut reader = Cursor::new("abc");
+    reader.skip();
+    assert_eq!(reader.position(), 1);
+}
+
+#[test]
+fn test_get_remaining() {
+    let mut reader = Cursor::new("Hello!");
+    assert_eq!(reader.get_remaining(), "Hello!");
+    reader.set_position(3);
+    assert_eq!(reader.get_remaining(), "lo!");
+    reader.set_position(6);
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_get_read() {
+    let mut reader = Cursor::new("Hello!");
+    assert_eq!(reader.get_read(), "");
+    reader.set_position(3);
+    assert_eq!(reader.get_read(), "Hel");
+    reader.set_position(6);
+    assert_eq!(reader.get_read(), "Hello!");
+}
+
+#[test]
+fn test_skip_whitespace__none() {
+    let mut reader = Cursor::new("Hello!");
+    reader.skip_whitespace();
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_skip_whitespace__mixed() {
+    let mut reader = Cursor::new(" \t \t\nHello!");
+    reader.skip_whitespace();
+    assert_eq!(reader.position(), 5);
+}
+
+#[test]
+fn test_skip_whitespace__empty() {
+    let mut reader = Cursor::new("");
+    reader.skip_whitespace();
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_unquoted_str() {
+    let mut reader = Cursor::new("hello world");
+    assert_eq!(reader.read_unquoted_str(), "hello");
+    assert_eq!(reader.get_read(), "hello");
+    assert_eq!(reader.get_remaining(), " world");
+}
+
+#[test]
+fn test_read_unquoted_str__empty() {
+    let mut reader = Cursor::new("");
+    assert_eq!(reader.read_unquoted_str(), "");
+    assert_eq!(reader.get_read(), "");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_unquoted_str__empty_with_remaining() {
+    let mut reader = Cursor::new(" hello world");
+    assert_eq!(reader.read_unquoted_str(), "");
+    assert_eq!(reader.get_read(), "");
+    assert_eq!(reader.get_remaining(), " hello world");
+}
+
+#[test]
+fn test_read_quoted_string() {
+    let mut reader = Cursor::new("\"hello world\"");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "hello world",
+    );
+    assert_eq!(reader.get_read(), "\"hello world\"");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_quoted_string__single() {
+    let mut reader = Cursor::new("'hello world'");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "hello world",
+    );
+    assert_eq!(reader.get_read(), "'hello world'");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_quoted_string__double_inside_single() {
+    let mut reader = Cursor::new("'hello \"world\"'");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "hello \"world\"",
+    );
+    assert_eq!(reader.get_read(), "'hello \"world\"'");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_quoted_string__single_inside_double() {
+    let mut reader = Cursor::new("\"hello 'world'\"");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "hello 'world'",
+    );
+    assert_eq!(reader.get_read(), "\"hello 'world'\"");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_quoted_string__empty() {
+    let mut reader = Cursor::new("");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "",
+    );
+    assert_eq!(reader.get_read(), "");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_quoted_string__empty_quoted() {
+    let mut reader = Cursor::new("\"\"");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "",
+    );
+    assert_eq!(reader.get_read(), "\"\"");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+
+#[test]
+fn test_read_quoted_string__empty_quoted_with_remaining() {
+    let mut reader = Cursor::new("\"\" hello world");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "",
+    );
+    assert_eq!(reader.get_read(), "\"\"");
+    assert_eq!(reader.get_remaining(), " hello world");
+}
+
+#[test]
+fn test_read_quoted_string__with_escaped_quote() {
+    let mut reader = Cursor::new("\"hello \\\"world\\\"\"");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "hello \"world\"",
+    );
+    assert_eq!(reader.get_read(), "\"hello \\\"world\\\"\"");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_quoted_string__with_escaped_escapes() {
+    let mut reader = Cursor::new("\"\\\\o/\"");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "\\o/",
+    );
+    assert_eq!(reader.get_read(), "\"\\\\o/\"");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_quoted_string__with_remaining() {
+    let mut reader = Cursor::new("\"hello world\" foo bar");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "hello world",
+    );
+    assert_eq!(reader.get_read(), "\"hello world\"");
+    assert_eq!(reader.get_remaining(), " foo bar");
+}
+
+#[test]
+fn test_read_quoted_string__with_immediate_remaining() {
+    let mut reader = Cursor::new("\"hello world\"foo bar");
+    assert_eq!(
+        reader.read_quoted_string::<ErrorPanic>().unwrap(),
+        "hello world",
+    );
+    assert_eq!(reader.get_read(), "\"hello world\"");
+    assert_eq!(reader.get_remaining(), "foo bar");
+}
+
+#[test]
+fn test_read_quoted_string__no_open() {
+    let mut reader = Cursor::new("hello world\"");
+    assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::ExpectedStartOfQuote);
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_quoted_string__no_close() {
+    let mut reader = Cursor::new("\"hello world");
+    assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::ExpectedEndOfQuote);
+            assert_eq!(context.position(), 12);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_quoted_string__invalid_escape() {
+    let mut reader = Cursor::new("\"hello\\nworld\"");
+    assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::InvalidEscape("n"));
+            // NOTE: brigadier makes this 7. we make this 8.
+            // FIXME: maybe do the same as brigadier?
+            assert_eq!(context.position(), 8);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_quoted_string__invalid_quote_escape() {
+    let mut reader = Cursor::new("'hello\\\"'world");
+    assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::InvalidEscape("\""));
+            assert_eq!(context.position(), 8);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_string__no_quotes() {
+    let mut reader = Cursor::new("hello world");
+    assert_eq!(
+        reader.read_string::<ErrorPanic>().unwrap(),
+        "hello",
+    );
+    assert_eq!(reader.get_read(), "hello");
+    assert_eq!(reader.get_remaining(), " world");
+}
+
+#[test]
+fn test_read_string__single_quotes() {
+    let mut reader = Cursor::new("'hello world'");
+    assert_eq!(
+        reader.read_string::<ErrorPanic>().unwrap(),
+        "hello world",
+    );
+    assert_eq!(reader.get_read(), "'hello world'");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_string__double_quotes() {
+    let mut reader = Cursor::new("\"hello world\"");
+    assert_eq!(
+        reader.read_string::<ErrorPanic>().unwrap(),
+        "hello world",
+    );
+    assert_eq!(reader.get_read(), "\"hello world\"");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_integer() {
+    let mut reader = Cursor::new("1234567890");
+    assert_eq!(
+        reader.read_integer::<i32, ErrorPanic>().unwrap(),
+        1234567890,
+    );
+    assert_eq!(reader.get_read(), "1234567890");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_integer__negative() {
+    let mut reader = Cursor::new("-1234567890");
+    assert_eq!(
+        reader.read_integer::<i32, ErrorPanic>().unwrap(),
+        -1234567890,
+    );
+    assert_eq!(reader.get_read(), "-1234567890");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+
+#[test]
+fn test_read_integer__invalid() {
+    let mut reader = Cursor::new("12.34");
+    assert!(reader.read_integer::<i32, ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::InvalidInteger("12.34"));
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_integer__none() {
+    let mut reader = Cursor::new("");
+    assert!(reader.read_integer::<i32, ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::ExpectedInteger);
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_integer__with_remaining() {
+    let mut reader = Cursor::new("1234567890 foo bar");
+    assert_eq!(
+        reader.read_integer::<i32, ErrorPanic>().unwrap(),
+        1234567890,
+    );
+    assert_eq!(reader.get_read(), "1234567890");
+    assert_eq!(reader.get_remaining(), " foo bar");
+}
+
+#[test]
+fn test_read_integer__with_remaining_immediate() {
+    let mut reader = Cursor::new("1234567890foo bar");
+    assert_eq!(
+        reader.read_integer::<i32, ErrorPanic>().unwrap(),
+        1234567890,
+    );
+    assert_eq!(reader.get_read(), "1234567890");
+    assert_eq!(reader.get_remaining(), "foo bar");
+}
+
+#[test]
+fn test_read_float() {
+    let mut reader = Cursor::new("123");
+    assert_eq!(
+        reader.read_float::<f32, ErrorPanic>().unwrap(),
+        123.0,
+    );
+    assert_eq!(reader.get_read(), "123");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_float__with_decimal() {
+    let mut reader = Cursor::new("12.34");
+    assert_eq!(
+        reader.read_float::<f32, ErrorPanic>().unwrap(),
+        12.34,
+    );
+    assert_eq!(reader.get_read(), "12.34");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_float__negative() {
+    let mut reader = Cursor::new("-123");
+    assert_eq!(
+        reader.read_float::<f32, ErrorPanic>().unwrap(),
+        -123.0,
+    );
+    assert_eq!(reader.get_read(), "-123");
+    assert_eq!(reader.get_remaining(), "");
+}
+
+#[test]
+fn test_read_float__invalid() {
+    let mut reader = Cursor::new("12.34.56");
+    assert!(reader.read_float::<f32, ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::InvalidFloat("12.34.56"));
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_float__none() {
+    let mut reader = Cursor::new("");
+    assert!(reader.read_float::<f32, ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::ExpectedFloat);
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_float__with_remaining() {
+    let mut reader = Cursor::new("12.34 foo bar");
+    assert_eq!(
+        reader.read_float::<f32, ErrorPanic>().unwrap(),
+        12.34,
+    );
+    assert_eq!(reader.get_read(), "12.34");
+    assert_eq!(reader.get_remaining(), " foo bar");
+}
+
+#[test]
+fn test_read_float__with_remaining_immediate() {
+    let mut reader = Cursor::new("12.34foo bar");
+    assert_eq!(
+        reader.read_float::<f32, ErrorPanic>().unwrap(),
+        12.34,
+    );
+    assert_eq!(reader.get_read(), "12.34");
+    assert_eq!(reader.get_remaining(), "foo bar");
+}
+
+#[test]
+fn test_expect__correct() {
+    let mut reader = Cursor::new("abc");
+    assert!(reader.expect::<ErrorPanic>('a').is_ok());
+    assert_eq!(reader.position(), 1);
+}
+
+#[test]
+fn test_expect__incorrect() {
+    let mut reader = Cursor::new("bcd");
+    assert!(reader.expect::<ErrorCall<ErrFn>>('a').is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::ExpectedSymbol("a"));
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_expect__none() {
+    let mut reader = Cursor::new("");
+    assert!(reader.expect::<ErrorCall<ErrFn>>('a').is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::ExpectedSymbol("a"));
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_bool__correct() {
+    let mut reader = Cursor::new("true");
+    assert_eq!(reader.read_bool::<ErrorPanic>().unwrap(), true);
+    assert_eq!(reader.get_read(), "true");
+}
+
+#[test]
+fn test_read_bool__incorrect() {
+    let mut reader = Cursor::new("tuesday");
+    assert!(reader.read_bool::<ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::InvalidBool("tuesday"));
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}
+
+#[test]
+fn test_read_bool__none() {
+    let mut reader = Cursor::new("");
+    assert!(reader.read_bool::<ErrorCall<ErrFn>>().is_err());
+    struct ErrFn;
+    impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn {
+        fn call(context: &Cursor<&'a str>, ty: ErrorType) {
+            assert_eq!(ty, ErrorType::ExpectedBool);
+            assert_eq!(context.position(), 0);
+        }
+    }
+    assert_eq!(reader.position(), 0);
+}