cml/
error.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use cm_fidl_validator::error::ErrorList;
6use cm_types::ParseError;
7use fidl_fuchsia_component_decl as fdecl;
8use std::path::Path;
9use std::str::Utf8Error;
10use std::{error, fmt, io};
11
12/// The location in the file where an error was detected.
13#[derive(PartialEq, Clone, Debug)]
14pub struct Location {
15    /// One-based line number of the error.
16    pub line: usize,
17
18    /// One-based column number of the error.
19    pub column: usize,
20}
21
22/// Enum type that can represent any error encountered by a cml operation.
23#[derive(Debug)]
24pub enum Error {
25    DuplicateRights(String),
26    InvalidArgs(String),
27    Io(io::Error),
28    FidlEncoding(fidl::Error),
29    MissingRights(String),
30    Parse {
31        err: String,
32        location: Option<Location>,
33        filename: Option<String>,
34    },
35    Validate {
36        err: String,
37        filename: Option<String>,
38    },
39    FidlValidator {
40        errs: ErrorList,
41    },
42    Internal(String),
43    Utf8(Utf8Error),
44    /// A restricted feature was used without opting-in.
45    RestrictedFeature(String),
46}
47
48impl error::Error for Error {}
49
50impl Error {
51    pub fn invalid_args(err: impl Into<String>) -> Self {
52        Self::InvalidArgs(err.into())
53    }
54
55    pub fn parse(
56        err: impl fmt::Display,
57        location: Option<Location>,
58        filename: Option<&Path>,
59    ) -> Self {
60        Self::Parse {
61            err: err.to_string(),
62            location,
63            filename: filename.map(|f| f.to_string_lossy().into_owned()),
64        }
65    }
66
67    pub fn validate(err: impl fmt::Display) -> Self {
68        Self::Validate { err: err.to_string(), filename: None }
69    }
70
71    pub fn fidl_validator(errs: ErrorList) -> Self {
72        Self::FidlValidator { errs }
73    }
74
75    pub fn duplicate_rights(err: impl Into<String>) -> Self {
76        Self::DuplicateRights(err.into())
77    }
78
79    pub fn missing_rights(err: impl Into<String>) -> Self {
80        Self::MissingRights(err.into())
81    }
82
83    pub fn internal(err: impl Into<String>) -> Self {
84        Self::Internal(err.into())
85    }
86
87    pub fn json5(err: json5format::Error, file: Option<&Path>) -> Self {
88        match err {
89            json5format::Error::Configuration(errstr) => Error::Internal(errstr),
90            json5format::Error::Parse(location, errstr) => match location {
91                Some(location) => Error::parse(
92                    errstr,
93                    Some(Location { line: location.line, column: location.col }),
94                    file,
95                ),
96                None => Error::parse(errstr, None, file),
97            },
98            json5format::Error::Internal(location, errstr) => match location {
99                Some(location) => Error::Internal(format!("{}: {}", location, errstr)),
100                None => Error::Internal(errstr),
101            },
102            json5format::Error::TestFailure(location, errstr) => match location {
103                Some(location) => {
104                    Error::Internal(format!("{}: Test failure: {}", location, errstr))
105                }
106                None => Error::Internal(format!("Test failure: {}", errstr)),
107            },
108        }
109    }
110}
111
112impl fmt::Display for Error {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match &self {
115            Error::DuplicateRights(err) => write!(f, "Duplicate rights: {}", err),
116            Error::InvalidArgs(err) => write!(f, "Invalid args: {}", err),
117            Error::Io(err) => write!(f, "IO error: {}", err),
118            Error::FidlEncoding(err) => write!(f, "Fidl encoding error: {}", err),
119            Error::MissingRights(err) => write!(f, "Missing rights: {}", err),
120            Error::Parse { err, location, filename } => {
121                let mut prefix = String::new();
122                if let Some(filename) = filename {
123                    prefix.push_str(&format!("{}:", filename));
124                }
125                if let Some(location) = location {
126                    // Check for a syntax error generated by pest. These error messages have
127                    // the line and column number embedded in them, so we don't want to
128                    // duplicate that.
129                    //
130                    // TODO: If serde_json5 had an error type for json5 syntax errors, we wouldn't
131                    // need to parse the string like this.
132                    if !err.starts_with(" -->") {
133                        prefix.push_str(&format!("{}:{}:", location.line, location.column));
134                    }
135                }
136                if !prefix.is_empty() {
137                    write!(f, "Error at {} {}", prefix, err)
138                } else {
139                    write!(f, "{}", err)
140                }
141            }
142            Error::Validate {  err, filename } => {
143                let mut prefix = String::new();
144                if let Some(filename) = filename {
145                    prefix.push_str(&format!("{}:", filename));
146                }
147                if !prefix.is_empty() {
148                    write!(f, "Error at {} {}", prefix, err)
149                } else {
150                    write!(f, "{}", err)
151                }
152            }
153            Error::FidlValidator{ errs} => format_multiple_fidl_validator_errors(errs, f),
154            Error::Internal(err) => write!(f, "Internal error: {}", err),
155            Error::Utf8(err) => write!(f, "UTF8 error: {}", err),
156            Error::RestrictedFeature(feature) => write!(
157                f,
158                "Use of restricted feature \"{}\". To opt-in, see https://fuchsia.dev/go/components/restricted-features",
159                feature
160            ),
161        }
162    }
163}
164
165fn format_multiple_fidl_validator_errors(e: &ErrorList, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166    // Some errors are caught by `cm_fidl_validator` but not caught by `cml` validation.
167    //
168    // Our strategy is:
169    //
170    // - If a `cm_fidl_validator` error can be easily transformed back to be relevant in the context
171    //   of `cml`, do that. For example, we should transform the FIDL declaration names
172    //   to corresponding cml names.
173    //
174    // - Otherwise, we consider that a bug and we should add corresponding validation in `cml`.
175    //   As such, we will surface this kind of errors as `Internal` as an indication.
176    //   That is represented by the `_` match arm here.
177    //
178    use cm_fidl_validator::error::Error as CmFidlError;
179    let mut found_internal_errors = false;
180    for e in e.errs.iter() {
181        match e {
182            CmFidlError::DifferentAvailabilityInAggregation(availability_list) => {
183                // Format the availability in `cml` syntax.
184                let comma_separated = availability_list
185                    .0
186                    .iter()
187                    .map(|s| match s {
188                        fdecl::Availability::Required => "\"required\"".to_string(),
189                        fdecl::Availability::Optional => "\"optional\"".to_string(),
190                        fdecl::Availability::SameAsTarget => "\"same_as_target\"".to_string(),
191                        fdecl::Availability::Transitional => "\"transitional\"".to_string(),
192                    })
193                    .collect::<Vec<_>>()
194                    .join(", ");
195
196                write!(f, "All sources that feed into an aggregation operation should have the same availability. ")?;
197                write!(f, "Got [ {comma_separated} ].")?;
198            }
199            _ => {
200                write!(f, "Internal error: {}\n", e)?;
201                found_internal_errors = true;
202            }
203        }
204    }
205
206    if found_internal_errors {
207        write!(
208            f,
209            "This reflects error(s) in the `.cml` file. \
210Unfortunately, for some of them, cmc cannot provide more details at this time.
211Please file a bug at https://bugs.fuchsia.dev/p/fuchsia/issues/entry?template=ComponentFramework \
212with the cml in question, so we can work on better error reporting."
213        )?;
214    }
215
216    Ok(())
217}
218
219impl From<io::Error> for Error {
220    fn from(err: io::Error) -> Self {
221        Error::Io(err)
222    }
223}
224
225impl From<Utf8Error> for Error {
226    fn from(err: Utf8Error) -> Self {
227        Error::Utf8(err)
228    }
229}
230
231impl From<serde_json::error::Error> for Error {
232    fn from(err: serde_json::error::Error) -> Self {
233        use serde_json::error::Category;
234        match err.classify() {
235            Category::Io | Category::Eof => Error::Io(err.into()),
236            Category::Syntax => {
237                let line = err.line();
238                let column = err.column();
239                Error::parse(err, Some(Location { line, column }), None)
240            }
241            Category::Data => Error::validate(err),
242        }
243    }
244}
245
246impl From<fidl::Error> for Error {
247    fn from(err: fidl::Error) -> Self {
248        Error::FidlEncoding(err)
249    }
250}
251
252impl From<ParseError> for Error {
253    fn from(err: ParseError) -> Self {
254        match err {
255            ParseError::InvalidValue => Self::internal("invalid value"),
256            ParseError::InvalidComponentUrl { details } => {
257                Self::internal(&format!("invalid component url: {details}"))
258            }
259            ParseError::TooLong => Self::internal("too long"),
260            ParseError::Empty => Self::internal("empty"),
261            ParseError::InvalidSegment => Self::internal("invalid path segment"),
262            ParseError::NoLeadingSlash => Self::internal("missing leading slash"),
263        }
264    }
265}
266
267impl TryFrom<serde_json5::Error> for Location {
268    type Error = &'static str;
269    fn try_from(e: serde_json5::Error) -> Result<Self, Self::Error> {
270        match e {
271            serde_json5::Error::Message { location: Some(l), .. } => {
272                Ok(Location { line: l.line, column: l.column })
273            }
274            _ => Err("location unavailable"),
275        }
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use anyhow::format_err;
283    use assert_matches::assert_matches;
284    use cm_types as cm;
285
286    #[test]
287    fn test_syntax_error_message() {
288        let result = serde_json::from_str::<cm::Name>("foo").map_err(Error::from);
289        assert_matches!(result, Err(Error::Parse { .. }));
290    }
291
292    #[test]
293    fn test_validation_error_message() {
294        let result = serde_json::from_str::<cm::Name>("\"foo$\"").map_err(Error::from);
295        assert_matches!(result, Err(Error::Validate { .. }));
296    }
297
298    #[test]
299    fn test_parse_error() {
300        let result = Error::parse(format_err!("oops"), None, None);
301        assert_eq!(format!("{}", result), "oops");
302
303        let result = Error::parse(format_err!("oops"), Some(Location { line: 2, column: 3 }), None);
304        assert_eq!(format!("{}", result), "Error at 2:3: oops");
305
306        let result = Error::parse(
307            format_err!("oops"),
308            Some(Location { line: 2, column: 3 }),
309            Some(&Path::new("test.cml")),
310        );
311        assert_eq!(format!("{}", result), "Error at test.cml:2:3: oops");
312
313        let result = Error::parse(
314            format_err!(" --> pest error"),
315            Some(Location { line: 42, column: 42 }),
316            Some(&Path::new("test.cml")),
317        );
318        assert_eq!(format!("{}", result), "Error at test.cml:  --> pest error");
319    }
320
321    #[test]
322    fn test_validation_error() {
323        let mut result = Error::validate(format_err!("oops"));
324        assert_eq!(format!("{}", result), "oops");
325
326        if let Error::Validate { filename, .. } = &mut result {
327            *filename = Some("test.cml".to_string());
328        }
329        assert_eq!(format!("{}", result), "Error at test.cml: oops");
330    }
331
332    #[test]
333    fn test_format_multiple_fidl_validator_errors() {
334        use cm_fidl_validator::error::{AvailabilityList, Error as CmFidlError};
335
336        let error = Error::FidlValidator {
337            errs: ErrorList {
338                errs: vec![CmFidlError::DifferentAvailabilityInAggregation(AvailabilityList(
339                    vec![fdecl::Availability::Required, fdecl::Availability::Optional],
340                ))],
341            },
342        };
343        assert_eq!(
344            format!("{error}"),
345            "All sources that feed into an aggregation operation should \
346            have the same availability. Got [ \"required\", \"optional\" ]."
347        );
348    }
349}