summaryrefslogtreecommitdiffstats
path: root/ipc
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2020-08-17 12:44:22 +0200
committerJustus Winter <justus@sequoia-pgp.org>2020-08-17 16:19:24 +0200
commitbc93b8a039753a626406f15a346f84ecaed49583 (patch)
treede89a8d571aac56f402baf2d6327abb838c64ce6 /ipc
parent5c80cd51fa5453be3207bb2c9491fbe8d9c0afe3 (diff)
openpgp: Move crypto::sexp to the ipc crate.
- This is only used to communicate with the GnuPG agent, so it should not be in the openpgp crate.
Diffstat (limited to 'ipc')
-rw-r--r--ipc/Cargo.toml3
-rw-r--r--ipc/build.rs36
-rw-r--r--ipc/src/gnupg.rs2
-rw-r--r--ipc/src/lib.rs26
-rw-r--r--ipc/src/sexp.rs436
-rw-r--r--ipc/src/sexp/parse/grammar.lalrpop64
-rw-r--r--ipc/src/sexp/parse/lexer.rs153
-rw-r--r--ipc/src/sexp/parse/mod.rs144
-rw-r--r--ipc/src/sexp/serialize.rs173
-rw-r--r--ipc/src/tests.rs41
-rw-r--r--ipc/tests/data/sexp/dsa-signature.sexp1
-rw-r--r--ipc/tests/data/sexp/ecdsa-signature.sexp2
-rw-r--r--ipc/tests/data/sexp/eddsa-signature.sexp1
-rw-r--r--ipc/tests/data/sexp/rsa-signature.sexp3
14 files changed, 1084 insertions, 1 deletions
diff --git a/ipc/Cargo.toml b/ipc/Cargo.toml
index b21fcfd1..ef5ed0ea 100644
--- a/ipc/Cargo.toml
+++ b/ipc/Cargo.toml
@@ -24,10 +24,12 @@ sequoia-openpgp = { path = "../openpgp", version = "0.18" }
sequoia-core = { path = "../core", version = "0.18" }
anyhow = "1"
+buffered-reader = { path = "../buffered-reader", version = "0.18", default-features = false }
capnp-rpc = "0.10"
fs2 = "0.4.2"
futures = "0.1"
lalrpop-util = "0.17"
+lazy_static = "1.3"
libc = "0.2.33"
memsec = "0.5.6"
rand = { version = "0.7", default-features = false }
@@ -52,3 +54,4 @@ lalrpop = "0.17"
[dev-dependencies]
clap = "2.32.0"
+quickcheck = { version = "0.9", default-features = false }
diff --git a/ipc/build.rs b/ipc/build.rs
index 23c7d3f8..5c8a9ffb 100644
--- a/ipc/build.rs
+++ b/ipc/build.rs
@@ -1,5 +1,41 @@
+use std::env;
+use std::fs;
+use std::io::{self, Write};
+use std::path::PathBuf;
extern crate lalrpop;
fn main() {
lalrpop::process_root().unwrap();
+ include_test_data().unwrap();
+}
+
+/// Builds the index of the test data for use with the `::tests`
+/// module.
+fn include_test_data() -> io::Result<()> {
+ let cwd = env::current_dir()?;
+ let mut sink = fs::File::create(
+ PathBuf::from(env::var_os("OUT_DIR").unwrap())
+ .join("tests.index.rs.inc")).unwrap();
+
+ writeln!(&mut sink, "{{")?;
+ let mut dirs = vec![PathBuf::from("tests/data")];
+ while let Some(dir) = dirs.pop() {
+ println!("rerun-if-changed={}", dir.to_str().unwrap());
+ for entry in fs::read_dir(dir).unwrap() {
+ let entry = entry?;
+ let path = entry.path();
+ if path.is_file() {
+ writeln!(
+ &mut sink, " add!({:?}, {:?});",
+ path.components().skip(2)
+ .map(|c| c.as_os_str().to_str().expect("valid UTF-8"))
+ .collect::<Vec<_>>().join("/"),
+ cwd.join(path))?;
+ } else if path.is_dir() {
+ dirs.push(path.clone());
+ }
+ }
+ }
+ writeln!(&mut sink, "}}")?;
+ Ok(())
}
diff --git a/ipc/src/gnupg.rs b/ipc/src/gnupg.rs
index 4b565553..b81e0927 100644
--- a/ipc/src/gnupg.rs
+++ b/ipc/src/gnupg.rs
@@ -14,13 +14,13 @@ use sequoia_openpgp as openpgp;
use openpgp::types::HashAlgorithm;
use openpgp::fmt::hex;
use openpgp::crypto;
-use openpgp::crypto::sexp::Sexp;
use openpgp::packet::prelude::*;
use openpgp::parse::Parse;
use openpgp::serialize::Serialize;
use crate::Result;
use crate::assuan;
+use crate::sexp::Sexp;
/// A GnuPG context.
#[derive(Debug)]
diff --git a/ipc/src/lib.rs b/ipc/src/lib.rs
index 7e6a62ab..f0d2c2b6 100644
--- a/ipc/src/lib.rs
+++ b/ipc/src/lib.rs
@@ -65,9 +65,35 @@ use std::thread;
use sequoia_openpgp as openpgp;
use sequoia_core as core;
+// Turns an `if let` into an expression so that it is possible to do
+// things like:
+//
+// ```rust,nocompile
+// if destructures_to(Foo::Bar(_) = value)
+// || destructures_to(Foo::Bam(_) = value) { ... }
+// ```
+// TODO: Replace with `std::matches!` once MSRV is bumped to 1.42.
+#[cfg(test)]
+macro_rules! destructures_to {
+ ( $error: pat = $expr:expr ) => {
+ {
+ let x = $expr;
+ if let $error = x {
+ true
+ } else {
+ false
+ }
+ }
+ };
+}
+
#[macro_use] mod trace;
pub mod assuan;
pub mod gnupg;
+pub mod sexp;
+
+#[cfg(test)]
+mod tests;
macro_rules! platform {
{ unix => { $($unix:tt)* }, windows => { $($windows:tt)* } } => {
diff --git a/ipc/src/sexp.rs b/ipc/src/sexp.rs
new file mode 100644
index 00000000..38429557
--- /dev/null
+++ b/ipc/src/sexp.rs
@@ -0,0 +1,436 @@
+//! *S-Expressions* for communicating cryptographic primitives.
+//!
+//! *S-Expressions* as described in the internet draft [S-Expressions],
+//! are a way to communicate cryptographic primitives like keys,
+//! signatures, and ciphertexts between agents or implementations.
+//!
+//! [S-Expressions]: https://people.csail.mit.edu/rivest/Sexp.txt
+
+use std::convert::TryFrom;
+use std::fmt;
+use std::ops::Deref;
+
+#[cfg(test)]
+use quickcheck::{Arbitrary, Gen};
+
+use sequoia_openpgp as openpgp;
+use openpgp::crypto::{mpi, SessionKey};
+use openpgp::crypto::mem::Protected;
+
+use openpgp::Error;
+use openpgp::Result;
+
+mod parse;
+mod serialize;
+
+/// An *S-Expression*.
+///
+/// An *S-Expression* is either a string, or a list of *S-Expressions*.
+#[derive(Clone, PartialEq, Eq)]
+pub enum Sexp {
+ /// Just a string.
+ String(String_),
+ /// A list of *S-Expressions*.
+ List(Vec<Sexp>),
+}
+
+impl fmt::Debug for Sexp {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Sexp::String(ref s) => s.fmt(f),
+ Sexp::List(ref l) => l.fmt(f),
+ }
+ }
+}
+
+impl Sexp {
+ /// Completes the decryption of this S-Expression representing a
+ /// wrapped session key.
+ ///
+ /// Such an expression is returned from gpg-agent's `PKDECRYPT`
+ /// command. `padding` must be set according to the status
+ /// messages sent.
+ pub fn finish_decryption<R>(&self,
+ recipient: &openpgp::packet::Key<
+ openpgp::packet::key::PublicParts, R>,
+ ciphertext: &mpi::Ciphertext,
+ padding: bool)
+ -> Result<SessionKey>
+ where R: openpgp::packet::key::KeyRole
+ {
+ use openpgp::crypto::mpi::PublicKey;
+ let not_a_session_key = || -> anyhow::Error {
+ Error::MalformedMPI(
+ format!("Not a session key: {:?}", self)).into()
+ };
+
+ let value = self.get(b"value")?.ok_or_else(not_a_session_key)?
+ .into_iter().nth(0).ok_or_else(not_a_session_key)?;
+
+ match value {
+ Sexp::String(ref s) => match recipient.mpis() {
+ PublicKey::RSA { .. } | PublicKey::ElGamal { .. } if padding =>
+ {
+ // The session key is padded. The format is
+ // described in g10/pubkey-enc.c (note that we,
+ // like GnuPG 2.2, only support the new encoding):
+ //
+ // * Later versions encode the DEK like this:
+ // *
+ // * 0 2 RND(n bytes) [...]
+ // *
+ // * (mpi_get_buffer already removed the leading zero).
+ // *
+ // * RND are non-zero random bytes.
+ let mut s = &s[..];
+
+ // The leading 0 may or may not be swallowed along
+ // the way due to MPI encoding.
+ if s[0] == 0 {
+ s = &s[1..];
+ }
+
+ // Version.
+ if s[0] != 2 {
+ return Err(Error::MalformedMPI(
+ format!("DEK encoding version {} not understood",
+ s[0])).into());
+ }
+
+ // Skip non-zero bytes.
+ while s.len() > 0 && s[0] > 0 {
+ s = &s[1..];
+ }
+
+ if s.len() == 0 {
+ return Err(Error::MalformedMPI(
+ "Invalid DEK encoding, no zero found".into())
+ .into());
+ }
+
+ // Skip zero.
+ s = &s[1..];
+
+ Ok(s.to_vec().into())
+ },
+
+ PublicKey::RSA { .. } | PublicKey::ElGamal { .. } => {
+ // The session key is not padded. Currently, this
+ // happens if the session key is decrypted using
+ // scdaemon.
+ assert!(! padding); // XXX: Don't assert that.
+ Ok(s.to_vec().into())
+ },
+
+ PublicKey::ECDH { curve, .. } => {
+ // The shared point has been computed by the
+ // remote agent. The shared point is not padded.
+ let s_: mpi::ProtectedMPI = s.to_vec().into();
+ #[allow(non_snake_case)]
+ let S: Protected = s_.decode_point(curve)?.0.into();
+ // XXX: Erase shared point from s.
+
+ // Now finish the decryption.
+ openpgp::crypto::ecdh::decrypt_shared(recipient, &S, ciphertext)
+ },
+
+ _ =>
+ Err(Error::InvalidArgument(
+ format!("Don't know how to handle key {:?}", recipient))
+ .into()),
+ }
+ Sexp::List(..) => Err(not_a_session_key()),
+ }
+ }
+
+ /// Parses this s-expression to a signature.
+ ///
+ /// Such an expression is returned from gpg-agent's `PKSIGN`
+ /// command.
+ pub fn to_signature(&self) -> Result<mpi::Signature> {
+ let not_a_signature = || -> anyhow::Error {
+ Error::MalformedMPI(
+ format!("Not a signature: {:?}", self)).into()
+ };
+
+ let sig = self.get(b"sig-val")?.ok_or_else(not_a_signature)?
+ .into_iter().nth(0).ok_or_else(not_a_signature)?;
+
+ if let Some(param) = sig.get(b"eddsa")? {
+ let r = param.iter().find_map(|p| {
+ p.get(b"r").ok().unwrap_or_default()
+ .and_then(|l| l.get(0).and_then(Sexp::string).cloned())
+ }).ok_or_else(not_a_signature)?;
+ let s = param.iter().find_map(|p| {
+ p.get(b"s").ok().unwrap_or_default()
+ .and_then(|l| l.get(0).and_then(Sexp::string).cloned())
+ }).ok_or_else(not_a_signature)?;
+ Ok(mpi::Signature::EdDSA {
+ r: mpi::MPI::new(&r),
+ s: mpi::MPI::new(&s),
+ })
+ } else if let Some(param) = sig.get(b"ecdsa")? {
+ let r = param.iter().find_map(|p| {
+ p.get(b"r").ok().unwrap_or_default()
+ .and_then(|l| l.get(0).and_then(Sexp::string).cloned())
+ }).ok_or_else(not_a_signature)?;
+ let s = param.iter().find_map(|p| {
+ p.get(b"s").ok().unwrap_or_default()
+ .and_then(|l| l.get(0).and_then(Sexp::string).cloned())
+ }).ok_or_else(not_a_signature)?;
+ Ok(mpi::Signature::ECDSA {
+ r: mpi::MPI::new(&r),
+ s: mpi::MPI::new(&s),
+ })
+ } else if let Some(param) = sig.get(b"rsa")? {
+ let s = param.iter().find_map(|p| {
+ p.get(b"s").ok().unwrap_or_default()
+ .and_then(|l| l.get(0).and_then(Sexp::string).cloned())
+ }).ok_or_else(not_a_signature)?;
+ Ok(mpi::Signature::RSA {
+ s: mpi::MPI::new(&s),
+ })
+ } else if let Some(param) = sig.get(b"dsa")? {
+ let r = param.iter().find_map(|p| {
+ p.get(b"r").ok().unwrap_or_default()
+ .and_then(|l| l.get(0).and_then(Sexp::string).cloned())
+ }).ok_or_else(not_a_signature)?;
+ let s = param.iter().find_map(|p| {
+ p.get(b"s").ok().unwrap_or_default()
+ .and_then(|l| l.get(0).and_then(Sexp::string).cloned())
+ }).ok_or_else(not_a_signature)?;
+ Ok(mpi::Signature::DSA {
+ r: mpi::MPI::new(&r),
+ s: mpi::MPI::new(&s),
+ })
+ } else {
+ Err(Error::MalformedMPI(
+ format!("Unknown signature sexp: {:?}", self)).into())
+ }
+ }
+
+ /// Casts this to a string.
+ pub fn string(&self) -> Option<&String_> {
+ match self {
+ Sexp::String(ref s) => Some(s),
+ _ => None,
+ }
+ }
+
+ /// Casts this to a list.
+ pub fn list(&self) -> Option<&[Sexp]> {
+ match self {
+ Sexp::List(ref s) => Some(s.as_slice()),
+ _ => None,
+ }
+ }
+
+ /// Given an alist, selects by key and returns the value.
+ fn get(&self, key: &[u8]) -> Result<Option<Vec<Sexp>>> {
+ match self {
+ Sexp::List(ref ll) => match ll.get(0) {
+ Some(Sexp::String(ref tag)) =>
+ if tag.deref() == key {
+ Ok(Some(ll[1..].iter().cloned().collect()))
+ } else {
+ Ok(None)
+ }
+ _ =>
+ Err(Error::InvalidArgument(
+ format!("Malformed alist: {:?}", ll)).into()),
+ },
+ _ =>
+ Err(Error::InvalidArgument(
+ format!("Malformed alist: {:?}", self)).into()),
+ }
+ }
+}
+
+impl TryFrom<&mpi::Ciphertext> for Sexp {
+ type Error = anyhow::Error;
+
+ /// Constructs an S-Expression representing `ciphertext`.
+ ///
+ /// The resulting expression is suitable for gpg-agent's `INQUIRE
+ /// CIPHERTEXT` inquiry.
+ fn try_from(ciphertext: &mpi::Ciphertext) -> Result<Self> {
+ use openpgp::crypto::mpi::Ciphertext::*;
+ match ciphertext {
+ RSA { ref c } =>
+ Ok(Sexp::List(vec![
+ Sexp::String("enc-val".into()),
+ Sexp::List(vec![
+ Sexp::String("rsa".into()),
+ Sexp::List(vec![
+ Sexp::String("a".into()),
+ Sexp::String(c.value().into())])])])),
+
+ &ElGamal { ref e, ref c } =>
+ Ok(Sexp::List(vec![
+ Sexp::String("enc-val".into()),
+ Sexp::List(vec![
+ Sexp::String("elg".into()),
+ Sexp::List(vec![
+ Sexp::String("a".into()),
+ Sexp::String(e.value().into())]),
+ Sexp::List(vec![
+ Sexp::String("b".into()),
+ Sexp::String(c.value().into())])])])),
+
+ &ECDH { ref e, ref key } =>
+ Ok(Sexp::List(vec![
+ Sexp::String("enc-val".into()),
+ Sexp::List(vec![
+ Sexp::String("ecdh".into()),
+ Sexp::List(vec![
+ Sexp::String("s".into()),
+ Sexp::String(key.as_ref().into())]),
+ Sexp::List(vec![
+ Sexp::String("e".into()),
+ Sexp::String(e.value().into())])])])),
+
+ &Unknown { .. } =>
+ Err(Error::InvalidArgument(
+ format!("Don't know how to convert {:?}", ciphertext))
+ .into()),
+
+ __Nonexhaustive => unreachable!(),
+ }
+ }
+}
+
+#[cfg(any(test, feature = "quickcheck"))]
+impl Arbitrary for Sexp {
+ fn arbitrary<G: Gen>(g: &mut G) -> Self {
+ if f32::arbitrary(g) < 0.7 {
+ Sexp::String(String_::arbitrary(g))
+ } else {
+ let mut v = Vec::new();
+ for _ in 0..usize::arbitrary(g) % 3 {
+ v.push(Sexp::arbitrary(g));
+ }
+ Sexp::List(v)
+ }
+ }
+}
+
+/// A string.
+///
+/// A string can optionally have a display hint.
+#[derive(Clone, PartialEq, Eq)]
+pub struct String_(Box<[u8]>, Option<Box<[u8]>>);
+
+impl fmt::Debug for String_ {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fn bstring(f: &mut fmt::Formatter, buf: &[u8]) -> fmt::Result {
+ write!(f, "b\"")?;
+ for &b in buf {
+ match b {
+ 0..=31 | 128..=255 =>
+ write!(f, "\\x{:02x}", b)?,
+ 0x22 => // "
+ write!(f, "\\\"")?,
+ 0x5c => // \
+ write!(f, "\\\\")?,
+ _ =>
+ write!(f, "{}", b as char)?,
+ }
+ }
+ write!(f, "\"")
+ }
+
+ if let Some(hint) = self.display_hint() {
+ write!(f, "[")?;
+ bstring(f, hint)?;
+ write!(f, "]")?;
+ }
+ bstring(f, &self.0)
+ }
+}
+
+impl String_ {
+ /// Constructs a new *Simple String*.
+ pub fn new<S>(s: S) -> Self
+ where S: Into<Box<[u8]>>
+ {
+ Self(s.into(), None)
+ }
+
+ /// Constructs a new *String*.
+ pub fn with_display_hint<S, T>(s: S, display_hint: T) -> Self
+ where S: Into<Box<[u8]>>, T: Into<Box<[u8]>>
+ {
+ Self(s.into(), Some(display_hint.into()))
+ }
+
+ /// Gets a reference to this *String*'s display hint, if any.
+ pub fn display_hint(&self) -> Option<&[u8]> {
+ self.1.as_ref().map(|b| b.as_ref())
+ }
+}
+
+impl From<&str> for String_ {
+ fn from(b: &str) -> Self {
+ Self::new(b.as_bytes().to_vec())
+ }
+}
+
+impl From<&[u8]> for String_ {
+ fn from(b: &[u8]) -> Self {
+ Self::new(b.to_vec())
+ }
+}
+
+impl Deref for String_ {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+#[cfg(any(test, feature = "quickcheck"))]
+impl Arbitrary for String_ {
+ fn arbitrary<G: Gen>(g: &mut G) -> Self {
+ if bool::arbitrary(g) {
+ Self::new(Vec::arbitrary(g).into_boxed_slice())
+ } else {
+ Self::with_display_hint(Vec::arbitrary(g).into_boxed_slice(),
+ Vec::arbitrary(g).into_boxed_slice())
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use openpgp::parse::Parse;
+ use openpgp::serialize::Serialize;
+
+ quickcheck::quickcheck! {
+ fn roundtrip(s: Sexp) -> bool {
+ let mut buf = Vec::new();
+ s.serialize(&mut buf).unwrap();
+ let t = Sexp::from_bytes(&buf).unwrap();
+ assert_eq!(s, t);
+ true
+ }
+ }
+
+ #[test]
+ fn to_signature() {
+ use openpgp::crypto::mpi::Signature::*;
+ assert!(destructures_to!(DSA { .. } = Sexp::from_bytes(
+ crate::tests::file("sexp/dsa-signature.sexp")).unwrap()
+ .to_signature().unwrap()));
+ assert!(destructures_to!(ECDSA { .. } = Sexp::from_bytes(
+ crate::tests::file("sexp/ecdsa-signature.sexp")).unwrap()
+ .to_signature().unwrap()));
+ assert!(destructures_to!(EdDSA { .. } = Sexp::from_bytes(
+ crate::tests::file("sexp/eddsa-signature.sexp")).unwrap()
+ .to_signature().unwrap()));
+ assert!(destructures_to!(RSA { .. } = Sexp::from_bytes(
+ crate::tests::file("sexp/rsa-signature.sexp")).unwrap()
+ .to_signature().unwrap()));
+ }
+}
diff --git a/ipc/src/sexp/parse/grammar.lalrpop b/ipc/src/sexp/parse/grammar.lalrpop
new file mode 100644
index 00000000..9a880361
--- /dev/null
+++ b/ipc/src/sexp/parse/grammar.lalrpop
@@ -0,0 +1,64 @@
+// -*- mode: Rust; -*-
+//
+// This implements parsing of [S-Expressions] encoded using the
+// canonical and basic transport encoding.
+//
+// [S-Expressions]: https://people.csail.mit.edu/rivest/Sexp.txt
+
+use crate::sexp::parse::lexer::{self, LexicalError};
+use crate::sexp::{Sexp, String_};
+
+grammar<'input>;
+
+// For canonical and basic transport:
+//
+// <sexpr> :: <string> | <list>
+pub Sexpr: Sexp = {
+ String,
+ List,
+};
+
+// <list> :: "(" <sexp>* ")" ;
+List: Sexp = {
+ LPAREN <Sexpr*> RPAREN => Sexp::List(<>),
+};
+
+// <string> :: <display>? <simple-string> ;
+String: Sexp = {
+ <d: Display> <s: SimpleString> =>
+ Sexp::String(String_::with_display_hint(s, d)),
+ <s: SimpleString> =>
+ Sexp::String(String_::new(s)),
+};
+
+// <simple-string> :: <raw> ;
+SimpleString: Vec<u8> =
+ RAW => if let lexer::Token::RAW(r) = <> {
+ r.iter().cloned().collect::<Vec<_>>()
+ } else {
+ unreachable!("Production matched on Token::RAW")
+ };
+
+// <display> :: "[" <simple-string> "]" ;
+Display = LBRACKET <SimpleString> RBRACKET;
+
+// <raw> :: <decimal> ":" <bytes> ;
+// <decimal-digit> :: "0" | ... | "9" ;
+// <decimal> :: <decimal-digit>+ ;
+// -- decimal numbers should have no unnecessary leading zeros
+// <bytes> -- any string of bytes, of the indicated length
+//
+// RAW is handled in the lexer.
+
+extern {
+ type Location = usize;
+ type Error = LexicalError;
+
+ enum lexer::Token<'input> {
+ LPAREN => lexer::Token::LPAREN,
+ RPAREN => lexer::Token::RPAREN,
+ LBRACKET => lexer::Token::LBRACKET,
+ RBRACKET => lexer::Token::RBRACKET,
+ RAW => lexer::Token::RAW(_),
+ }
+}
diff --git a/ipc/src/sexp/parse/lexer.rs b/ipc/src/sexp/parse/lexer.rs
new file mode 100644
index 00000000..0a570475
--- /dev/null
+++ b/ipc/src/sexp/parse/lexer.rs
@@ -0,0 +1,153 @@
+use std::fmt;
+
+// Controls tracing in the lexer.
+const TRACE: bool = false;
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum LexicalError {
+ LengthOverflow(String),
+ TruncatedInput(String),
+ UnexpectedCharacter(String),
+}
+
+impl fmt::Display for LexicalError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+pub type Spanned<Token, Loc, LexicalError>
+ = Result<(Loc, Token, Loc), LexicalError>;
+
+// The type of the parser's input.
+//
+// The parser iterators over tuples consisting of the token's starting
+// position, the token itself, and the token's ending position.
+pub(crate) type LexerItem<Token, Loc, LexicalError>
+ = Spanned<Token, Loc, LexicalError>;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Token<'a> {
+ LPAREN,
+ RPAREN,
+ LBRACKET,
+ RBRACKET,
+ RAW(&'a [u8]),
+}
+
+impl<'a> fmt::Display for Token<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl<'a> From<Token<'a>> for String {
+ fn from(t: Token<'a>) -> String {
+ use self::Token::*;
+ match t {
+ LPAREN => '('.to_string(),
+ RPAREN => ')'.to_string(),
+ LBRACKET =>