use cm_fidl_validator::error::ErrorList;
use cm_types::ParseError;
use fidl_fuchsia_component_decl as fdecl;
use std::path::Path;
use std::str::Utf8Error;
use std::{error, fmt, io};
#[derive(PartialEq, Clone, Debug)]
pub struct Location {
pub line: usize,
pub column: usize,
}
#[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 {
err: String,
filename: Option<String>,
},
FidlValidator {
errs: ErrorList,
},
Internal(String),
Utf8(Utf8Error),
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 { err: err.to_string(), filename: None }
}
pub fn fidl_validator(errs: ErrorList) -> Self {
Self::FidlValidator { errs }
}
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: Option<&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 }),
file,
),
None => Error::parse(errstr, None, 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 {
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 { 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::FidlValidator{ errs} => format_multiple_fidl_validator_errors(errs, f),
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
),
}
}
}
fn format_multiple_fidl_validator_errors(e: &ErrorList, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use cm_fidl_validator::error::Error as CmFidlError;
let mut found_internal_errors = false;
for e in e.errs.iter() {
match e {
CmFidlError::DifferentAvailabilityInAggregation(availability_list) => {
let comma_separated = availability_list
.0
.iter()
.map(|s| match s {
fdecl::Availability::Required => "\"required\"".to_string(),
fdecl::Availability::Optional => "\"optional\"".to_string(),
fdecl::Availability::SameAsTarget => "\"same_as_target\"".to_string(),
fdecl::Availability::Transitional => "\"transitional\"".to_string(),
})
.collect::<Vec<_>>()
.join(", ");
write!(f, "All sources that feed into an aggregation operation should have the same availability. ")?;
write!(f, "Got [ {comma_separated} ].")?;
}
_ => {
write!(f, "Internal error: {}\n", e)?;
found_internal_errors = true;
}
}
}
if found_internal_errors {
write!(
f,
"This reflects error(s) in the `.cml` file. \
Unfortunately, for some of them, cmc cannot provide more details at this time.
Please file a bug at https://bugs.fuchsia.dev/p/fuchsia/issues/entry?template=ComponentFramework \
with the cml in question, so we can work on better error reporting."
)?;
}
Ok(())
}
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<ParseError> for Error {
fn from(err: ParseError) -> Self {
match err {
ParseError::InvalidValue => Self::internal("invalid value"),
ParseError::InvalidComponentUrl { details } => {
Self::internal(&format!("invalid component url: {details}"))
}
ParseError::TooLong => Self::internal("too long"),
ParseError::Empty => Self::internal("empty"),
ParseError::InvalidSegment => Self::internal("invalid path segment"),
ParseError::NoLeadingSlash => Self::internal("missing leading slash"),
}
}
}
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 anyhow::format_err;
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 { .. }));
}
#[test]
fn test_parse_error() {
let result = Error::parse(format_err!("oops"), None, None);
assert_eq!(format!("{}", result), "oops");
let result = Error::parse(format_err!("oops"), Some(Location { line: 2, column: 3 }), None);
assert_eq!(format!("{}", result), "Error at 2:3: oops");
let result = Error::parse(
format_err!("oops"),
Some(Location { line: 2, column: 3 }),
Some(&Path::new("test.cml")),
);
assert_eq!(format!("{}", result), "Error at test.cml:2:3: oops");
let result = Error::parse(
format_err!(" --> pest error"),
Some(Location { line: 42, column: 42 }),
Some(&Path::new("test.cml")),
);
assert_eq!(format!("{}", result), "Error at test.cml: --> pest error");
}
#[test]
fn test_validation_error() {
let mut result = Error::validate(format_err!("oops"));
assert_eq!(format!("{}", result), "oops");
if let Error::Validate { filename, .. } = &mut result {
*filename = Some("test.cml".to_string());
}
assert_eq!(format!("{}", result), "Error at test.cml: oops");
}
#[test]
fn test_format_multiple_fidl_validator_errors() {
use cm_fidl_validator::error::{AvailabilityList, Error as CmFidlError};
let error = Error::FidlValidator {
errs: ErrorList {
errs: vec![CmFidlError::DifferentAvailabilityInAggregation(AvailabilityList(
vec![fdecl::Availability::Required, fdecl::Availability::Optional],
))],
},
};
assert_eq!(
format!("{error}"),
"All sources that feed into an aggregation operation should \
have the same availability. Got [ \"required\", \"optional\" ]."
);
}
}