1use std::io;
6use std::path::PathBuf;
7
8use thiserror::Error;
9
10use crate::config::HostConfigError;
11
12pub type Result<T, E = Error> = std::result::Result<T, E>;
13
14#[derive(Debug, Error)]
15pub enum Error {
16 #[error("snapshot error: {0}")]
17 Snapshot(#[from] SnapshotError),
18
19 #[error("store error: {0}")]
20 Store(#[from] StoreError),
21
22 #[error("config error: {0}")]
23 Config(#[from] ConfigError),
24
25 #[error("patch error: {0}")]
26 Patch(#[from] PatchError),
27
28 #[error("name error: {0}")]
29 Name(#[from] NameError),
30
31 #[error("i/o error: {0}")]
32 Io(#[from] io::Error),
33}
34
35#[derive(Debug, Error)]
36pub enum NameError {
37 #[error("empty value where {kind} was expected")]
38 Empty { kind: &'static str },
39
40 #[error("{kind} too long: {len} > {max}")]
41 TooLong {
42 kind: &'static str,
43 len: usize,
44 max: usize,
45 },
46
47 #[error("invalid character {ch:?} in {kind}: {value}")]
48 InvalidCharacter {
49 kind: &'static str,
50 ch: char,
51 value: String,
52 },
53
54 #[error("{kind} {value:?} does not match required pattern {pattern}")]
55 PatternMismatch {
56 kind: &'static str,
57 value: String,
58 pattern: &'static str,
59 },
60
61 #[error("invalid commit sha {value:?}: must be exactly 40 lowercase hex characters")]
62 InvalidCommitSha { value: String },
63
64 #[error("invalid verdict fingerprint {value:?}: must be exactly 32 lowercase hex characters")]
65 InvalidVerdictFingerprint { value: String },
66
67 #[error("commit sha prefix is empty")]
68 EmptyCommitShaPrefix,
69
70 #[error("invalid commit sha prefix {value:?}: too short ({len} chars; needs 7 to 40)")]
71 CommitShaPrefixTooShort { value: String, len: usize },
72
73 #[error("invalid commit sha prefix {value:?}: too long ({len} chars; needs 7 to 40)")]
74 CommitShaPrefixTooLong { value: String, len: usize },
75
76 #[error("invalid commit sha prefix {value:?}: non-hex character {ch:?} at position {position}")]
77 CommitShaPrefixNonHex {
78 value: String,
79 ch: char,
80 position: usize,
81 },
82
83 #[error("invalid arity: {raw:?} could not be parsed as an integer in 0..=255")]
84 InvalidArityParse { raw: String },
85
86 #[error("invalid arity {value}: must be 0..=255")]
87 InvalidArity { value: i64 },
88
89 #[error("invalid mfa {value:?}: expected 'mod:fun/arity'")]
90 InvalidMfa { value: String },
91
92 #[error("invalid {kind} {value:?}: {reason}")]
93 Invalid {
94 kind: &'static str,
95 value: String,
96 reason: &'static str,
97 },
98}
99
100#[derive(Debug, Error)]
101pub enum SnapshotError {
102 #[error("malformed header at line {line}: {detail}")]
103 MalformedHeader { line: usize, detail: String },
104
105 #[error("unknown header key {key:?} at line {line}")]
106 UnknownHeaderKey { line: usize, key: String },
107
108 #[error("missing required header key {key:?}")]
109 MissingHeaderKey { key: &'static str },
110
111 #[error("unknown format-version {found}: expected {expected}")]
112 UnknownFormatVersion { found: String, expected: u32 },
113
114 #[error("unexpected token at line {line}: {detail}")]
115 UnexpectedToken { line: usize, detail: String },
116
117 #[error("non-canonical input at line {line}: {detail}")]
118 NotCanonical { line: usize, detail: String },
119
120 #[error("non-canonical snapshot: {detail}")]
123 NotCanonicalValue { detail: String },
124
125 #[error("snapshot cannot be written: {detail}")]
126 Unwritable { detail: String },
127
128 #[error("invalid utf-8 at byte offset {offset}")]
129 InvalidUtf8 { offset: usize },
130
131 #[error("input exceeds size limit ({size} > {limit} bytes)")]
132 SizeLimit { size: usize, limit: usize },
133
134 #[error("name error: {0}")]
135 Name(#[from] NameError),
136
137 #[error("i/o error: {0}")]
138 Io(#[from] io::Error),
139}
140
141#[derive(Debug, Error)]
142pub enum StoreError {
143 #[error("snapshot store root does not exist: {0}")]
144 RootMissing(PathBuf),
145
146 #[error("snapshot not found: project {project}, tag {tag}")]
147 SnapshotNotFound { project: String, tag: String },
148
149 #[error("snapshot already exists: {0}")]
150 AlreadyExists(PathBuf),
151
152 #[error("path escape: {0:?}")]
153 PathEscape(PathBuf),
154
155 #[error("snapshot error: {0}")]
156 Snapshot(#[from] SnapshotError),
157
158 #[error("i/o error: {0}")]
159 Io(#[from] io::Error),
160}
161
162#[derive(Debug, Error)]
166#[error("unknown {noun} {rejected:?}; expected one of {accepted}")]
167pub struct UnknownLabel {
168 noun: &'static str,
169 rejected: String,
170 accepted: String,
171}
172
173impl UnknownLabel {
174 pub fn new(
175 noun: &'static str,
176 rejected: &str,
177 labels: impl IntoIterator<Item = &'static str>,
178 ) -> Self {
179 Self {
180 noun,
181 rejected: rejected.to_owned(),
182 accepted: labels.into_iter().collect::<Vec<_>>().join(", "),
183 }
184 }
185}
186
187#[derive(Debug, Error)]
188pub enum ConfigError {
189 #[error("config file not found: {0}")]
190 NotFound(PathBuf),
191
192 #[error("malformed toml: {0}")]
193 Toml(#[from] toml::de::Error),
194
195 #[error("unknown config-version {0}")]
196 UnknownConfigVersion(u32),
197
198 #[error("project {0:?} referenced but not configured")]
199 UnknownProject(String),
200
201 #[error("series {0:?} referenced but not configured")]
202 UnknownSeries(String),
203
204 #[error("host {0:?} referenced but not configured")]
205 UnknownHost(String),
206
207 #[error(transparent)]
208 Host(#[from] HostConfigError),
209
210 #[error("series {series:?} pins unknown project {project:?}")]
211 SeriesPinsUnknownProject { series: String, project: String },
212
213 #[error("pin pattern {pattern:?} for project {project:?} matched no snapshot tag")]
214 PinPatternNoMatch { project: String, pattern: String },
215
216 #[error("snapshot for pin {project} @ {tag} is not on disk at {expected_path}")]
217 PinSnapshotMissing {
218 project: String,
219 tag: String,
220 expected_path: String,
221 },
222
223 #[error(
224 "pin pattern {pattern:?} for project {project:?} could not be resolved: \
225 snapshot store error: {detail}"
226 )]
227 PinPatternStore {
228 project: String,
229 pattern: String,
230 detail: String,
231 },
232
233 #[error("pin `select` must be \"latest\" or \"oldest\", got {0:?}")]
234 PinUnknownSelect(String),
235
236 #[error("{0}")]
237 UnknownLabel(#[from] UnknownLabel),
238
239 #[error("project {0:?} has kind=\"self\" but also specifies git_url; remove one")]
240 SelfProjectHasGitUrl(String),
241
242 #[error("project {0:?} requires git_url (external kind, the default)")]
243 ExternalProjectMissingGitUrl(String),
244
245 #[error("config defines more than one project with kind=\"self\": {projects:?}")]
246 MultipleSelfProjects { projects: Vec<String> },
247
248 #[error("self-project pin {project} @ {git_ref} requires `--repo-dir-path` for resolution")]
249 SelfPinNeedsRepoDirPath { project: String, git_ref: String },
250
251 #[error(
252 "series pins {project} as a self-pin (branch/sha form) but {project} is configured as kind=\"external\""
253 )]
254 SelfPinReferencesExternalProject { project: String },
255
256 #[error("project {project:?} has layout {layout:?} but no `app_roots` defaulted or set")]
257 LayoutWithoutAppRoots { project: String, layout: String },
258
259 #[error("suite_rule[{rule_index}]: invalid regex: {detail}")]
260 SuiteRuleRegex { rule_index: usize, detail: String },
261
262 #[error("suite_rule[{rule_index}]: template references unknown capture group {placeholder:?}")]
263 SuiteRuleUnknownPlaceholder {
264 rule_index: usize,
265 placeholder: String,
266 },
267
268 #[error(
269 "suite_rule[{rule_index}]: include_suite_for_dep_modules requires a named capture `dep` from the path or line regex"
270 )]
271 SuiteRuleMissingDepCapture { rule_index: usize },
272
273 #[error("path_translation file not found: {0:?}")]
274 PathTranslationFileMissing(PathBuf),
275
276 #[error("path_translation: name must be non-empty")]
277 PathTranslationEmptyName,
278
279 #[error("path_translation[{name:?}]: {field} must be non-empty")]
280 PathTranslationEmptyPrefix { name: String, field: &'static str },
281
282 #[error("path_translation[{name:?}]: {field} {value:?} must end in '/'")]
283 PathTranslationPrefixNoTrailingSlash {
284 name: String,
285 field: &'static str,
286 value: String,
287 },
288
289 #[error("path_translation[{name:?}]: unknown direction {direction:?}")]
290 PathTranslationUnknownDirection { name: String, direction: String },
291
292 #[error("path_translation: duplicate name {0:?}")]
293 PathTranslationDuplicateName(String),
294
295 #[error("path_translation: duplicate source_prefix {0:?}")]
296 PathTranslationDuplicateSource(String),
297
298 #[error(
299 "path_translation: source_prefix overlap: {a:?} and {b:?} (one is a strict prefix of the other)"
300 )]
301 PathTranslationPrefixOverlap { a: String, b: String },
302
303 #[error("name error: {0}")]
304 Name(#[from] NameError),
305
306 #[error("i/o error: {0}")]
307 Io(#[from] io::Error),
308}
309
310#[derive(Debug, Error)]
311pub enum PatchError {
312 #[error("malformed unified diff at line {line}: {detail}")]
313 Malformed { line: usize, detail: String },
314
315 #[error("input exceeds size limit ({size} > {limit} bytes)")]
316 SizeLimit { size: usize, limit: usize },
317
318 #[error("patch is not valid UTF-8 (first invalid byte at offset {offset})")]
319 InvalidUtf8 { offset: usize },
320
321 #[error("unsupported file type: {0}")]
322 UnsupportedFileType(String),
323
324 #[error("binary patch is not analyzable")]
325 BinaryPatch,
326}