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 #[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 #[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 #[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 #[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 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>;