1use 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
259fn 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}