1use 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#[derive(PartialEq, Clone, Debug, Eq, Hash, PartialOrd, Ord, Default)]
15pub struct Location {
16 pub line: usize,
18
19 pub column: usize,
21}
22
23#[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 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 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 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 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}