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