merge ot with rask

This commit is contained in:
Dustin J. Mitchell
2020-01-01 19:54:26 -05:00
20 changed files with 849 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
extern crate clap;
use clap::{App, Arg, SubCommand};
use ot::{Replica, DB};
use rask::{Replica, DB};
use uuid::Uuid;
fn main() {

View File

@@ -4,4 +4,7 @@ use failure::Fail;
pub enum Error {
#[fail(display = "Task Database Error: {}", _0)]
DBError(String),
#[fail(display = "TDB2 Error: {}", _0)]
TDB2Error(String),
}

View File

@@ -1,13 +1,27 @@
// TODO: remove this eventually when there's an API
#![allow(dead_code)]
#[macro_use]
extern crate failure;
mod errors;
mod operation;
mod replica;
mod server;
mod task;
mod taskdb;
mod tdb2;
pub use operation::Operation;
pub use replica::Replica;
pub use server::Server;
pub use task::Task;
pub use taskdb::DB;
use failure::Fallible;
use std::io::BufRead;
// TODO: remove (artifact of merging projects)
pub fn parse(filename: &str, reader: impl BufRead) -> Fallible<Vec<Task>> {
Ok(tdb2::parse(filename, reader)?)
}

View File

@@ -1,7 +1,7 @@
use crate::errors::Error;
use crate::operation::Operation;
use crate::taskdb::DB;
use chrono::Utc;
use failure::Fallible;
use std::collections::HashMap;
use uuid::Uuid;
@@ -16,12 +16,12 @@ impl Replica {
}
/// Create a new task. The task must not already exist.
pub fn create_task(&mut self, uuid: Uuid) -> Result<(), Error> {
pub fn create_task(&mut self, uuid: Uuid) -> Fallible<()> {
self.taskdb.apply(Operation::Create { uuid })
}
/// Delete a task. The task must exist.
pub fn delete_task(&mut self, uuid: Uuid) -> Result<(), Error> {
pub fn delete_task(&mut self, uuid: Uuid) -> Fallible<()> {
self.taskdb.apply(Operation::Delete { uuid })
}
@@ -33,7 +33,7 @@ impl Replica {
uuid: Uuid,
property: S1,
value: Option<S2>,
) -> Result<(), Error>
) -> Fallible<()>
where
S1: Into<String>,
S2: Into<String>,

7
src/task/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
mod task;
mod taskbuilder;
pub use self::taskbuilder::TaskBuilder;
pub use self::task::{Task, Priority, Status, Timestamp, Annotation};
pub use self::task::Priority::*;
pub use self::task::Status::*;

55
src/task/task.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::collections::HashMap;
use uuid::Uuid;
use chrono::prelude::*;
pub type Timestamp = DateTime<Utc>;
#[derive(Debug, PartialEq)]
pub enum Priority {
L,
M,
H,
}
#[derive(Debug, PartialEq)]
pub enum Status {
Pending,
Completed,
Deleted,
Recurring,
Waiting,
}
#[derive(Debug, PartialEq)]
pub struct Annotation {
pub entry: Timestamp,
pub description: String,
}
/// A task, the fundamental business object of this tool.
///
/// This structure is based on https://taskwarrior.org/docs/design/task.html
#[derive(Debug)]
pub struct Task {
pub status: Status,
pub uuid: Uuid,
pub entry: Timestamp,
pub description: String,
pub start: Option<Timestamp>,
pub end: Option<Timestamp>,
pub due: Option<Timestamp>,
pub until: Option<Timestamp>,
pub wait: Option<Timestamp>,
pub modified: Timestamp,
pub scheduled: Option<Timestamp>,
pub recur: Option<String>,
pub mask: Option<String>,
pub imask: Option<u64>,
pub parent: Option<Uuid>,
pub project: Option<String>,
pub priority: Option<Priority>,
pub depends: Vec<Uuid>,
pub tags: Vec<String>,
pub annotations: Vec<Annotation>,
pub udas: HashMap<String, String>,
}

196
src/task/taskbuilder.rs Normal file
View File

@@ -0,0 +1,196 @@
use crate::task::{Annotation, Priority, Status, Task, Timestamp};
use chrono::prelude::*;
use failure::Fallible;
use std::collections::HashMap;
use std::str;
use uuid::Uuid;
#[derive(Default)]
pub struct TaskBuilder {
status: Option<Status>,
uuid: Option<Uuid>,
entry: Option<Timestamp>,
description: Option<String>,
start: Option<Timestamp>,
end: Option<Timestamp>,
due: Option<Timestamp>,
until: Option<Timestamp>,
wait: Option<Timestamp>,
modified: Option<Timestamp>,
scheduled: Option<Timestamp>,
recur: Option<String>,
mask: Option<String>,
imask: Option<u64>,
parent: Option<Uuid>,
project: Option<String>,
priority: Option<Priority>,
depends: Vec<Uuid>,
tags: Vec<String>,
annotations: Vec<Annotation>,
udas: HashMap<String, String>,
}
/// Parse an "integer", allowing for occasional integers with trailing decimal zeroes
fn parse_int<T>(value: &str) -> Result<T, <T as str::FromStr>::Err>
where
T: str::FromStr,
{
// some integers are rendered with following decimal zeroes
if let Some(i) = value.find('.') {
let mut nonzero = false;
for c in value[i + 1..].chars() {
if c != '0' {
nonzero = true;
break;
}
}
if !nonzero {
return value[..i].parse();
}
}
value.parse()
}
/// Parse a status into a Status enum value
fn parse_status(value: &str) -> Fallible<Status> {
match value {
"pending" => Ok(Status::Pending),
"completed" => Ok(Status::Completed),
"deleted" => Ok(Status::Deleted),
"recurring" => Ok(Status::Recurring),
"waiting" => Ok(Status::Waiting),
_ => Err(format_err!("invalid status {}", value)),
}
}
/// Parse "L", "M", "H" into the Priority enum
fn parse_priority(value: &str) -> Fallible<Priority> {
match value {
"L" => Ok(Priority::L),
"M" => Ok(Priority::M),
"H" => Ok(Priority::H),
_ => Err(format_err!("invalid priority {}", value)),
}
}
/// Parse a UNIX timestamp into a UTC DateTime
fn parse_timestamp(value: &str) -> Result<Timestamp, <i64 as str::FromStr>::Err> {
Ok(Utc.timestamp(parse_int::<i64>(value)?, 0))
}
/// Parse depends, as a list of ,-separated UUIDs
fn parse_depends(value: &str) -> Result<Vec<Uuid>, uuid::Error> {
value.split(',').map(|s| Uuid::parse_str(s)).collect()
}
/// Parse tags, as a list of ,-separated strings
fn parse_tags(value: &str) -> Vec<String> {
value.split(',').map(|s| s.to_string()).collect()
}
impl TaskBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn set(mut self, name: &str, value: String) -> Self {
const ANNOTATION_PREFIX: &str = "annotation_";
if name.starts_with(ANNOTATION_PREFIX) {
let entry = parse_timestamp(&name[ANNOTATION_PREFIX.len()..]).unwrap();
// TODO: sort by entry time
self.annotations.push(Annotation {
entry,
description: value.to_string(),
});
return self;
}
match name {
"status" => self.status = Some(parse_status(&value).unwrap()),
"uuid" => self.uuid = Some(Uuid::parse_str(&value).unwrap()),
"entry" => self.entry = Some(parse_timestamp(&value).unwrap()),
"description" => self.description = Some(value),
"start" => self.start = Some(parse_timestamp(&value).unwrap()),
"end" => self.end = Some(parse_timestamp(&value).unwrap()),
"due" => self.due = Some(parse_timestamp(&value).unwrap()),
"until" => self.until = Some(parse_timestamp(&value).unwrap()),
"wait" => self.wait = Some(parse_timestamp(&value).unwrap()),
"modified" => self.modified = Some(parse_timestamp(&value).unwrap()),
"scheduled" => self.scheduled = Some(parse_timestamp(&value).unwrap()),
"recur" => self.recur = Some(value),
"mask" => self.mask = Some(value),
"imask" => self.imask = Some(parse_int::<u64>(&value).unwrap()),
"parent" => self.uuid = Some(Uuid::parse_str(&value).unwrap()),
"project" => self.project = Some(value),
"priority" => self.priority = Some(parse_priority(&value).unwrap()),
"depends" => self.depends = parse_depends(&value).unwrap(),
"tags" => self.tags = parse_tags(&value),
_ => {
self.udas.insert(name.to_string(), value);
}
}
self
}
pub fn finish(self) -> Task {
Task {
status: self.status.unwrap(),
uuid: self.uuid.unwrap(),
description: self.description.unwrap(),
entry: self.entry.unwrap(),
start: self.start,
end: self.end,
due: self.due,
until: self.until,
wait: self.wait,
modified: self.modified.unwrap(),
scheduled: self.scheduled,
recur: self.recur,
mask: self.mask,
imask: self.imask,
parent: self.parent,
project: self.project,
priority: self.priority,
depends: self.depends,
tags: self.tags,
annotations: self.annotations,
udas: self.udas,
}
// TODO: check validity per https://taskwarrior.org/docs/design/task.html
}
}
#[cfg(test)]
mod test {
use super::{parse_depends, parse_int};
use uuid::Uuid;
#[test]
fn test_parse_int() {
assert_eq!(parse_int::<u8>("123").unwrap(), 123u8);
assert_eq!(parse_int::<u32>("123000000").unwrap(), 123000000u32);
assert_eq!(parse_int::<i32>("-123000000").unwrap(), -123000000i32);
}
#[test]
fn test_parse_int_decimals() {
assert_eq!(parse_int::<u8>("123.00").unwrap(), 123u8);
assert_eq!(parse_int::<u32>("123.0000").unwrap(), 123u32);
assert_eq!(parse_int::<i32>("-123.").unwrap(), -123i32);
}
#[test]
fn test_parse_depends() {
let u1 = "123e4567-e89b-12d3-a456-426655440000";
let u2 = "123e4567-e89b-12d3-a456-999999990000";
assert_eq!(
parse_depends(u1).unwrap(),
vec![Uuid::parse_str(u1).unwrap()]
);
assert_eq!(
parse_depends(&format!("{},{}", u1, u2)).unwrap(),
vec![Uuid::parse_str(u1).unwrap(), Uuid::parse_str(u2).unwrap()]
);
}
}

View File

@@ -1,6 +1,7 @@
use crate::errors::Error;
use crate::operation::Operation;
use crate::server::{Server, VersionAdd};
use failure::Fallible;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
@@ -42,7 +43,7 @@ impl DB {
/// Apply an operation to the DB. Aside from synchronization operations, this is the only way
/// to modify the DB. In cases where an operation does not make sense, this function will do
/// nothing and return an error (but leave the DB in a consistent state).
pub fn apply(&mut self, op: Operation) -> Result<(), Error> {
pub fn apply(&mut self, op: Operation) -> Fallible<()> {
if let err @ Err(_) = self.apply_op(&op) {
return err;
}
@@ -50,19 +51,19 @@ impl DB {
Ok(())
}
fn apply_op(&mut self, op: &Operation) -> Result<(), Error> {
fn apply_op(&mut self, op: &Operation) -> Fallible<()> {
match op {
&Operation::Create { uuid } => {
// insert if the task does not already exist
if let ent @ Entry::Vacant(_) = self.tasks.entry(uuid) {
ent.or_insert(HashMap::new());
} else {
return Err(Error::DBError(format!("Task {} already exists", uuid)));
return Err(Error::DBError(format!("Task {} already exists", uuid)).into());
}
}
&Operation::Delete { ref uuid } => {
if let None = self.tasks.remove(uuid) {
return Err(Error::DBError(format!("Task {} does not exist", uuid)));
return Err(Error::DBError(format!("Task {} does not exist", uuid)).into());
}
}
&Operation::Update {
@@ -78,7 +79,7 @@ impl DB {
None => task.remove(property),
};
} else {
return Err(Error::DBError(format!("Task {} does not exist", uuid)));
return Err(Error::DBError(format!("Task {} does not exist", uuid)).into());
}
}
}
@@ -203,8 +204,8 @@ mod tests {
let op = Operation::Create { uuid };
db.apply(op.clone()).unwrap();
assert_eq!(
db.apply(op.clone()).err().unwrap(),
Error::DBError(format!("Task {} already exists", uuid))
db.apply(op.clone()).err().unwrap().to_string(),
format!("Task Database Error: Task {} already exists", uuid)
);
let mut exp = HashMap::new();
@@ -285,8 +286,8 @@ mod tests {
timestamp: Utc::now(),
};
assert_eq!(
db.apply(op).err().unwrap(),
Error::DBError(format!("Task {} does not exist", uuid))
db.apply(op).err().unwrap().to_string(),
format!("Task Database Error: Task {} does not exist", uuid)
);
assert_eq!(db.tasks(), &HashMap::new());
@@ -314,8 +315,8 @@ mod tests {
let op1 = Operation::Delete { uuid };
assert_eq!(
db.apply(op1).err().unwrap(),
Error::DBError(format!("Task {} does not exist", uuid))
db.apply(op1).err().unwrap().to_string(),
format!("Task Database Error: Task {} does not exist", uuid)
);
assert_eq!(db.tasks(), &HashMap::new());

244
src/tdb2/ff4.rs Normal file
View File

@@ -0,0 +1,244 @@
use std::str;
use super::pig::Pig;
use crate::task::{Task, TaskBuilder};
use failure::Fallible;
/// Rust implementation of part of utf8_codepoint from Taskwarrior's src/utf8.cpp
///
/// Note that the original function will return garbage for invalid hex sequences;
/// this panics instead.
fn hex_to_unicode(value: &[u8]) -> Fallible<String> {
if value.len() < 4 {
bail!("too short");
}
fn nyb(c: u8) -> Fallible<u16> {
match c {
b'0'..=b'9' => Ok((c - b'0') as u16),
b'a'..=b'f' => Ok((c - b'a' + 10) as u16),
b'A'..=b'F' => Ok((c - b'A' + 10) as u16),
_ => bail!("invalid hex character"),
}
};
let words = [nyb(value[0])? << 12 | nyb(value[1])? << 8 | nyb(value[2])? << 4 | nyb(value[3])?];
Ok(String::from_utf16(&words[..])?)
}
/// Rust implementation of JSON::decode in Taskwarrior's src/JSON.cpp
///
/// Decode the given byte slice into a string using Taskwarrior JSON's escaping The slice is
/// assumed to be ASCII; unicode escapes within it will be expanded.
fn json_decode(value: &[u8]) -> Fallible<String> {
let length = value.len();
let mut rv = String::with_capacity(length);
let mut pos = 0;
while pos < length {
let v = value[pos];
if v == b'\\' {
pos += 1;
if pos == length {
rv.push(v as char);
break;
}
let v = value[pos];
match v {
b'"' | b'\\' | b'/' => rv.push(v as char),
b'b' => rv.push(8 as char),
b'f' => rv.push(12 as char),
b'n' => rv.push('\n' as char),
b'r' => rv.push('\r' as char),
b't' => rv.push('\t' as char),
b'u' => {
let unicode = hex_to_unicode(&value[pos + 1..pos + 5]).map_err(|_| {
let esc = &value[pos - 1..pos + 5];
match str::from_utf8(esc) {
Ok(s) => format_err!("invalid unicode escape `{}`", s),
Err(_) => format_err!("invalid unicode escape bytes {:?}", esc),
}
})?;
rv.push_str(&unicode);
pos += 4;
}
_ => {
rv.push(b'\\' as char);
rv.push(v as char);
}
}
} else {
rv.push(v as char)
}
pos += 1;
}
Ok(rv)
}
/// Rust implementation of Task::decode in Taskwarrior's src/Task.cpp
///
/// Note that the docstring for the C++ function does not match the
/// implementation!
fn decode(value: String) -> String {
if let Some(_) = value.find('&') {
return value.replace("&open;", "[").replace("&close;", "]");
}
value
}
/// Parse an "FF4" formatted task line. From Task::parse in Taskwarrior's src/Task.cpp.
///
/// While Taskwarrior supports additional formats, this is the only format supported by rask.
pub(super) fn parse_ff4(line: &str) -> Fallible<Task> {
let mut pig = Pig::new(line.as_bytes());
let mut builder = TaskBuilder::new();
pig.skip(b'[')?;
let line = pig.get_until(b']')?;
let mut subpig = Pig::new(line);
while !subpig.depleted() {
let name = subpig.get_until(b':')?;
let name = str::from_utf8(name)?;
subpig.skip(b':')?;
let value = subpig.get_quoted(b'"')?;
let value = json_decode(value)?;
let value = decode(value);
builder = builder.set(name, value);
subpig.skip(b' ').ok(); // ignore if not found..
}
pig.skip(b']')?;
if !pig.depleted() {
bail!("trailing characters on line");
}
Ok(builder.finish())
}
#[cfg(test)]
mod test {
use super::{decode, hex_to_unicode, json_decode, parse_ff4};
use crate::task::Pending;
#[test]
fn test_hex_to_unicode_digits() {
assert_eq!(hex_to_unicode(b"1234").unwrap(), "\u{1234}");
}
#[test]
fn test_hex_to_unicode_lower() {
assert_eq!(hex_to_unicode(b"abcd").unwrap(), "\u{abcd}");
}
#[test]
fn test_hex_to_unicode_upper() {
assert_eq!(hex_to_unicode(b"ABCD").unwrap(), "\u{abcd}");
}
#[test]
fn test_hex_to_unicode_too_short() {
assert!(hex_to_unicode(b"AB").is_err());
}
#[test]
fn test_hex_to_unicode_invalid() {
assert!(hex_to_unicode(b"defg").is_err());
}
#[test]
fn test_json_decode_no_change() {
assert_eq!(json_decode(b"abcd").unwrap(), "abcd");
}
#[test]
fn test_json_decode_escape_quote() {
assert_eq!(json_decode(b"ab\\\"cd").unwrap(), "ab\"cd");
}
#[test]
fn test_json_decode_escape_backslash() {
assert_eq!(json_decode(b"ab\\\\cd").unwrap(), "ab\\cd");
}
#[test]
fn test_json_decode_escape_frontslash() {
assert_eq!(json_decode(b"ab\\/cd").unwrap(), "ab/cd");
}
#[test]
fn test_json_decode_escape_b() {
assert_eq!(json_decode(b"ab\\bcd").unwrap(), "ab\x08cd");
}
#[test]
fn test_json_decode_escape_f() {
assert_eq!(json_decode(b"ab\\fcd").unwrap(), "ab\x0ccd");
}
#[test]
fn test_json_decode_escape_n() {
assert_eq!(json_decode(b"ab\\ncd").unwrap(), "ab\ncd");
}
#[test]
fn test_json_decode_escape_r() {
assert_eq!(json_decode(b"ab\\rcd").unwrap(), "ab\rcd");
}
#[test]
fn test_json_decode_escape_t() {
assert_eq!(json_decode(b"ab\\tcd").unwrap(), "ab\tcd");
}
#[test]
fn test_json_decode_escape_other() {
assert_eq!(json_decode(b"ab\\xcd").unwrap(), "ab\\xcd");
}
#[test]
fn test_json_decode_escape_eos() {
assert_eq!(json_decode(b"ab\\").unwrap(), "ab\\");
}
#[test]
fn test_json_decode_escape_unicode() {
assert_eq!(json_decode(b"ab\\u1234").unwrap(), "ab\u{1234}");
}
#[test]
fn test_json_decode_escape_unicode_bad() {
let rv = json_decode(b"ab\\uwxyz");
assert_eq!(
rv.unwrap_err().to_string(),
"invalid unicode escape `\\uwxyz`"
);
}
#[test]
fn test_decode_no_change() {
let s = "abcd &quot; efgh &".to_string();
assert_eq!(decode(s.clone()), s);
}
#[test]
fn test_decode_multi() {
let s = "abcd &open; efgh &close; &open".to_string();
assert_eq!(decode(s), "abcd [ efgh ] &open".to_string());
}
#[test]
fn test_parse_ff4() {
let s = "[description:\"desc\" entry:\"1437855511\" modified:\"1479480556\" \
priority:\"L\" project:\"lists\" status:\"pending\" tags:\"watch\" \
uuid:\"83ce989e-8634-4d62-841c-eb309383ff1f\"]";
let task = parse_ff4(s).unwrap();
assert_eq!(task.status, Pending);
assert_eq!(task.description, "desc");
}
#[test]
fn test_parse_ff4_fail() {
assert!(parse_ff4("abc:10]").is_err());
assert!(parse_ff4("[abc:10").is_err());
assert!(parse_ff4("[abc:10 123:123]").is_err());
}
}

25
src/tdb2/mod.rs Normal file
View File

@@ -0,0 +1,25 @@
//! TDB2 is Taskwarrior's on-disk database format. This module implements
//! support for the data structure as a compatibility layer.
mod ff4;
mod pig;
use self::ff4::parse_ff4;
use crate::task::Task;
use failure::Fallible;
use std::io::BufRead;
pub(crate) fn parse(filename: &str, reader: impl BufRead) -> Fallible<Vec<Task>> {
let mut tasks = vec![];
for (i, line) in reader.lines().enumerate() {
tasks.push(parse_ff4(&line?).map_err(|e| {
format_err!(
"TDB2 Error at {}:{}: {}",
filename.to_string(),
i as u64 + 1,
e
)
})?);
}
Ok(tasks)
}

201
src/tdb2/pig.rs Normal file
View File

@@ -0,0 +1,201 @@
//! A minimal implementation of the "Pig" parsing utility from the Taskwarrior
//! source. This is just enough to parse FF4 lines.
use failure::Fallible;
pub struct Pig<'a> {
input: &'a [u8],
cursor: usize,
}
impl<'a> Pig<'a> {
pub fn new(input: &'a [u8]) -> Self {
Pig {
input: input,
cursor: 0,
}
}
pub fn get_until(&mut self, c: u8) -> Fallible<&'a [u8]> {
if self.cursor >= self.input.len() {
bail!("input truncated");
}
let mut i = self.cursor;
while i < self.input.len() {
if self.input[i] == c {
let rv = &self.input[self.cursor..i];
self.cursor = i;
return Ok(rv);
}
i += 1;
}
let rv = &self.input[self.cursor..];
self.cursor = self.input.len();
Ok(rv)
}
pub fn get_quoted(&mut self, c: u8) -> Fallible<&'a [u8]> {
let length = self.input.len();
if self.cursor >= length || self.input[self.cursor] != c {
bail!("quoted string does not begin with quote character");
}
let start = self.cursor + 1;
let mut i = start;
while i < length {
while i < length && self.input[i] != c {
i += 1
}
if i == length {
bail!("unclosed quote");
}
if i == start {
return Ok(&self.input[i..i]);
}
if self.input[i - 1] == b'\\' {
// work backward looking for escaped backslashes
let mut j = i - 2;
let mut quote = true;
while j >= start && self.input[j] == b'\\' {
quote = !quote;
j -= 1;
}
if quote {
i += 1;
continue;
}
}
// none of the above matched, so we are at the end
self.cursor = i + 1;
return Ok(&self.input[start..i]);
}
unreachable!();
}
pub fn skip(&mut self, c: u8) -> Fallible<()> {
if self.cursor < self.input.len() && self.input[self.cursor] == c {
self.cursor += 1;
return Ok(());
}
bail!(
"expected character `{}`",
String::from_utf8(vec![c]).unwrap()
);
}
pub fn depleted(&self) -> bool {
self.cursor >= self.input.len()
}
}
#[cfg(test)]
mod test {
use super::Pig;
#[test]
fn test_get_until() {
let s = b"abc:123";
let mut pig = Pig::new(s);
assert_eq!(pig.get_until(b':').unwrap(), &s[..3]);
}
#[test]
fn test_get_until_empty() {
let s = b"abc:123";
let mut pig = Pig::new(s);
assert_eq!(pig.get_until(b'a').unwrap(), &s[..0]);
}
#[test]
fn test_get_until_not_found() {
let s = b"abc:123";
let mut pig = Pig::new(s);
assert_eq!(pig.get_until(b'/').unwrap(), &s[..]);
}
#[test]
fn test_get_quoted() {
let s = b"'abcd'efg";
let mut pig = Pig::new(s);
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..5]);
assert_eq!(pig.cursor, 6);
}
#[test]
fn test_get_quoted_unopened() {
let s = b"abcd'efg";
let mut pig = Pig::new(s);
assert!(pig.get_quoted(b'\'').is_err());
assert_eq!(pig.cursor, 0); // nothing consumed
}
#[test]
fn test_get_quoted_unclosed() {
let s = b"'abcdefg";
let mut pig = Pig::new(s);
assert!(pig.get_quoted(b'\'').is_err());
assert_eq!(pig.cursor, 0);
}
#[test]
fn test_get_quoted_escaped() {
let s = b"'abc\\'de'fg";
let mut pig = Pig::new(s);
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..8]);
assert_eq!(pig.cursor, 9);
}
#[test]
fn test_get_quoted_double_escaped() {
let s = b"'abc\\\\'de'fg";
let mut pig = Pig::new(s);
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..6]);
assert_eq!(pig.cursor, 7);
}
#[test]
fn test_get_quoted_triple_escaped() {
let s = b"'abc\\\\\\'de'fg";
let mut pig = Pig::new(s);
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..10]);
assert_eq!(pig.cursor, 11);
}
#[test]
fn test_get_quoted_all_escapes() {
let s = b"'\\\\\\'\\\\'fg";
let mut pig = Pig::new(s);
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..7]);
assert_eq!(pig.cursor, 8);
}
#[test]
fn test_skip_match() {
let s = b"foo";
let mut pig = Pig::new(s);
assert!(pig.skip(b'f').is_ok());
assert_eq!(pig.cursor, 1);
}
#[test]
fn test_skip_no_match() {
let s = b"foo";
let mut pig = Pig::new(s);
assert!(pig.skip(b'x').is_err());
assert_eq!(pig.cursor, 0); // nothing consumed
}
#[test]
fn test_skip_eos() {
let s = b"f";
let mut pig = Pig::new(s);
assert!(pig.skip(b'f').is_ok());
assert!(pig.skip(b'f').is_err());
}
}