mirror of
https://github.com/JockeTF/fimfareader.git
synced 2024-11-27 07:28:00 +01:00
Use struct for error handling
This commit is contained in:
parent
b250e8e1cb
commit
dbebf22d42
5 changed files with 172 additions and 59 deletions
|
@ -1,6 +1,7 @@
|
||||||
//! Archive fetcher.
|
//! Archive fetcher.
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::io::ErrorKind as IoErrorKind;
|
||||||
use std::io::{BufReader, Read, Seek};
|
use std::io::{BufReader, Read, Seek};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
@ -8,9 +9,9 @@ use std::sync::Mutex;
|
||||||
use zip::read::ZipArchive;
|
use zip::read::ZipArchive;
|
||||||
use zip::result::ZipError;
|
use zip::result::ZipError;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
|
||||||
use super::parser::parse;
|
use super::parser::parse;
|
||||||
use super::story::Story;
|
use super::story::Story;
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
pub struct Fetcher<T>
|
pub struct Fetcher<T>
|
||||||
where
|
where
|
||||||
|
@ -22,10 +23,11 @@ where
|
||||||
|
|
||||||
impl Fetcher<BufReader<File>> {
|
impl Fetcher<BufReader<File>> {
|
||||||
pub fn from(path: impl AsRef<Path>) -> Result<Self> {
|
pub fn from(path: impl AsRef<Path>) -> Result<Self> {
|
||||||
use Error::*;
|
use IoErrorKind::*;
|
||||||
|
|
||||||
let file = File::open(path).map_err(|e| match e {
|
let file = File::open(path).map_err(|e| match e.kind() {
|
||||||
_ => SourceError("Could not open archive file."),
|
NotFound => Error::archive("File not found"),
|
||||||
|
_ => Error::archive("Could not open file"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Self::from_reader(BufReader::with_capacity(8_000_000, file))
|
Self::from_reader(BufReader::with_capacity(8_000_000, file))
|
||||||
|
@ -45,26 +47,24 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open(archive: T) -> Result<ZipArchive<T>> {
|
fn open(archive: T) -> Result<ZipArchive<T>> {
|
||||||
use Error::*;
|
|
||||||
use ZipError::*;
|
use ZipError::*;
|
||||||
|
|
||||||
ZipArchive::new(archive).map_err(|e| match e {
|
ZipArchive::new(archive).map_err(|e| match e {
|
||||||
InvalidArchive(e) => SourceError(e),
|
InvalidArchive(e) => Error::archive(e),
|
||||||
UnsupportedArchive(e) => SourceError(e),
|
UnsupportedArchive(e) => Error::archive(e),
|
||||||
_ => SourceError("Could not read archive."),
|
_ => Error::archive("Unknown ZIP-file issue"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(archive: &mut ZipArchive<T>) -> Result<Vec<Story>> {
|
fn load(archive: &mut ZipArchive<T>) -> Result<Vec<Story>> {
|
||||||
use Error::*;
|
|
||||||
use ZipError::*;
|
use ZipError::*;
|
||||||
|
|
||||||
let file = archive.by_name("index.json").map_err(|e| match e {
|
let file = archive.by_name("index.json").map_err(|e| match e {
|
||||||
FileNotFound => SourceError("Missing archive index."),
|
FileNotFound => Error::archive("Missing story index"),
|
||||||
_ => SourceError("Could not open archive index."),
|
_ => Error::archive("Could not open story index"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
parse(BufReader::with_capacity(8_000_000, file))
|
parse(BufReader::with_capacity(8_000_000, file)).map_err(Error::index)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(&self, key: i64) -> Option<&Story> {
|
pub fn fetch(&self, key: i64) -> Option<&Story> {
|
||||||
|
@ -75,23 +75,23 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(&self, path: &str) -> Result<Vec<u8>> {
|
pub fn read(&self, path: &str) -> Result<Vec<u8>> {
|
||||||
use Error::*;
|
|
||||||
use ZipError::*;
|
use ZipError::*;
|
||||||
|
|
||||||
let mut archive = self.archive.lock().map_err(|e| match e {
|
let mut archive = self.archive.lock().map_err(|e| match e {
|
||||||
_ => SourceError("Could not acquire archive lock."),
|
_ => Error::archive("Could not acquire fetcher lock"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut file = archive.by_name(path).map_err(|e| match e {
|
let mut file = archive.by_name(path).map_err(|e| match e {
|
||||||
FileNotFound => SourceError("File not found."),
|
FileNotFound => Error::archive("Missing story data"),
|
||||||
_ => SourceError("Could not open file."),
|
_ => Error::archive("Could not open story data"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let size = file.size() as usize;
|
let size = file.size() as usize;
|
||||||
let mut buf = Vec::with_capacity(size);
|
let mut buf = Vec::with_capacity(size);
|
||||||
|
|
||||||
file.read_to_end(&mut buf)
|
file.read_to_end(&mut buf).map_err(|e| match e {
|
||||||
.map_err(|_| SourceError("Could not read file."))?;
|
_ => Error::archive("Could not read story data"),
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,21 @@ use std::io::BufRead;
|
||||||
use std::sync::mpsc::{channel, Receiver};
|
use std::sync::mpsc::{channel, Receiver};
|
||||||
use std::thread::spawn;
|
use std::thread::spawn;
|
||||||
|
|
||||||
|
use serde::de::Error;
|
||||||
|
use serde_json::error::Result;
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
|
||||||
use super::story::Story;
|
use super::story::Story;
|
||||||
|
|
||||||
const TRIM: &[char] = &['"', ',', ' ', '\t', '\n', '\r'];
|
const TRIM: &[char] = &['"', ',', ' ', '\t', '\n', '\r'];
|
||||||
|
|
||||||
pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> {
|
pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> {
|
||||||
use Error::*;
|
|
||||||
|
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
let rx = spawn_parser(rx);
|
let rx = spawn_parser(rx);
|
||||||
|
|
||||||
for line in reader.lines() {
|
for line in reader.lines() {
|
||||||
let line = line.map_err(|e| match e {
|
let line = line.map_err(|e| match e {
|
||||||
_ => SourceError("Could not read index line."),
|
_ => Error::custom("Could not read line"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if tx.send(line).is_ok() {
|
if tx.send(line).is_ok() {
|
||||||
|
@ -27,8 +26,8 @@ pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(match rx.recv() {
|
return Err(match rx.recv() {
|
||||||
Err(_) => SourceError("Parser disappeared unexpectedly."),
|
Err(_) => Error::custom("Parser disappeared unexpectedly"),
|
||||||
Ok(Ok(_)) => SourceError("Parser returned unexpectedly."),
|
Ok(Ok(_)) => Error::custom("Parser returned unexpectedly"),
|
||||||
Ok(Err(error)) => error,
|
Ok(Err(error)) => error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -36,13 +35,11 @@ pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> {
|
||||||
drop(tx);
|
drop(tx);
|
||||||
|
|
||||||
rx.recv().map_err(|e| match e {
|
rx.recv().map_err(|e| match e {
|
||||||
_ => SourceError("Missing parser result."),
|
_ => Error::custom("Missing parser result"),
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> {
|
fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> {
|
||||||
use Error::*;
|
|
||||||
|
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
spawn(move || {
|
spawn(move || {
|
||||||
|
@ -68,11 +65,11 @@ fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> {
|
||||||
stories.shrink_to_fit();
|
stories.shrink_to_fit();
|
||||||
|
|
||||||
if wrappers != "{}" {
|
if wrappers != "{}" {
|
||||||
return tx.send(Err(SourceError("Invalid index structure.")));
|
return tx.send(Err(Error::custom("Invalid file structure")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if count != stories.len() {
|
if count != stories.len() {
|
||||||
return tx.send(Err(SourceError("Index contains duplicates.")));
|
return tx.send(Err(Error::custom("Found duplicate story")));
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.send(Ok(stories))
|
tx.send(Ok(stories))
|
||||||
|
@ -82,8 +79,6 @@ fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize(line: String) -> Result<Story> {
|
fn deserialize(line: String) -> Result<Story> {
|
||||||
use Error::*;
|
|
||||||
|
|
||||||
let split = line
|
let split = line
|
||||||
.splitn(2, ':')
|
.splitn(2, ':')
|
||||||
.map(|value| value.trim_matches(TRIM))
|
.map(|value| value.trim_matches(TRIM))
|
||||||
|
@ -91,19 +86,17 @@ fn deserialize(line: String) -> Result<Story> {
|
||||||
|
|
||||||
let (skey, json) = match split[..] {
|
let (skey, json) = match split[..] {
|
||||||
[skey, json] => Ok((skey, json)),
|
[skey, json] => Ok((skey, json)),
|
||||||
_ => Err(SourceError("Invalid line format.")),
|
_ => Err(Error::custom("Invalid line format")),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let key: i64 = skey.parse().map_err(|e| match e {
|
let story: Story = from_str(json)?;
|
||||||
_ => SourceError("Invalid meta key."),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let story: Story = from_str(json).map_err(|e| match e {
|
let key: i64 = skey.parse().map_err(|e| match e {
|
||||||
_ => SourceError("Invalid meta value."),
|
_ => Error::custom("Invalid line key"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if key != story.id {
|
if key != story.id {
|
||||||
return Err(SourceError("Meta key mismatch."));
|
return Err(Error::custom("Line key mismatch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(story)
|
Ok(story)
|
||||||
|
|
|
@ -182,10 +182,10 @@ where
|
||||||
Ok(value.as_i64().unwrap())
|
Ok(value.as_i64().unwrap())
|
||||||
} else if value.is_string() {
|
} else if value.is_string() {
|
||||||
value.as_str().unwrap().parse().map_err(|e| match e {
|
value.as_str().unwrap().parse().map_err(|e| match e {
|
||||||
_ => Error::custom("Could not parse ID string."),
|
_ => Error::custom("Could not parse ID string"),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::custom("Invalid type for ID value."))
|
Err(Error::custom("Invalid type for ID value"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,15 +199,15 @@ impl<'de> Deserialize<'de> for Color {
|
||||||
let text = object
|
let text = object
|
||||||
.get("hex")
|
.get("hex")
|
||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.ok_or_else(|| Error::custom("Color is missing hex value."))?;
|
.ok_or_else(|| Error::custom("Color is missing hex value"))?;
|
||||||
|
|
||||||
let array = hex::decode(text).map_err(|e| match e {
|
let array = hex::decode(text).map_err(|e| match e {
|
||||||
_ => Error::custom("Color hex has invalid value."),
|
_ => Error::custom("Color hex has invalid value"),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match array[..] {
|
match array[..] {
|
||||||
[red, green, blue] => Ok(Color { red, green, blue }),
|
[red, green, blue] => Ok(Color { red, green, blue }),
|
||||||
_ => Err(Error::custom("Color hex has invalid length.")),
|
_ => Err(Error::custom("Color hex has invalid length")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
128
src/error.rs
128
src/error.rs
|
@ -1,10 +1,126 @@
|
||||||
//! Error types.
|
//! Error types.
|
||||||
|
|
||||||
#[derive(Debug)]
|
use std::error::Error as StdError;
|
||||||
pub enum Error {
|
use std::fmt::Result as FmtResult;
|
||||||
InvalidStory(),
|
use std::fmt::{Display, Formatter};
|
||||||
SourceError(&'static str),
|
use std::result::Result as StdResult;
|
||||||
UserError(&'static str),
|
|
||||||
|
use serde_json::error::Error as SerdeError;
|
||||||
|
|
||||||
|
use self::ErrorKind::*;
|
||||||
|
|
||||||
|
pub type Result<T> = StdResult<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
ArchiveError,
|
||||||
|
IndexError,
|
||||||
|
InvalidStory,
|
||||||
|
UsageError,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
#[derive(Debug)]
|
||||||
|
pub struct Error {
|
||||||
|
kind: ErrorKind,
|
||||||
|
message: Option<String>,
|
||||||
|
source: Option<Box<dyn StdError + 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ErrorBuilder(Error);
|
||||||
|
|
||||||
|
impl ErrorBuilder {
|
||||||
|
pub fn new(kind: ErrorKind) -> Self {
|
||||||
|
ErrorBuilder(Error {
|
||||||
|
kind,
|
||||||
|
message: None,
|
||||||
|
source: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(mut self, message: impl ToString) -> Self {
|
||||||
|
self.0.message = Some(message.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source(mut self, source: impl StdError + 'static) -> Self {
|
||||||
|
self.0.source = Some(Box::new(source));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Error {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn archive(message: impl ToString) -> Self {
|
||||||
|
ErrorBuilder::new(ArchiveError).message(message).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(error: SerdeError) -> Self {
|
||||||
|
ErrorBuilder::new(IndexError)
|
||||||
|
.message(&error)
|
||||||
|
.source(error)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid() -> Self {
|
||||||
|
ErrorBuilder::new(InvalidStory).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn usage(message: impl ToString) -> Self {
|
||||||
|
ErrorBuilder::new(UsageError).message(message).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> ErrorKind {
|
||||||
|
self.kind.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> Option<&String> {
|
||||||
|
self.message.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower(message: &str) -> String {
|
||||||
|
let mut chars = message.chars();
|
||||||
|
|
||||||
|
let head: String = match chars.next() {
|
||||||
|
Some(c) => c.to_lowercase().collect(),
|
||||||
|
None => return String::from(message),
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{}{}", head, chars.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ErrorKind {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||||
|
let message = match self {
|
||||||
|
ArchiveError => "Archive error",
|
||||||
|
IndexError => "Index error",
|
||||||
|
InvalidStory => "Invalid story",
|
||||||
|
UsageError => "Usage error",
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{}", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||||
|
let message = match &self.message {
|
||||||
|
Some(message) => message,
|
||||||
|
None => "Unknown cause",
|
||||||
|
};
|
||||||
|
|
||||||
|
let kind = self.kind();
|
||||||
|
let info = lower(message);
|
||||||
|
|
||||||
|
write!(f, "{}, {}.", kind, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
|
self.source.as_ref().map(Box::as_ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -7,26 +7,30 @@ use std::env::args;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::archive::Fetcher;
|
use crate::archive::Fetcher;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::Error;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn exit(error: Error) -> ! {
|
||||||
use Error::*;
|
eprintln!("{}", error);
|
||||||
|
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
let argv = args().collect::<Vec<String>>();
|
let argv = args().collect::<Vec<String>>();
|
||||||
|
|
||||||
let path = match argv.len() {
|
if argv.len() != 2 {
|
||||||
2 => Ok(argv.get(1).unwrap()),
|
eprintln!("Usage: fimfareader <ARCHIVE>");
|
||||||
_ => Err(UserError("Usage: fimfareader <ARCHIVE>")),
|
std::process::exit(1);
|
||||||
}?;
|
}
|
||||||
|
|
||||||
println!("Hellopaca, World!");
|
println!("Hellopaca, World!");
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let fetcher = Fetcher::from(path)?;
|
let result = Fetcher::from(&argv[1]);
|
||||||
let finish = Instant::now() - start;
|
let finish = Instant::now() - start;
|
||||||
|
|
||||||
|
let fetcher = result.map_err(exit).unwrap();
|
||||||
|
|
||||||
println!("Finished loading in {} milliseconds.", finish.as_millis());
|
println!("Finished loading in {} milliseconds.", finish.as_millis());
|
||||||
println!("The archive contains {} stories.", fetcher.iter().count());
|
println!("The archive contains {} stories.", fetcher.iter().count());
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue