Skip to main content

loonfs_core/commit/
validate_error.rs

1//! [`CommitValidationError`]: every way a commit request can fail
2//! validation.
3
4use loonfs_api::{ChangeSeq, InodeId, InodeKind, NameKey, RevisionNo, WriterEpoch};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
9pub enum CommitValidationError {
10    #[error("commit contains no operations")]
11    EmptyCommit,
12    #[error("commit namespace does not match the namespace head")]
13    NamespaceMismatch,
14    #[error("name precondition parent inode `{parent_inode_id}` is missing")]
15    NamePreconditionParentMissing { parent_inode_id: InodeId },
16    #[error(
17        "name precondition parent inode `{parent_inode_id}` is not a directory (found `{actual_kind}`)"
18    )]
19    NamePreconditionParentNotDirectory {
20        parent_inode_id: InodeId,
21        actual_kind: InodeKind,
22    },
23    #[error(
24        "binding precondition failed: name `{name_key}` is not bound under parent inode `{parent_inode_id}`"
25    )]
26    BindingPreconditionMissing {
27        parent_inode_id: InodeId,
28        name_key: NameKey,
29    },
30    #[error(
31        "binding precondition failed: name `{name_key}` under parent inode `{parent_inode_id}` expected child inode `{expected_child_inode_id}` but found `{actual_child_inode_id}`"
32    )]
33    BindingPreconditionMismatch {
34        parent_inode_id: InodeId,
35        name_key: NameKey,
36        expected_child_inode_id: InodeId,
37        actual_child_inode_id: InodeId,
38    },
39    #[error("directory-empty precondition inode `{inode_id}` is missing")]
40    DirectoryEmptyPreconditionInodeMissing { inode_id: InodeId },
41    #[error(
42        "directory-empty precondition inode `{inode_id}` is not a directory (found `{actual_kind}`)"
43    )]
44    DirectoryEmptyPreconditionInodeNotDirectory {
45        inode_id: InodeId,
46        actual_kind: InodeKind,
47    },
48    #[error("directory-empty precondition failed: directory inode `{inode_id}` is not empty")]
49    DirectoryEmptyPreconditionNotEmpty { inode_id: InodeId },
50    #[error("create parent inode `{parent_inode_id}` is missing")]
51    CreateParentMissing { parent_inode_id: InodeId },
52    #[error("create parent inode `{parent_inode_id}` is not a directory (found `{actual_kind}`)")]
53    CreateParentNotDirectory {
54        parent_inode_id: InodeId,
55        actual_kind: InodeKind,
56    },
57    #[error(
58        "create collides with existing name `{name_key}` under parent inode `{parent_inode_id}` (bound to inode `{child_inode_id}`)"
59    )]
60    CreateChildNameCollision {
61        parent_inode_id: InodeId,
62        name_key: NameKey,
63        child_inode_id: InodeId,
64    },
65    #[error("invalid display name: {reason}")]
66    InvalidDisplayName {
67        display_name: String,
68        reason: String,
69    },
70    #[error(
71        "create under parent inode `{parent_inode_id}` conflicts with subtree tombstone rooted at inode `{root_inode_id}` from seq `{tombstone_seq}`"
72    )]
73    CreateUnderSubtreeTombstone {
74        parent_inode_id: InodeId,
75        root_inode_id: InodeId,
76        tombstone_seq: ChangeSeq,
77    },
78    #[error("replace target file inode `{inode_id}` is missing")]
79    ReplaceFileInodeMissing { inode_id: InodeId },
80    #[error("replace target inode `{inode_id}` is not a file (found `{actual_kind}`)")]
81    ReplaceFileInodeNotFile {
82        inode_id: InodeId,
83        actual_kind: InodeKind,
84    },
85    #[error(
86        "replace base revision mismatch for inode `{inode_id}`: {}",
87        revision_mismatch(.expected, .actual)
88    )]
89    ReplaceFileBaseRevisionMismatch {
90        inode_id: InodeId,
91        expected: RevisionNo,
92        actual: Option<RevisionNo>,
93    },
94    #[error("restore target file inode `{inode_id}` is missing")]
95    RestoreRevisionInodeMissing { inode_id: InodeId },
96    #[error("restore target inode `{inode_id}` is not a file (found `{actual_kind}`)")]
97    RestoreRevisionInodeNotFile {
98        inode_id: InodeId,
99        actual_kind: InodeKind,
100    },
101    #[error(
102        "restore base revision mismatch for inode `{inode_id}`: {}",
103        revision_mismatch(.expected, .actual)
104    )]
105    RestoreRevisionBaseRevisionMismatch {
106        inode_id: InodeId,
107        expected: RevisionNo,
108        actual: Option<RevisionNo>,
109    },
110    #[error("restore source revision `{source_revision_no}` not found for inode `{inode_id}`")]
111    RestoreRevisionSourceRevisionMissing {
112        inode_id: InodeId,
113        source_revision_no: RevisionNo,
114    },
115    #[error(
116        "restore of inode `{inode_id}` conflicts with subtree tombstone rooted at inode `{root_inode_id}` from seq `{tombstone_seq}`"
117    )]
118    RestoreRevisionUnderSubtreeTombstone {
119        inode_id: InodeId,
120        root_inode_id: InodeId,
121        tombstone_seq: ChangeSeq,
122    },
123    #[error(
124        "replace of inode `{inode_id}` conflicts with subtree tombstone rooted at inode `{root_inode_id}` from seq `{tombstone_seq}`"
125    )]
126    ReplaceFileUnderSubtreeTombstone {
127        inode_id: InodeId,
128        root_inode_id: InodeId,
129        tombstone_seq: ChangeSeq,
130    },
131    #[error("delete target file inode `{inode_id}` is missing")]
132    DeleteFileInodeMissing { inode_id: InodeId },
133    #[error("delete target inode `{inode_id}` is not a file (found `{actual_kind}`)")]
134    DeleteFileInodeNotFile {
135        inode_id: InodeId,
136        actual_kind: InodeKind,
137    },
138    #[error(
139        "delete of file inode `{inode_id}` is already covered by subtree tombstone rooted at inode `{covering_root_inode_id}` from seq `{tombstone_seq}`"
140    )]
141    DeleteFileCoveredByTombstone {
142        inode_id: InodeId,
143        covering_root_inode_id: InodeId,
144        tombstone_seq: ChangeSeq,
145    },
146    #[error("rename source inode `{inode_id}` is missing")]
147    RenameInodeMissing { inode_id: InodeId },
148    #[error("rename source inode `{inode_id}` has no current binding")]
149    RenameSourceBindingMissing { inode_id: InodeId },
150    #[error("source inode `{inode_id}` has no current binding")]
151    SourceBindingMissing { inode_id: InodeId },
152    #[error("rename target parent inode `{parent_inode_id}` is missing")]
153    RenameTargetParentMissing { parent_inode_id: InodeId },
154    #[error(
155        "rename target parent inode `{parent_inode_id}` is not a directory (found `{actual_kind}`)"
156    )]
157    RenameTargetParentNotDirectory {
158        parent_inode_id: InodeId,
159        actual_kind: InodeKind,
160    },
161    #[error(
162        "rename collides with existing name `{name_key}` under parent inode `{parent_inode_id}` (bound to inode `{child_inode_id}`)"
163    )]
164    RenameTargetNameCollision {
165        parent_inode_id: InodeId,
166        name_key: NameKey,
167        child_inode_id: InodeId,
168    },
169    #[error(
170        "rename of directory inode `{inode_id}` into inode `{new_parent_inode_id}` would create a cycle"
171    )]
172    RenameWouldCycleDirectory {
173        inode_id: InodeId,
174        new_parent_inode_id: InodeId,
175    },
176    #[error(
177        "rename of inode `{inode_id}` conflicts with subtree tombstone rooted at inode `{root_inode_id}` from seq `{tombstone_seq}`"
178    )]
179    RenameInodeUnderSubtreeTombstone {
180        inode_id: InodeId,
181        root_inode_id: InodeId,
182        tombstone_seq: ChangeSeq,
183    },
184    #[error(
185        "rename target parent inode `{parent_inode_id}` conflicts with subtree tombstone rooted at inode `{root_inode_id}` from seq `{tombstone_seq}`"
186    )]
187    RenameTargetParentUnderSubtreeTombstone {
188        parent_inode_id: InodeId,
189        root_inode_id: InodeId,
190        tombstone_seq: ChangeSeq,
191    },
192    #[error("delete subtree root inode `{root_inode_id}` is missing")]
193    DeleteSubtreeRootMissing { root_inode_id: InodeId },
194    #[error(
195        "delete subtree root inode `{root_inode_id}` is not a directory (found `{actual_kind}`)"
196    )]
197    DeleteSubtreeRootNotDirectory {
198        root_inode_id: InodeId,
199        actual_kind: InodeKind,
200    },
201    #[error(
202        "delete subtree root inode `{root_inode_id}` is already covered by subtree tombstone rooted at inode `{covering_root_inode_id}` from seq `{tombstone_seq}`"
203    )]
204    DeleteSubtreeRootCoveredByTombstone {
205        root_inode_id: InodeId,
206        covering_root_inode_id: InodeId,
207        tombstone_seq: ChangeSeq,
208    },
209    #[error("undelete target inode `{inode_id}` is missing")]
210    UndeleteInodeMissing { inode_id: InodeId },
211    #[error("undelete target inode `{inode_id}` is not the root of a live deletion")]
212    UndeleteTargetNotDeleted { inode_id: InodeId },
213    #[error(
214        "undelete of inode `{inode_id}` targets a deletion at seq `{requested_seq}`, which is not from an earlier commit"
215    )]
216    UndeleteTargetsCurrentCommit {
217        inode_id: InodeId,
218        requested_seq: ChangeSeq,
219    },
220    #[error(
221        "undelete of inode `{inode_id}` targets the deletion at seq `{requested_seq}`, but the active deletion is at seq `{active_seq}`"
222    )]
223    UndeleteGenerationMismatch {
224        inode_id: InodeId,
225        requested_seq: ChangeSeq,
226        active_seq: ChangeSeq,
227    },
228    #[error(
229        "revision counter overflow restoring inode `{inode_id}` at base revision `{base_revision_no}`"
230    )]
231    RestoreRevisionOverflow {
232        inode_id: InodeId,
233        base_revision_no: RevisionNo,
234    },
235    #[error(
236        "revision counter overflow replacing inode `{inode_id}` at base revision `{base_revision_no}`"
237    )]
238    ReplaceFileRevisionOverflow {
239        inode_id: InodeId,
240        base_revision_no: RevisionNo,
241    },
242    #[error("stale writer epoch: requested `{requested}` but active is `{active}`")]
243    StaleWriterEpoch {
244        active: WriterEpoch,
245        requested: WriterEpoch,
246    },
247    #[error("validated preview apply failed: {0}")]
248    ValidatedPreviewApplyFailed(String),
249    #[error("sequence counter overflow")]
250    SeqOverflow,
251    #[error("next inode id counter overflow")]
252    NextInodeOverflow,
253    #[error("op index overflow")]
254    OpIndexOverflow,
255    #[error("delta index overflow")]
256    DeltaIndexOverflow,
257}
258
259/// What a base-revision guard asked for against what it found, in words.
260///
261/// The revision it found is absent when the file carries no revision at all,
262/// and that case reads as a sentence rather than printing the `Option` — a
263/// message is for a person, while the same pair rides the wire as typed
264/// `expected_revision` and `actual_revision` details for a program.
265fn revision_mismatch(expected: &RevisionNo, actual: &Option<RevisionNo>) -> String {
266    match actual {
267        Some(actual) => format!("expected revision {expected}, found revision {actual}"),
268        None => format!("expected revision {expected}, found no revision"),
269    }
270}