1use 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#[derive(PartialEq, Clone, Debug)]
14pub struct Location {
15 pub line: usize,
17
18 pub column: usize,
20}
21
22#[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 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 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 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 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}