Skip to main content

cgx_core/
error.rs

1use std::path::PathBuf;
2
3pub use reqwest::StatusCode;
4use snafu::prelude::*;
5
6use crate::config::BinaryProvider;
7
8#[derive(Debug, Snafu)]
9#[snafu(visibility(pub))]
10#[non_exhaustive]
11pub enum Error {
12    #[snafu(display("Crate name is required"))]
13    MissingCrateParameter,
14
15    #[snafu(display("Missing crate name in crate spec '{spec}'"))]
16    MissingCrateName { spec: String },
17
18    #[snafu(display("Repository format must be 'owner/repo', got '{repo}'"))]
19    InvalidRepoFormat { repo: String },
20
21    #[snafu(display(
22        "Git selectors (--branch, --tag, --rev) can only be used with git sources (--git, --github, \
23         --gitlab)"
24    ))]
25    GitSelectorWithoutGitSource,
26
27    #[snafu(display("Invalid version requirement '{version}': {source}"))]
28    InvalidVersionReq { version: String, source: semver::Error },
29
30    #[snafu(display("Invalid URL '{url}': {source}"))]
31    InvalidUrl { url: String, source: url::ParseError },
32
33    #[snafu(display(
34        "Crate versions in the crate name ({at_version}) and the --crate-version flag ({flag_version}) are \
35         mutually exclusive; specify one or the other but not both"
36    ))]
37    ConflictingVersions {
38        at_version: String,
39        flag_version: String,
40    },
41
42    #[snafu(display(
43        "cgx cannot run cargo itself, and pinning a cargo version is not supported. To run a cargo \
44         subcommand through cgx, use `cgx cargo <subcommand>` (e.g. `cgx cargo deny`) or the plugin crate \
45         name directly (e.g. `cgx cargo-deny`)"
46    ))]
47    CargoNotRunnable,
48
49    // Resolution errors
50    #[snafu(display("Crate '{name}' not found in registry"))]
51    CrateNotFoundInRegistry { name: String },
52
53    #[snafu(display("No version of crate '{name}' matches requirement '{requirement}'"))]
54    NoMatchingVersion { name: String, requirement: String },
55
56    #[snafu(display(
57        "Package '{}' not found in workspace. Available packages: {}",
58        name,
59        available.join(", ")
60    ))]
61    PackageNotFoundInWorkspace { name: String, available: Vec<String> },
62
63    #[snafu(display(
64        "Ambiguous package name: found {count} packages in workspace, but no name was specified. Specify \
65         which package to use with the 'name' field."
66    ))]
67    AmbiguousPackageName { count: usize },
68
69    #[snafu(display("The crate '{krate}' does not have any binary targets so it cannot be executed"))]
70    NoPackageBinaries { krate: String },
71
72    #[snafu(display(
73        "Package '{}' has multiple binary targets [{}], but no default was specified. Use --bin to \
74         specify which binary to build, or set 'default-run' in Cargo.toml",
75        package,
76        available.join(", ")
77    ))]
78    AmbiguousBinaryTarget { package: String, available: Vec<String> },
79
80    #[snafu(display(
81        "Package '{package}' does not contain a {kind} target named '{target}'. Available {kind} targets: {}",
82        available.join(", ")
83    ))]
84    RunnableTargetNotFound {
85        kind: &'static str,
86        package: String,
87        target: String,
88        available: Vec<String>,
89    },
90
91    #[snafu(display("Version mismatch: required version '{requirement}' but found '{found}'"))]
92    VersionMismatch {
93        requirement: String,
94        found: semver::Version,
95    },
96
97    #[snafu(transparent)]
98    Git {
99        source: Box<dyn std::error::Error + Send + Sync>,
100    },
101
102    #[snafu(display("Failed to query registry: {source}"))]
103    Registry { source: tame_index::Error },
104
105    #[snafu(display("Error invoking `{}` to read metadata from source dir `{}`: {}",
106        cargo_path.display(),
107        source_dir.display(),
108        source
109    ))]
110    CargoMetadata {
111        cargo_path: PathBuf,
112        source_dir: PathBuf,
113        source: cargo_metadata::Error,
114    },
115
116    #[snafu(display("Cargo.toml not found in {}", source_dir.display()))]
117    CargoTomlNotFound { source_dir: PathBuf },
118
119    #[snafu(display("Failed to parse version '{version}': {source}"))]
120    InvalidVersion { version: String, source: semver::Error },
121
122    #[snafu(display("{}: {}", path.display(), source))]
123    Io { path: PathBuf, source: std::io::Error },
124
125    #[snafu(display("Failed to rename {} to {}: {}", src.display(), dst.display(), source))]
126    RenameFile {
127        src: PathBuf,
128        dst: PathBuf,
129        source: std::io::Error,
130    },
131
132    #[snafu(display("Failed to copy binary from {} to {}: {}", src.display(), dst.display(), source))]
133    CopyBinary {
134        src: PathBuf,
135        dst: PathBuf,
136        source: std::io::Error,
137    },
138
139    #[snafu(display("Failed to create temporary directory in {}: {}", parent.display(), source))]
140    TempDirInCreation { parent: PathBuf, source: std::io::Error },
141
142    #[snafu(display("Failed to execute command: {}", source))]
143    CommandExecution { source: std::io::Error },
144
145    #[snafu(display("Failed to build SBOM component: {}", message))]
146    SbomBuilder { message: String },
147
148    #[snafu(display("JSON serialization error: {source}"))]
149    Json { source: serde_json::Error },
150
151    #[snafu(display("TOML serialization error: {source}"))]
152    TomlSerialize { source: toml::ser::Error },
153
154    #[snafu(display("Cannot download '{name}' v{version}: network required but offline mode enabled"))]
155    OfflineMode { name: String, version: String },
156
157    #[snafu(display("Failed to download registry crate: {source}"))]
158    RegistryDownload { source: reqwest::Error },
159
160    #[snafu(display("Failed to extract crate tarball: {source}"))]
161    TarExtraction { source: std::io::Error },
162
163    #[snafu(display("Download URL not available for crate '{name}' version '{version}'"))]
164    DownloadUrlUnavailable { name: String, version: String },
165
166    #[snafu(display("Executable '{name}' not found in PATH or standard locations"))]
167    ExecutableNotFound { name: String },
168
169    #[snafu(display("Toolchain '{toolchain}' specified but rustup not found"))]
170    RustupNotFound { toolchain: String },
171
172    #[snafu(display("Expected binary not found in cargo build output"))]
173    BinaryNotFoundInOutput,
174
175    #[snafu(display(
176        "cargo build failed with exit code {}",
177        exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".to_string())
178    ))]
179    CargoBuildFailed { exit_code: Option<i32> },
180
181    #[snafu(display("Failed to copy source tree from {} to {}: {}", src.display(), dst.display(), source))]
182    CopySourceTree {
183        src: PathBuf,
184        dst: PathBuf,
185        source: Box<dyn std::error::Error + Send + Sync + 'static>,
186    },
187
188    // Configuration loading errors
189    #[snafu(display("Failed to load configuration from {}: {}", path.display(), source))]
190    ConfigLoad { path: PathBuf, source: figment::Error },
191
192    #[snafu(display("Invalid configuration value for '{}': {}", field, message))]
193    InvalidConfigValue { field: String, message: String },
194
195    #[snafu(display("Failed to extract configuration: {}", source))]
196    ConfigExtract { source: figment::Error },
197
198    // Binary execution errors
199    #[snafu(display("Failed to execute binary at {}: {source}", path.display()))]
200    ExecFailed { path: PathBuf, source: std::io::Error },
201
202    #[snafu(display("Failed to spawn process at {}: {source}", path.display()))]
203    SpawnFailed { path: PathBuf, source: std::io::Error },
204
205    #[snafu(display("Failed to wait for child process: {source}"))]
206    WaitFailed { source: std::io::Error },
207
208    #[cfg(windows)]
209    #[snafu(display("Failed to set up Windows console control handler"))]
210    ConsoleHandlerFailed { source: ctrlc::Error },
211
212    #[snafu(display("Error determining home directory"))]
213    Etcetera { source: etcetera::HomeDirError },
214
215    // Prebuilt binary resolution errors
216    #[snafu(display(
217        "No binary providers are configured, but prebuilt binaries are enabled. Either enable at least one \
218         binary provider or set use_prebuilt_binaries to 'never'."
219    ))]
220    NoProvidersConfigured,
221
222    #[snafu(display(
223        "Prebuilt binary required (--prebuilt-binary always) but no prebuilt binary found for crate \
224         '{name}' version '{version}'"
225    ))]
226    PrebuiltBinaryRequired { name: String, version: String },
227
228    #[snafu(display(
229        "Prebuilt binary required (--prebuilt-binary always) but resolution could not be completed for \
230         crate '{name}' version '{version}'"
231    ))]
232    PrebuiltBinaryResolutionFailed {
233        name: String,
234        version: String,
235        source: Box<Error>,
236    },
237
238    #[snafu(display(
239        "Prebuilt binary required (--prebuilt-binary always) but {reason}, which requires building crate \
240         '{name}' version '{version}' from source"
241    ))]
242    PrebuiltBinaryDisqualified {
243        name: String,
244        version: String,
245        reason: String,
246    },
247
248    #[snafu(display(
249        "Checksum verification failed for downloaded binary: expected {expected}, got {actual}"
250    ))]
251    ChecksumMismatch { expected: String, actual: String },
252
253    #[snafu(display("Failed to parse SHA256 checksum contents: {contents}"))]
254    ChecksumUnparsable { contents: String },
255
256    #[snafu(display("Provider {provider:?} failed to prepare asset {url}: {source}"))]
257    ProviderAssetPreparationFailed {
258        provider: BinaryProvider,
259        url: String,
260        source: Box<Error>,
261    },
262
263    #[snafu(display("Unsupported archive format: {format}"))]
264    UnsupportedArchiveFormat { format: String },
265
266    #[snafu(display("GitHub API error: {source}"))]
267    GithubApiError {
268        source: Box<dyn std::error::Error + Send + Sync>,
269    },
270
271    #[snafu(display("Quickinstall API error: {source}"))]
272    QuickinstallApiError {
273        source: Box<dyn std::error::Error + Send + Sync>,
274    },
275
276    #[snafu(display("Failed to download prebuilt binary from {url}: {source}"))]
277    BinaryDownloadFailed { url: String, source: reqwest::Error },
278
279    #[snafu(display("HTTP {} downloading prebuilt binary from {url}: {source}", status.as_u16()))]
280    BinaryDownloadHttpError {
281        url: String,
282        status: StatusCode,
283        source: reqwest::Error,
284    },
285
286    #[snafu(display("Failed to extract binary archive: {source}"))]
287    ArchiveExtractionFailed {
288        source: Box<dyn std::error::Error + Send + Sync>,
289    },
290
291    #[snafu(display("Failed to parse {}: {}", path.display(), source))]
292    CargoTomlParse { path: PathBuf, source: toml::de::Error },
293
294    #[snafu(display("Invalid [package.metadata.binstall]: {source}"))]
295    BinstallMetadataInvalidInCargoToml { source: toml::de::Error },
296
297    #[snafu(display("Invalid binstall template '{template}': {source}"))]
298    BinstallTemplateParse {
299        template: String,
300        source: leon::ParseError,
301    },
302
303    #[snafu(display("Failed to render binstall template '{template}': {source}"))]
304    BinstallTemplateRender {
305        template: String,
306        source: leon::RenderError,
307    },
308
309    #[snafu(display("Failed to build HTTP client: {message}"))]
310    HttpClientBuild { message: String },
311
312    #[snafu(display("HTTP request to {url} failed: {source}"))]
313    HttpRequest { url: String, source: reqwest::Error },
314
315    #[snafu(display("Failed to stream HTTP response from {url} to {}: {source}", path.display()))]
316    HttpDownloadToFile {
317        url: String,
318        path: PathBuf,
319        source: std::io::Error,
320    },
321
322    #[snafu(display("HTTP response from {url} exceeded maximum size of {limit} bytes"))]
323    HttpResponseTooLarge { url: String, limit: usize },
324
325    #[snafu(display("Failed to read HTTP response body from {url}: {source}"))]
326    HttpResponseRead { url: String, source: std::io::Error },
327
328    #[snafu(display("HTTP response from {url} is not valid UTF-8: {source}"))]
329    HttpResponseUtf8 {
330        url: String,
331        source: std::string::FromUtf8Error,
332    },
333
334    #[snafu(display("HTTP {status} from {url}"))]
335    HttpStatus { url: String, status: u16 },
336
337    #[snafu(display("Provider request to {url} was throttled: HTTP {status}"))]
338    ProviderThrottled { url: String, status: u16 },
339
340    #[snafu(display(
341        "Failed to prefetch {} configured tool(s): {}",
342        failures.len(),
343        failures.join("; ")
344    ))]
345    PrefetchAllFailed { failures: Vec<String> },
346
347    #[snafu(display("Invalid HTTP timeout duration '{value}': {source}"))]
348    InvalidHttpTimeout {
349        value: String,
350        source: humantime::DurationError,
351    },
352}
353
354impl Error {
355    /// Check whether an HTTP operation error should be retried by [`crate::http::HttpClient`].
356    pub(crate) fn is_retryable_http_error(&self) -> bool {
357        match self {
358            Self::HttpStatus { .. } => true,
359            Self::HttpRequest { source, .. } => {
360                source.is_connect() || source.is_timeout() || source.is_request()
361            }
362            _ => false,
363        }
364    }
365}
366
367impl From<crate::git::Error> for Error {
368    fn from(e: crate::git::Error) -> Self {
369        Self::Git {
370            source: Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
371        }
372    }
373}
374
375pub type Result<T> = std::result::Result<T, Error>;