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 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 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 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 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 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}