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