From 1272acb893c99c87b98685b8cdc7570e179958f7 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 11 Nov 2018 21:09:49 -0500 Subject: [PATCH] Use error_chain --- Cargo.lock | 51 +++++++++++++++++++ Cargo.toml | 1 + src/errors.rs | 9 ++++ src/main.rs | 12 ++++- src/tdb2/ff4.rs | 133 ++++++++++++++++++++++++++++-------------------- src/tdb2/mod.rs | 5 +- src/tdb2/pig.rs | 31 ++++++----- 7 files changed, 169 insertions(+), 73 deletions(-) create mode 100644 src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 664e462ef..fc2361e68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3 +1,34 @@ +[[package]] +name = "backtrace" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "chrono" version = "0.4.6" @@ -8,6 +39,14 @@ dependencies = [ "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "error-chain" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "libc" version = "0.2.43" @@ -31,6 +70,7 @@ name = "rask" version = "0.1.0" dependencies = [ "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -39,6 +79,11 @@ name = "redox_syscall" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rustc-demangle" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "time" version = "0.1.40" @@ -74,11 +119,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] +"checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" +"checksum backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "c66d56ac8dabd07f6aacdaf633f4b8262f5b3601a810a0dcddffd5c22c69daa0" +"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16" +"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" "checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02" "checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" "checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" +"checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" "checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" "checksum uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dab5c5526c5caa3d106653401a267fed923e7046f35895ffcb5ca42db64942e6" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" diff --git a/Cargo.toml b/Cargo.toml index 8f9fe62a7..739b8c05a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ authors = ["Dustin J. Mitchell "] #indicatif = "0.9.0" uuid = "0.7" chrono = "0.4" +error-chain = "0.12.0" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 000000000..5debda75a --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,9 @@ +error_chain!{ + foreign_links { + Io(::std::io::Error); + StrFromUtf8(::std::str::Utf8Error); + StringFromUtf8(::std::string::FromUtf8Error); + StringFromUtf16(::std::string::FromUtf16Error); + } + +} diff --git a/src/main.rs b/src/main.rs index 161b8513f..f8b3f6f82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,23 @@ extern crate chrono; extern crate uuid; +#[macro_use] +extern crate error_chain; mod tdb2; mod task; +mod errors; use tdb2::parse; use std::io::stdin; -fn main() { +use errors::*; + +quick_main!(run); + +fn run() -> Result<()> { let input = stdin(); - parse(input.lock()).unwrap().iter().for_each(|t| { + parse(input.lock())?.iter().for_each(|t| { println!("{:?}", t); }); + Ok(()) } diff --git a/src/tdb2/ff4.rs b/src/tdb2/ff4.rs index 8800b503f..63e353e73 100644 --- a/src/tdb2/ff4.rs +++ b/src/tdb2/ff4.rs @@ -1,38 +1,38 @@ use std::str; -use std::io::{Result, Error, ErrorKind}; use super::pig::Pig; use task::{TaskBuilder, Task}; +use errors::*; /// 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]) -> String { +fn hex_to_unicode(value: &[u8]) -> Result { if value.len() < 4 { - panic!(format!("unicode escape too short -- {:?}", value)); + bail!(format!("too short")); } - fn nyb(c: u8) -> u16 { + fn nyb(c: u8) -> Result { match c { - b'0'...b'9' => (c - b'0') as u16, - b'a'...b'f' => (c - b'a' + 10) as u16, - b'A'...b'F' => (c - b'A' + 10) as u16, - _ => panic!(format!("invalid hex character {:?}", 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]), + nyb(value[0])? << 12 | nyb(value[1])? << 8 | nyb(value[2])? << 4 | nyb(value[3])?, ]; - return String::from_utf16(&words[..]).unwrap(); + 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]) -> String { +fn json_decode(value: &[u8]) -> Result { let length = value.len(); let mut rv = String::with_capacity(length); @@ -54,7 +54,14 @@ fn json_decode(value: &[u8]) -> String { b'r' => rv.push('\r' as char), b't' => rv.push('\t' as char), b'u' => { - rv.push_str(&hex_to_unicode(&value[pos + 1..])); + let unicode = hex_to_unicode(&value[pos + 1..pos + 5]).chain_err(|| { + let esc = &value[pos - 1..pos + 5]; + match str::from_utf8(esc) { + Ok(s) => format!("invalid unicode escape `{}`", s), + Err(_) => format!("invalid unicode escape bytes {:?}", esc), + } + })?; + rv.push_str(&unicode); pos += 4; } _ => { @@ -68,7 +75,7 @@ fn json_decode(value: &[u8]) -> String { pos += 1; } - rv + Ok(rv) } /// Rust implementation of Task::decode in Taskwarrior's src/Task.cpp @@ -89,37 +96,25 @@ pub(super) fn parse_ff4(line: &str) -> Result { let mut pig = Pig::new(line.as_bytes()); let mut builder = TaskBuilder::new(); - if !pig.skip(b'[') { - return Err(Error::new(ErrorKind::Other, "bad line")); - } - if let Some(line) = pig.get_until(b']') { - let mut pig = Pig::new(line); - while !pig.depleted() { - if let Some(name) = pig.get_until(b':') { - let name = str::from_utf8(name).unwrap(); - if !pig.skip(b':') { - return Err(Error::new(ErrorKind::Other, "bad line")); - } - if let Some(value) = pig.get_quoted(b'"') { - let value = json_decode(value); - let value = decode(value); - builder = builder.set(name, value); - } else { - return Err(Error::new(ErrorKind::Other, "bad line")); - } - pig.skip(b' '); - } else { - return Err(Error::new(ErrorKind::Other, "bad line")); - } + 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':')?; + if let Some(value) = subpig.get_quoted(b'"') { + let value = json_decode(value)?; + let value = decode(value); + builder = builder.set(name, value); + } else { + bail!("bad line 3"); } - } else { - return Err(Error::new(ErrorKind::Other, "bad line")); - } - if !pig.skip(b']') { - return Err(Error::new(ErrorKind::Other, "bad line")); + subpig.skip(b' ').ok(); // ignore if not found.. } + pig.skip(b']')?; if !pig.depleted() { - return Err(Error::new(ErrorKind::Other, "bad line")); + bail!("bad line 5"); } Ok(builder.finish()) } @@ -131,77 +126,96 @@ mod test { #[test] fn test_hex_to_unicode_digits() { - assert_eq!(hex_to_unicode(b"1234"), "\u{1234}"); + assert_eq!(hex_to_unicode(b"1234").unwrap(), "\u{1234}"); } #[test] fn test_hex_to_unicode_lower() { - assert_eq!(hex_to_unicode(b"abcd"), "\u{abcd}"); + assert_eq!(hex_to_unicode(b"abcd").unwrap(), "\u{abcd}"); } #[test] fn test_hex_to_unicode_upper() { - assert_eq!(hex_to_unicode(b"ABCD"), "\u{abcd}"); + 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"), "abcd"); + assert_eq!(json_decode(b"abcd").unwrap(), "abcd"); } #[test] fn test_json_decode_escape_quote() { - assert_eq!(json_decode(b"ab\\\"cd"), "ab\"cd"); + assert_eq!(json_decode(b"ab\\\"cd").unwrap(), "ab\"cd"); } #[test] fn test_json_decode_escape_backslash() { - assert_eq!(json_decode(b"ab\\\\cd"), "ab\\cd"); + assert_eq!(json_decode(b"ab\\\\cd").unwrap(), "ab\\cd"); } #[test] fn test_json_decode_escape_frontslash() { - assert_eq!(json_decode(b"ab\\/cd"), "ab/cd"); + assert_eq!(json_decode(b"ab\\/cd").unwrap(), "ab/cd"); } #[test] fn test_json_decode_escape_b() { - assert_eq!(json_decode(b"ab\\bcd"), "ab\x08cd"); + assert_eq!(json_decode(b"ab\\bcd").unwrap(), "ab\x08cd"); } #[test] fn test_json_decode_escape_f() { - assert_eq!(json_decode(b"ab\\fcd"), "ab\x0ccd"); + assert_eq!(json_decode(b"ab\\fcd").unwrap(), "ab\x0ccd"); } #[test] fn test_json_decode_escape_n() { - assert_eq!(json_decode(b"ab\\ncd"), "ab\ncd"); + assert_eq!(json_decode(b"ab\\ncd").unwrap(), "ab\ncd"); } #[test] fn test_json_decode_escape_r() { - assert_eq!(json_decode(b"ab\\rcd"), "ab\rcd"); + assert_eq!(json_decode(b"ab\\rcd").unwrap(), "ab\rcd"); } #[test] fn test_json_decode_escape_t() { - assert_eq!(json_decode(b"ab\\tcd"), "ab\tcd"); + assert_eq!(json_decode(b"ab\\tcd").unwrap(), "ab\tcd"); } #[test] fn test_json_decode_escape_other() { - assert_eq!(json_decode(b"ab\\xcd"), "ab\\xcd"); + assert_eq!(json_decode(b"ab\\xcd").unwrap(), "ab\\xcd"); } #[test] fn test_json_decode_escape_eos() { - assert_eq!(json_decode(b"ab\\"), "ab\\"); + assert_eq!(json_decode(b"ab\\").unwrap(), "ab\\"); } #[test] fn test_json_decode_escape_unicode() { - assert_eq!(json_decode(b"ab\\u1234"), "ab\u{1234}"); + 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] @@ -225,4 +239,11 @@ mod test { 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()); + } } diff --git a/src/tdb2/mod.rs b/src/tdb2/mod.rs index 54252bfac..0e29d4c0b 100644 --- a/src/tdb2/mod.rs +++ b/src/tdb2/mod.rs @@ -1,9 +1,10 @@ mod pig; mod ff4; -use std::io::{BufRead, Result}; -use super::task::Task; +use std::io::BufRead; +use task::Task; use self::ff4::parse_ff4; +use errors::*; pub(super) fn parse(reader: impl BufRead) -> Result> { let mut tasks = vec![]; diff --git a/src/tdb2/pig.rs b/src/tdb2/pig.rs index 7a86c8a96..16319ccba 100644 --- a/src/tdb2/pig.rs +++ b/src/tdb2/pig.rs @@ -1,6 +1,8 @@ //! A minimal implementation of the "Pig" parsing utility from the Taskwarrior //! source. +use errors::*; + pub struct Pig<'a> { input: &'a [u8], cursor: usize, @@ -14,9 +16,9 @@ impl<'a> Pig<'a> { } } - pub fn get_until(&mut self, c: u8) -> Option<&'a [u8]> { + pub fn get_until(&mut self, c: u8) -> Result<&'a [u8]> { if self.cursor >= self.input.len() { - return None; + return Err(Error::from("input truncated")); } let mut i = self.cursor; @@ -24,13 +26,13 @@ impl<'a> Pig<'a> { if self.input[i] == c { let rv = &self.input[self.cursor..i]; self.cursor = i; - return Some(rv); + return Ok(rv); } i += 1; } let rv = &self.input[self.cursor..]; self.cursor = self.input.len(); - Some(rv) + Ok(rv) } // TODO: get_until_str @@ -104,12 +106,15 @@ impl<'a> Pig<'a> { return false; } - pub fn skip(&mut self, c: u8) -> bool { + pub fn skip(&mut self, c: u8) -> Result<()> { if self.cursor < self.input.len() && self.input[self.cursor] == c { self.cursor += 1; - return true; + return Ok(()); } - false + bail!(format!( + "expected character `{}`", + String::from_utf8(vec![c])? + )); } // TODO: skip_all_one_of @@ -142,21 +147,21 @@ mod test { fn test_get_until() { let s = b"abc:123"; let mut pig = Pig::new(s); - assert_eq!(pig.get_until(b':'), Some(&s[..3])); + 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'), Some(&s[..0])); + 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'/'), Some(&s[..])); + assert_eq!(pig.get_until(b'/').unwrap(), &s[..]); } #[test] @@ -251,7 +256,7 @@ mod test { fn test_skip_match() { let s = b"foo"; let mut pig = Pig::new(s); - assert!(pig.skip(b'f')); + assert!(pig.skip(b'f').is_ok()); assert_eq!(pig.get_until_eos(), Some(&s[1..])); } @@ -259,7 +264,7 @@ mod test { fn test_skip_no_match() { let s = b"foo"; let mut pig = Pig::new(s); - assert!(!pig.skip(b'x')); + assert!(pig.skip(b'x').is_err()); assert_eq!(pig.get_until_eos(), Some(&s[..])); } @@ -268,7 +273,7 @@ mod test { let s = b"foo"; let mut pig = Pig::new(s); assert!(pig.skip_n(3)); - assert!(!pig.skip(b'x')); + assert!(pig.skip(b'x').is_err()); } #[test]