1use crate::ISSUE_STATUS_VALUES_NEXT_STEP;
4use std::borrow::Cow;
5
6#[derive(Debug, thiserror::Error, PartialEq, Eq)]
8pub enum CliError {
9 #[error("unknown or missing command")]
11 UnknownCommand,
12 #[error("unknown command: {command}")]
14 UnknownCommandName {
15 command: String,
17 next: &'static str,
19 },
20 #[error("missing argument: {argument}")]
22 MissingArgument {
23 argument: &'static str,
25 next: &'static str,
27 },
28 #[error("missing value for {flag}")]
30 MissingFlagValue {
31 flag: &'static str,
33 next: &'static str,
35 },
36 #[error("duplicate flag: {flag}")]
38 DuplicateFlag {
39 flag: &'static str,
41 next: &'static str,
43 },
44 #[error("unexpected argument for {command}: {argument}")]
46 UnexpectedArgument {
47 argument: String,
49 command: &'static str,
51 next: &'static str,
53 },
54 #[error("unknown flag: {flag}")]
56 UnknownFlag {
57 flag: String,
59 next: &'static str,
61 },
62 #[error("unsupported flag for {command}: {flag}")]
64 UnsupportedFlag {
65 flag: String,
67 command: &'static str,
69 next: &'static str,
71 },
72 #[error("unknown resource: {resource}")]
74 UnknownResource {
75 resource: String,
77 next: &'static str,
79 },
80 #[error("unknown issue status: {0}")]
82 UnknownStatus(String),
83 #[error("unknown trace status: {0}")]
85 UnknownTraceStatus(String),
86 #[error("unknown log level: {0}")]
88 UnknownLogLevel(String),
89 #[error("invalid limit: {0}")]
91 InvalidLimit(String),
92 #[error("invalid minimum duration: {0}")]
94 InvalidMinDuration(String),
95 #[error("unknown pagination mode")]
97 UnknownPagination,
98 #[error("invalid action cursor: {0}")]
100 InvalidActionCursor(String),
101 #[error("invalid log cursor: {0}")]
103 InvalidLogCursor(String),
104 #[error("invalid issue cursor: {0}")]
106 InvalidIssueCursor(String),
107 #[error("invalid support cursor: {0}")]
109 InvalidSupportCursor(String),
110 #[error("unknown support category")]
112 UnknownSupportCategory,
113 #[error("invalid support ticket id")]
115 InvalidSupportTicketId,
116 #[error("invalid support retry key")]
118 InvalidSupportRetryKey,
119 #[error("invalid support context reply")]
121 InvalidSupportContextReply,
122 #[error("invalid support context command")]
124 InvalidSupportContextCommand,
125 #[error("invalid support context")]
127 InvalidSupportContext,
128 #[error("invalid issue investigation command")]
130 InvalidInvestigationCommand,
131 #[error("invalid project doctor command")]
133 InvalidDoctorCommand,
134 #[error("invalid project create command")]
136 InvalidProjectCreateCommand,
137 #[error("invalid projects command")]
139 InvalidProjectsCommand,
140 #[error("invalid usage command")]
142 InvalidUsageCommand,
143 #[error("invalid native debug-artifact command")]
145 InvalidNativeDebugCommand,
146 #[error("invalid native debug-artifact identity")]
148 InvalidNativeDebugIdentity,
149 #[error("invalid setup source: {0}")]
151 InvalidSetupSource(String),
152}
153
154#[derive(Debug, thiserror::Error)]
156pub enum RuntimeError {
157 #[error(transparent)]
159 Cli(#[from] CliError),
160 #[error(transparent)]
162 Io(#[from] std::io::Error),
163 #[error(transparent)]
165 Http(#[from] reqwest::Error),
166 #[error("not logged in: run logbrew login")]
168 MissingToken,
169 #[error("api returned status {status}: {body}")]
171 Api {
172 status: u16,
174 body: String,
176 auth_source: &'static str,
178 auth_label: &'static str,
180 },
181 #[error("LogBrew API unreachable: {message}")]
183 StatusUnavailable {
184 api_url: String,
186 status_code: Option<u16>,
188 body: Option<String>,
190 authenticated: bool,
192 auth_source: &'static str,
194 auth_label: &'static str,
196 message: String,
198 },
199 #[error("{message}")]
201 Unavailable {
202 message: &'static str,
204 next: &'static str,
206 },
207 #[error("issue investigation returned an invalid response")]
209 InvestigationResponseInvalid,
210 #[error("native debug artifact is invalid")]
212 NativeDebugArtifactInvalid,
213 #[error("native debug-artifact response is invalid")]
215 NativeDebugResponseInvalid,
216 #[error("native debug-artifact verification failed")]
218 NativeDebugVerificationFailed,
219}
220
221pub fn write_cli_error<W: std::io::Write>(
227 error: &CliError,
228 json: bool,
229 output: &mut W,
230) -> Result<(), std::io::Error> {
231 if json {
232 let body = serde_json::json!({
233 "ok": false,
234 "error": cli_error_code(error),
235 "message": error.to_string(),
236 "next": cli_error_next_step(error),
237 });
238 writeln!(output, "{body}")
239 } else {
240 writeln!(output, "{error}")?;
241 writeln!(output, "Next: {}", cli_error_next_step(error))
242 }
243}
244
245pub fn write_runtime_error<W: std::io::Write>(
251 error: &RuntimeError,
252 json: bool,
253 output: &mut W,
254) -> Result<(), std::io::Error> {
255 if json {
256 let body = runtime_error_json(error);
257 writeln!(output, "{body}")
258 } else {
259 write_human_runtime_error(error, output)
260 }
261}
262
263pub fn write_native_debug_runtime_error<W: std::io::Write>(
269 error: &RuntimeError,
270 output: &mut W,
271) -> Result<(), std::io::Error> {
272 let body = match error {
273 RuntimeError::Api { status, body, .. } => {
274 let details = ApiErrorDetails::parse(body);
275 let (fallback_code, fallback_next) = native_debug_api_recovery(*status);
276 let code = details
277 .code
278 .as_deref()
279 .and_then(|value| allowed_native_debug_code(*status, value))
280 .unwrap_or(fallback_code);
281 let next = details
282 .next
283 .as_deref()
284 .and_then(|value| allowed_native_debug_next(*status, value))
285 .unwrap_or(fallback_next);
286 serde_json::json!({
287 "ok": false,
288 "error": code,
289 "status": status,
290 "next": next,
291 })
292 }
293 RuntimeError::Cli(_)
294 | RuntimeError::Io(_)
295 | RuntimeError::Http(_)
296 | RuntimeError::MissingToken
297 | RuntimeError::StatusUnavailable { .. }
298 | RuntimeError::Unavailable { .. }
299 | RuntimeError::InvestigationResponseInvalid
300 | RuntimeError::NativeDebugArtifactInvalid
301 | RuntimeError::NativeDebugResponseInvalid
302 | RuntimeError::NativeDebugVerificationFailed => serde_json::json!({
303 "ok": false,
304 "error": runtime_error_code(error),
305 "next": fallback_runtime_error_next_step(error),
306 }),
307 };
308 writeln!(output, "{body}")
309}
310
311const fn native_debug_api_recovery(status: u16) -> (&'static str, &'static str) {
313 match status {
314 400 => (
315 "validation_failed",
316 "check the artifact identity and request scope, then retry",
317 ),
318 401 | 403 => (
319 "unauthorized",
320 "sign in and retry the native debug-artifact command",
321 ),
322 404 => (
323 "not_found",
324 "check the exact project, release, environment, service, UUID, and architecture",
325 ),
326 405 => (
327 "method_not_allowed",
328 "use the supported native debug-artifact request method",
329 ),
330 408 => (
331 "request_timeout",
332 "retry the same native debug-artifact request",
333 ),
334 413 => (
335 "payload_too_large",
336 "reduce the native debug-artifact upload below the documented size limits and retry",
337 ),
338 422 => (
339 "validation_failed",
340 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
341 ),
342 429 => (
343 "rate_limited",
344 "retry the same native debug-artifact command later",
345 ),
346 500..=599 => (
347 "server_error",
348 "retry the same native debug-artifact command later",
349 ),
350 _ => (
351 "unexpected_response",
352 "retry the native debug-artifact command",
353 ),
354 }
355}
356
357fn allowed_native_debug_code(status: u16, value: &str) -> Option<&'static str> {
359 match (status, value) {
360 (400 | 422, "validation_failed") => Some("validation_failed"),
361 (401 | 403, "unauthorized") => Some("unauthorized"),
362 (404, "not_found") => Some("not_found"),
363 (405, "method_not_allowed") => Some("method_not_allowed"),
364 (408, "request_timeout") => Some("request_timeout"),
365 (413, "payload_too_large") => Some("payload_too_large"),
366 (429, "rate_limited") => Some("rate_limited"),
367 (500..=599, "server_error") => Some("server_error"),
368 _ => None,
369 }
370}
371
372fn allowed_native_debug_next(status: u16, value: &str) -> Option<&'static str> {
374 match (status, value) {
375 (400, "check the artifact identity and request scope, then retry") => {
376 Some("check the artifact identity and request scope, then retry")
377 }
378 (401 | 403, "sign in and retry the native debug-artifact command") => {
379 Some("sign in and retry the native debug-artifact command")
380 }
381 (404, "check the exact project, release, environment, service, UUID, and architecture") => {
382 Some("check the exact project, release, environment, service, UUID, and architecture")
383 }
384 (404, "check the exact project and upload scope") => {
385 Some("check the exact project and upload scope")
386 }
387 (404, "start the native debug artifact upload session again with the same manifest") => {
388 Some("start the native debug artifact upload session again with the same manifest")
389 }
390 (405, "use the supported native debug-artifact request method") => {
391 Some("use the supported native debug-artifact request method")
392 }
393 (408, "retry the same native debug-artifact request") => {
394 Some("retry the same native debug-artifact request")
395 }
396 (
397 413,
398 "reduce the native debug-artifact upload below the documented size limits and retry",
399 ) => Some(
400 "reduce the native debug-artifact upload below the documented size limits and retry",
401 ),
402 (
403 422,
404 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
405 ) => Some(
406 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
407 ),
408 (422, "check the native debug-artifact manifest and retry") => {
409 Some("check the native debug-artifact manifest and retry")
410 }
411 (422, "retry only the missing native debug artifact chunk with its exact digest") => {
412 Some("retry only the missing native debug artifact chunk with its exact digest")
413 }
414 (
415 422,
416 "wait briefly, then retry the same upload completion or verify the exact artifact lookup",
417 ) => Some(
418 "wait briefly, then retry the same upload completion or verify the exact artifact lookup",
419 ),
420 (422, "check the native debug-artifact manifest and upload session, then retry") => {
421 Some("check the native debug-artifact manifest and upload session, then retry")
422 }
423 (429 | 500..=599, "retry the same native debug-artifact request later") => {
424 Some("retry the same native debug-artifact request later")
425 }
426 (429 | 500..=599, "retry the same native debug-artifact command later") => {
427 Some("retry the same native debug-artifact command later")
428 }
429 _ => None,
430 }
431}
432
433fn write_human_runtime_error<W: std::io::Write>(
435 error: &RuntimeError,
436 output: &mut W,
437) -> Result<(), std::io::Error> {
438 match error {
439 RuntimeError::StatusUnavailable {
440 api_url,
441 status_code,
442 body,
443 auth_label,
444 message,
445 ..
446 } => {
447 writeln!(output, "LogBrew API unreachable.")?;
448 writeln!(output, "API: {api_url}")?;
449 writeln!(output, "Auth: {auth_label}")?;
450 if let Some(status_code) = status_code {
451 writeln!(output, "Status: {status_code}")?;
452 }
453 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
454 writeln!(output, "Body: {body}")?;
455 } else {
456 writeln!(output, "Reason: {message}")?;
457 }
458 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
459 Ok(())
460 }
461 RuntimeError::Api {
462 status,
463 body,
464 auth_label,
465 ..
466 } => {
467 let api_details = ApiErrorDetails::parse(body);
468 writeln!(output, "{error}")?;
469 if let Some(code) = api_details.code.as_deref() {
470 writeln!(output, "Code: {code}")?;
471 }
472 writeln!(output, "Auth: {auth_label}")?;
473 writeln!(output, "Next: {}", api_next_step(*status, &api_details))
474 }
475 RuntimeError::Cli(_)
476 | RuntimeError::Io(_)
477 | RuntimeError::Http(_)
478 | RuntimeError::MissingToken
479 | RuntimeError::InvestigationResponseInvalid
480 | RuntimeError::NativeDebugArtifactInvalid
481 | RuntimeError::NativeDebugResponseInvalid
482 | RuntimeError::NativeDebugVerificationFailed
483 | RuntimeError::Unavailable { .. } => {
484 writeln!(output, "{error}")?;
485 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
486 Ok(())
487 }
488 }
489}
490
491fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
493 match error {
494 RuntimeError::MissingToken
495 | RuntimeError::Unavailable { .. }
496 | RuntimeError::InvestigationResponseInvalid
497 | RuntimeError::NativeDebugArtifactInvalid
498 | RuntimeError::NativeDebugResponseInvalid
499 | RuntimeError::NativeDebugVerificationFailed
500 | RuntimeError::Io(_)
501 | RuntimeError::Http(_) => serde_json::json!({
502 "ok": false,
503 "error": runtime_error_code(error),
504 "message": error.to_string(),
505 "next": runtime_error_next_step(error),
506 }),
507 RuntimeError::Api {
508 status,
509 body,
510 auth_source,
511 ..
512 } => {
513 let api_details = ApiErrorDetails::parse(body);
514 let next = api_next_step(*status, &api_details);
515 serde_json::json!({
516 "ok": false,
517 "error": runtime_error_code(error),
518 "message": error.to_string(),
519 "status": status,
520 "body": body,
521 "api_error": api_details.error.as_deref(),
522 "api_code": api_details.code.as_deref(),
523 "api_next": api_details.next.as_deref(),
524 "auth_source": auth_source,
525 "next": next,
526 })
527 }
528 RuntimeError::StatusUnavailable {
529 api_url,
530 status_code,
531 body,
532 authenticated,
533 auth_source,
534 message,
535 ..
536 } => serde_json::json!({
537 "ok": false,
538 "error": runtime_error_code(error),
539 "status": "unreachable",
540 "status_code": status_code,
541 "body": body,
542 "api_url": api_url,
543 "authenticated": authenticated,
544 "auth_source": auth_source,
545 "message": message,
546 "next": runtime_error_next_step(error),
547 }),
548 RuntimeError::Cli(error) => serde_json::json!({
549 "ok": false,
550 "error": cli_error_code(error),
551 "message": error.to_string(),
552 "next": cli_error_next_step(error),
553 }),
554 }
555}
556
557const fn cli_error_code(error: &CliError) -> &'static str {
559 match error {
560 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
561 CliError::MissingArgument { .. } => "missing_argument",
562 CliError::MissingFlagValue { .. } => "missing_flag_value",
563 CliError::DuplicateFlag { .. } => "duplicate_flag",
564 CliError::UnexpectedArgument { .. } => "unexpected_argument",
565 CliError::UnknownFlag { .. } => "unknown_flag",
566 CliError::UnsupportedFlag { .. } => "unsupported_flag",
567 CliError::UnknownResource { .. } => "unknown_resource",
568 CliError::UnknownStatus(_) => "unknown_status",
569 CliError::UnknownTraceStatus(_) => "unknown_trace_status",
570 CliError::UnknownLogLevel(_) => "unknown_log_level",
571 CliError::InvalidLimit(_) => "invalid_limit",
572 CliError::InvalidMinDuration(_) => "invalid_min_duration",
573 CliError::UnknownPagination => "unknown_pagination",
574 CliError::InvalidActionCursor(_) => "invalid_action_cursor",
575 CliError::InvalidLogCursor(_) => "invalid_log_cursor",
576 CliError::InvalidIssueCursor(_) => "invalid_issue_cursor",
577 CliError::InvalidSupportCursor(_) => "invalid_support_cursor",
578 CliError::UnknownSupportCategory => "unknown_support_category",
579 CliError::InvalidSupportTicketId => "invalid_support_ticket_id",
580 CliError::InvalidSupportRetryKey => "invalid_support_retry_key",
581 CliError::InvalidSupportContextReply => "invalid_support_context_reply",
582 CliError::InvalidSupportContextCommand => "invalid_support_context_command",
583 CliError::InvalidSupportContext => "invalid_support_context",
584 CliError::InvalidInvestigationCommand => "invalid_investigation_command",
585 CliError::InvalidDoctorCommand => "invalid_doctor_command",
586 CliError::InvalidProjectCreateCommand => "invalid_project_create_command",
587 CliError::InvalidProjectsCommand => "invalid_projects_command",
588 CliError::InvalidUsageCommand => "invalid_usage_command",
589 CliError::InvalidNativeDebugCommand | CliError::InvalidNativeDebugIdentity => {
590 "invalid_native_debug_command"
591 }
592 CliError::InvalidSetupSource(_) => "invalid_setup_source",
593 }
594}
595
596const fn cli_error_next_step(error: &CliError) -> &'static str {
598 match error {
599 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
600 CliError::InvalidMinDuration(_) => "use --min-duration-ms with a non-negative whole number",
601 CliError::UnknownPagination
602 | CliError::InvalidActionCursor(_)
603 | CliError::InvalidLogCursor(_)
604 | CliError::InvalidIssueCursor(_)
605 | CliError::InvalidSupportCursor(_) => {
606 "use --pagination cursor alone for the first page, then use --cursor-time and --cursor-id together from next_cursor"
607 }
608 CliError::UnknownSupportCategory => {
609 "use sdk_install_failure, ingest_failure, auth_failure, project_setup, dashboard_issue, docs_confusion, cli_issue, mobile_issue, billing_question, or other"
610 }
611 CliError::InvalidSupportTicketId => {
612 "use the ticket_id returned by logbrew support create or list"
613 }
614 CliError::InvalidSupportRetryKey => {
615 "use --retry-key with 1 to 128 visible ASCII characters and reuse it only for an exact retry"
616 }
617 CliError::InvalidSupportContextReply => {
618 "use support reply <ticket_id> --context <text> --retry-key <key>"
619 }
620 CliError::InvalidSupportContextCommand => {
621 "use support context <ticket_id> with optional --json"
622 }
623 CliError::InvalidSupportContext => {
624 "use --context with 1 to 4000 characters after trimming whitespace"
625 }
626 CliError::InvalidInvestigationCommand => {
627 "use logbrew investigate issue <issue_id> with optional --json"
628 }
629 CliError::InvalidDoctorCommand => {
630 "use logbrew doctor --project <project_id> with optional --json"
631 }
632 CliError::InvalidProjectCreateCommand => {
633 "use logbrew projects create <name> --ingest-key-file <path> with optional --runtime, --environment, --abandon-retry, and --json"
634 }
635 CliError::InvalidProjectsCommand => {
636 "use logbrew projects with optional --json, or logbrew projects --help"
637 }
638 CliError::InvalidUsageCommand => "use logbrew usage with optional --json",
639 CliError::InvalidNativeDebugCommand => {
640 "use logbrew debug-artifacts upload <path> --project <project_id> --release <release> --environment <environment> --service <service> with optional --expect-image-uuid, --dry-run, and --json"
641 }
642 CliError::InvalidNativeDebugIdentity => {
643 "use a UUID in 8-4-4-4-12 form and architecture arm64, arm64e, or x86_64"
644 }
645 CliError::InvalidSetupSource(_) => "use --source api, cli, or sdk",
646 CliError::MissingArgument { next, .. }
647 | CliError::MissingFlagValue { next, .. }
648 | CliError::DuplicateFlag { next, .. }
649 | CliError::UnexpectedArgument { next, .. }
650 | CliError::UnsupportedFlag { next, .. }
651 | CliError::UnknownFlag { next, .. }
652 | CliError::UnknownResource { next, .. }
653 | CliError::UnknownCommandName { next, .. } => next,
654 CliError::UnknownCommand => "run logbrew --help",
655 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
656 CliError::UnknownTraceStatus(_) => "use --status error or --status ok",
657 CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
658 }
659}
660
661const fn runtime_error_code(error: &RuntimeError) -> &'static str {
663 match error {
664 RuntimeError::Cli(error) => cli_error_code(error),
665 RuntimeError::Io(_) => "io_error",
666 RuntimeError::Http(_) => "http_error",
667 RuntimeError::MissingToken => "not_logged_in",
668 RuntimeError::Api { .. } => "api_error",
669 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
670 RuntimeError::Unavailable { .. } => "unavailable",
671 RuntimeError::InvestigationResponseInvalid => "investigation_response_invalid",
672 RuntimeError::NativeDebugArtifactInvalid => "native_debug_artifact_invalid",
673 RuntimeError::NativeDebugResponseInvalid => "native_debug_response_invalid",
674 RuntimeError::NativeDebugVerificationFailed => "native_debug_verification_failed",
675 }
676}
677
678const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
680
681#[derive(Debug, Clone, Default, PartialEq, Eq)]
683struct ApiErrorDetails {
684 error: Option<String>,
686 code: Option<String>,
688 next: Option<String>,
690}
691
692impl ApiErrorDetails {
693 fn parse(body: &str) -> Self {
695 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
696 return Self::default();
697 };
698
699 Self {
700 error: json_string_field(&value, "error"),
701 code: json_string_field(&value, "code"),
702 next: json_string_field(&value, "next"),
703 }
704 }
705}
706
707fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
709 value
710 .get(key)
711 .and_then(serde_json::Value::as_str)
712 .map(str::trim)
713 .filter(|value| !value.is_empty())
714 .map(ToOwned::to_owned)
715}
716
717fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
719 match error {
720 RuntimeError::Api { status, body, .. } => {
721 let api_details = ApiErrorDetails::parse(body);
722 api_next_step(*status, &api_details)
723 }
724 RuntimeError::Cli(_)
725 | RuntimeError::Io(_)
726 | RuntimeError::Http(_)
727 | RuntimeError::MissingToken
728 | RuntimeError::InvestigationResponseInvalid
729 | RuntimeError::NativeDebugArtifactInvalid
730 | RuntimeError::NativeDebugResponseInvalid
731 | RuntimeError::NativeDebugVerificationFailed
732 | RuntimeError::StatusUnavailable { .. }
733 | RuntimeError::Unavailable { .. } => {
734 Cow::Borrowed(fallback_runtime_error_next_step(error))
735 }
736 }
737}
738
739fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
741 api_details.next.as_ref().map_or_else(
742 || Cow::Borrowed(fallback_api_next_step(status)),
743 |next| Cow::Owned(next.clone()),
744 )
745}
746
747const fn fallback_api_next_step(status: u16) -> &'static str {
749 match status {
750 401 | 403 => "run logbrew login",
751 400 | 422 => "check command arguments or filters",
752 404 => "check the resource id or filters",
753 429 => "retry later",
754 500..=599 => "check LOGBREW_API_URL or retry later",
755 _ => "check command arguments or retry later",
756 }
757}
758
759const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
761 match error {
762 RuntimeError::Cli(error) => cli_error_next_step(error),
763 RuntimeError::MissingToken => "run logbrew login",
764 RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
765 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
766 STATUS_UNAVAILABLE_NEXT_STEP
767 }
768 RuntimeError::Unavailable { next, .. } => next,
769 RuntimeError::InvestigationResponseInvalid => {
770 "retry the issue investigation; if it repeats, report the public response contract"
771 }
772 RuntimeError::NativeDebugArtifactInvalid => {
773 "provide one validated Apple dSYM, ZIP, or Mach-O object matching every --expect-image-uuid value"
774 }
775 RuntimeError::NativeDebugResponseInvalid => {
776 "retry the native debug-artifact request; if it repeats, report the public response contract"
777 }
778 RuntimeError::NativeDebugVerificationFailed => {
779 "retry exact native debug-artifact lookup before using native symbolication"
780 }
781 RuntimeError::Io(_) => "check local files and permissions",
782 }
783}