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