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