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)]
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
136impl fmt::Display for Error {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match &self {
139 Error::DuplicateRights(err) => write!(f, "Duplicate rights: {}", err),
140 Error::InvalidArgs(err) => write!(f, "Invalid args: {}", err),
141 Error::Io(err) => write!(f, "IO error: {}", err),
142 Error::FidlEncoding(err) => write!(f, "Fidl encoding error: {}", err),
143 Error::Merge { err, origin } => {
144 let mut prefix = String::new();
145
146 if let Some(origin) = origin {
147 prefix.push_str(&format!("{:?}:", origin.file));
148 prefix
149 .push_str(&format!("{}:{}:", origin.location.line, origin.location.column));
150 }
151
152 if !prefix.is_empty() {
153 write!(f, "Error merging at {} {}", prefix, err)
154 } else {
155 write!(f, "{}", err)
156 }
157 }
158 Error::MissingRights(err) => write!(f, "Missing rights: {}", err),
159 Error::Parse { err, location, filename } => {
160 let mut prefix = String::new();
161 if let Some(filename) = filename {
162 prefix.push_str(&format!("{}:", filename));
163 }
164 if let Some(location) = location {
165 if !err.starts_with(" -->") {
172 prefix.push_str(&format!("{}:{}:", location.line, location.column));
173 }
174 }
175 if !prefix.is_empty() {
176 write!(f, "Error at {} {}", prefix, err)
177 } else {
178 write!(f, "{}", err)
179 }
180 }
181 Error::Validate { err, filename } => {
182 let mut prefix = String::new();
183 if let Some(filename) = filename {
184 prefix.push_str(&format!("{}:", filename));
185 }
186 if !prefix.is_empty() {
187 write!(f, "Error at {} {}", prefix, err)
188 } else {
189 write!(f, "{}", err)
190 }
191 }
192 Error::ValidateContext { err, origin } => {
193 let mut prefix = String::new();
194
195 if let Some(origin) = origin {
196 prefix.push_str(&format!("{:?}:", origin.file));
197 prefix
198 .push_str(&format!("{}:{}:", origin.location.line, origin.location.column));
199 }
200
201 if !prefix.is_empty() {
202 write!(f, "Error at {} {}", prefix, err)
203 } else {
204 write!(f, "{}", err)
205 }
206 }
207 Error::ValidateContexts { err, origins } => {
208 let mut prefix = String::new();
209
210 for origin in origins {
211 if !prefix.is_empty() {
212 prefix.push_str(", and ");
213 }
214
215 prefix.push_str(&format!("{:?}:", origin.file));
216 prefix
217 .push_str(&format!("{}:{}:", origin.location.line, origin.location.column));
218 }
219
220 if !prefix.is_empty() {
221 write!(f, "Error at {} {}", prefix, err)
222 } else {
223 write!(f, "{}", err)
224 }
225 }
226 Error::FidlValidator { errs } => format_multiple_fidl_validator_errors(errs, f),
227 Error::Internal(err) => write!(f, "Internal error: {}", err),
228 Error::Utf8(err) => write!(f, "UTF8 error: {}", err),
229 Error::RestrictedFeature(feature) => write!(
230 f,
231 "Use of restricted feature \"{}\". To opt-in, see https://fuchsia.dev/go/components/restricted-features",
232 feature
233 ),
234 }
235 }
236}
237
238fn format_multiple_fidl_validator_errors(e: &ErrorList, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 use cm_fidl_validator::error::Error as CmFidlError;
252 let mut found_internal_errors = false;
253 for e in e.errs.iter() {
254 match e {
255 CmFidlError::DifferentAvailabilityInAggregation(availability_list) => {
256 let comma_separated = availability_list
258 .0
259 .iter()
260 .map(|s| match s {
261 fdecl::Availability::Required => "\"required\"".to_string(),
262 fdecl::Availability::Optional => "\"optional\"".to_string(),
263 fdecl::Availability::SameAsTarget => "\"same_as_target\"".to_string(),
264 fdecl::Availability::Transitional => "\"transitional\"".to_string(),
265 })
266 .collect::<Vec<_>>()
267 .join(", ");
268
269 write!(
270 f,
271 "All sources that feed into an aggregation operation should have the same availability. "
272 )?;
273 write!(f, "Got [ {comma_separated} ].")?;
274 }
275 _ => {
276 write!(f, "Internal error: {}\n", e)?;
277 found_internal_errors = true;
278 }
279 }
280 }
281
282 if found_internal_errors {
283 write!(
284 f,
285 "This reflects error(s) in the `.cml` file. \
286Unfortunately, for some of them, cmc cannot provide more details at this time.
287Please file a bug at https://bugs.fuchsia.dev/p/fuchsia/issues/entry?template=ComponentFramework \
288with the cml in question, so we can work on better error reporting."
289 )?;
290 }
291
292 Ok(())
293}
294
295impl From<io::Error> for Error {
296 fn from(err: io::Error) -> Self {
297 Error::Io(err)
298 }
299}
300
301impl From<Utf8Error> for Error {
302 fn from(err: Utf8Error) -> Self {
303 Error::Utf8(err)
304 }
305}
306
307impl From<serde_json::error::Error> for Error {
308 fn from(err: serde_json::error::Error) -> Self {
309 use serde_json::error::Category;
310 match err.classify() {
311 Category::Io | Category::Eof => Error::Io(err.into()),
312 Category::Syntax => {
313 let line = err.line();
314 let column = err.column();
315 Error::parse(err, Some(Location { line, column }), None)
316 }
317 Category::Data => Error::validate(err),
318 }
319 }
320}
321
322impl From<fidl::Error> for Error {
323 fn from(err: fidl::Error) -> Self {
324 Error::FidlEncoding(err)
325 }
326}
327
328impl From<ParseError> for Error {
329 fn from(err: ParseError) -> Self {
330 match err {
331 ParseError::InvalidValue => Self::internal("invalid value"),
332 ParseError::InvalidComponentUrl { details } => {
333 Self::internal(&format!("invalid component url: {details}"))
334 }
335 ParseError::TooLong => Self::internal("too long"),
336 ParseError::Empty => Self::internal("empty"),
337 ParseError::InvalidSegment => Self::internal("invalid path segment"),
338 ParseError::NoLeadingSlash => Self::internal("missing leading slash"),
339 }
340 }
341}
342
343impl TryFrom<serde_json5::Error> for Location {
344 type Error = &'static str;
345 fn try_from(e: serde_json5::Error) -> Result<Self, Self::Error> {
346 match e {
347 serde_json5::Error::Message { location: Some(l), .. } => {
348 Ok(Location { line: l.line, column: l.column })
349 }
350 _ => Err("location unavailable"),
351 }
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use anyhow::format_err;
359 use assert_matches::assert_matches;
360 use cm_types as cm;
361
362 #[test]
363 fn test_syntax_error_message() {
364 let result = serde_json::from_str::<cm::Name>("foo").map_err(Error::from);
365 assert_matches!(result, Err(Error::Parse { .. }));
366 }
367
368 #[test]
369 fn test_validation_error_message() {
370 let result = serde_json::from_str::<cm::Name>("\"foo$\"").map_err(Error::from);
371 assert_matches!(result, Err(Error::Validate { .. }));
372 }
373
374 #[test]
375 fn test_parse_error() {
376 let result = Error::parse(format_err!("oops"), None, None);
377 assert_eq!(format!("{}", result), "oops");
378
379 let result = Error::parse(format_err!("oops"), Some(Location { line: 2, column: 3 }), None);
380 assert_eq!(format!("{}", result), "Error at 2:3: oops");
381
382 let result = Error::parse(
383 format_err!("oops"),
384 Some(Location { line: 2, column: 3 }),
385 Some(&Path::new("test.cml")),
386 );
387 assert_eq!(format!("{}", result), "Error at test.cml:2:3: oops");
388
389 let result = Error::parse(
390 format_err!(" --> pest error"),
391 Some(Location { line: 42, column: 42 }),
392 Some(&Path::new("test.cml")),
393 );
394 assert_eq!(format!("{}", result), "Error at test.cml: --> pest error");
395 }
396
397 #[test]
398 fn test_validation_error() {
399 let mut result = Error::validate(format_err!("oops"));
400 assert_eq!(format!("{}", result), "oops");
401
402 if let Error::Validate { filename, .. } = &mut result {
403 *filename = Some("test.cml".to_string());
404 }
405 assert_eq!(format!("{}", result), "Error at test.cml: oops");
406 }
407
408 #[test]
409 fn test_format_multiple_fidl_validator_errors() {
410 use cm_fidl_validator::error::{AvailabilityList, Error as CmFidlError};
411
412 let error = Error::FidlValidator {
413 errs: ErrorList {
414 errs: vec![CmFidlError::DifferentAvailabilityInAggregation(AvailabilityList(
415 vec![fdecl::Availability::Required, fdecl::Availability::Optional],
416 ))],
417 },
418 };
419 assert_eq!(
420 format!("{error}"),
421 "All sources that feed into an aggregation operation should \
422 have the same availability. Got [ \"required\", \"optional\" ]."
423 );
424 }
425}