blob: b47fce5b448ab5da38b5f475a0a35c7522697d76 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
cm_types::ParseError,
std::path::Path,
std::str::Utf8Error,
std::{convert::TryFrom, error, fmt, io},
};
/// The location in the file where an error was detected.
#[derive(PartialEq, Clone, Debug)]
pub struct Location {
/// One-based line number of the error.
pub line: usize,
/// One-based column number of the error.
pub column: usize,
}
/// Enum type that can represent any error encountered by a cmx operation.
#[derive(Debug)]
pub enum Error {
DuplicateRights(String),
InvalidArgs(String),
Io(io::Error),
FidlEncoding(fidl::Error),
MissingRights(String),
Parse {
err: String,
location: Option<Location>,
filename: Option<String>,
},
Validate {
schema_name: Option<String>,
err: String,
filename: Option<String>,
},
Internal(String),
Utf8(Utf8Error),
/// A restricted feature was used without opting-in.
RestrictedFeature(String),
}
impl error::Error for Error {}
impl Error {
pub fn invalid_args(err: impl Into<String>) -> Self {
Self::InvalidArgs(err.into())
}
pub fn parse(
err: impl fmt::Display,
location: Option<Location>,
filename: Option<&Path>,
) -> Self {
Self::Parse {
err: err.to_string(),
location,
filename: filename.map(|f| f.to_string_lossy().into_owned()),
}
}
pub fn validate(err: impl fmt::Display) -> Self {
Self::Validate { schema_name: None, err: err.to_string(), filename: None }
}
pub fn validate_schema(schema_name: &str, err: impl Into<String>) -> Self {
Self::Validate {
schema_name: Some(schema_name.to_string()),
err: err.into(),
filename: None,
}
}
pub fn duplicate_rights(err: impl Into<String>) -> Self {
Self::DuplicateRights(err.into())
}
pub fn missing_rights(err: impl Into<String>) -> Self {
Self::MissingRights(err.into())
}
pub fn internal(err: impl Into<String>) -> Self {
Self::Internal(err.into())
}
pub fn json5(err: json5format::Error, file: &Path) -> Self {
match err {
json5format::Error::Configuration(errstr) => Error::Internal(errstr),
json5format::Error::Parse(location, errstr) => match location {
Some(location) => Error::parse(
errstr,
Some(Location { line: location.line, column: location.col }),
Some(file),
),
None => Error::parse(errstr, None, Some(file)),
},
json5format::Error::Internal(location, errstr) => match location {
Some(location) => Error::Internal(format!("{}: {}", location, errstr)),
None => Error::Internal(errstr),
},
json5format::Error::TestFailure(location, errstr) => match location {
Some(location) => {
Error::Internal(format!("{}: Test failure: {}", location, errstr))
}
None => Error::Internal(format!("Test failure: {}", errstr)),
},
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Error::DuplicateRights(err) => write!(f, "Duplicate rights: {}", err),
Error::InvalidArgs(err) => write!(f, "Invalid args: {}", err),
Error::Io(err) => write!(f, "IO error: {}", err),
Error::FidlEncoding(err) => write!(f, "Fidl encoding error: {}", err),
Error::MissingRights(err) => write!(f, "Missing rights: {}", err),
Error::Parse { err, location, filename } => {
let mut prefix = String::new();
if let Some(filename) = filename {
prefix.push_str(&format!("{}:", filename));
}
if let Some(location) = location {
// Check for a syntax error generated by pest. These error messages have
// the line and column number embedded in them, so we don't want to
// duplicate that.
//
// TODO: If serde_json5 had an error type for json5 syntax errors, we wouldn't
// need to parse the string like this.
if !err.starts_with(" -->") {
prefix.push_str(&format!("{}:{}:", location.line, location.column));
}
}
if !prefix.is_empty() {
write!(f, "Error at {} {}", prefix, err)
} else {
write!(f, "{}", err)
}
}
Error::Validate { schema_name: _, err, filename } => {
let mut prefix = String::new();
if let Some(filename) = filename {
prefix.push_str(&format!("{}:", filename));
}
if !prefix.is_empty() {
write!(f, "Error at {} {}", prefix, err)
} else {
write!(f, "{}", err)
}
}
Error::Internal(err) => write!(f, "Internal error: {}", err),
Error::Utf8(err) => write!(f, "UTF8 error: {}", err),
Error::RestrictedFeature(feature) => write!(
f,
"Use of restricted feature \"{}\". To opt-in, see https://fuchsia.dev/go/components/restricted-features",
feature
),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::Io(err)
}
}
impl From<Utf8Error> for Error {
fn from(err: Utf8Error) -> Self {
Error::Utf8(err)
}
}
impl From<serde_json::error::Error> for Error {
fn from(err: serde_json::error::Error) -> Self {
use serde_json::error::Category;
match err.classify() {
Category::Io | Category::Eof => Error::Io(err.into()),
Category::Syntax => {
let line = err.line();
let column = err.column();
Error::parse(err, Some(Location { line, column }), None)
}
Category::Data => Error::validate(err),
}
}
}
impl From<fidl::Error> for Error {
fn from(err: fidl::Error) -> Self {
Error::FidlEncoding(err)
}
}
impl From<cm_json::Error> for Error {
fn from(err: cm_json::Error) -> Self {
match err {
cm_json::Error::Io(err) => Error::Io(err),
cm_json::Error::Parse { err, location, filename } => {
Error::Parse { err, location: location.map(|loc| loc.into()), filename }
}
cm_json::Error::Validate { schema_name, err, filename } => {
Error::Validate { schema_name, err, filename }
}
}
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Self {
match err {
ParseError::InvalidValue => Self::internal("invalid value"),
ParseError::InvalidLength => Self::internal("invalid length"),
ParseError::NotAName => Self::internal("not a name"),
ParseError::NotAPath => Self::internal("not a path"),
}
}
}
impl From<cm_json::Location> for Location {
fn from(location: cm_json::Location) -> Self {
Location { line: location.line, column: location.column }
}
}
impl TryFrom<serde_json5::Error> for Location {
type Error = &'static str;
fn try_from(e: serde_json5::Error) -> Result<Self, Self::Error> {
match e {
serde_json5::Error::Message { location: Some(l), .. } => {
Ok(Location { line: l.line, column: l.column })
}
_ => Err("location unavailable"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use cm_types as cm;
#[test]
fn test_syntax_error_message() {
let result = serde_json::from_str::<cm::Name>("foo").map_err(Error::from);
assert_matches!(result, Err(Error::Parse { .. }));
}
#[test]
fn test_validation_error_message() {
let result = serde_json::from_str::<cm::Name>("\"foo$\"").map_err(Error::from);
assert_matches!(result, Err(Error::Validate { .. }));
}
}