1use {creusot_std::prelude::*, std::cmp::Ordering};
4
5#[krabka_macros::model_type]
7pub enum SnapshotChunkDecision {
8 Restart,
9 Continue { next_position: i64 },
10 Complete,
11}
12
13#[krabka_macros::model_type]
15pub enum SnapshotInstallDecision {
16 Reject,
17 Stale,
18 Install,
19}
20
21#[ensures(match result {
23 SnapshotInstallDecision::Reject => downgrade_pending
24 || snapshot_end@ < 0
25 || snapshot_epoch@ < 0
26 || log_end@ < 0,
27 SnapshotInstallDecision::Stale => !downgrade_pending
28 && snapshot_end@ >= 0
29 && snapshot_epoch@ >= 0
30 && log_end@ >= 0
31 && snapshot_end@ <= log_end@,
32 SnapshotInstallDecision::Install => !downgrade_pending
33 && snapshot_end@ >= 0
34 && snapshot_epoch@ >= 0
35 && log_end@ >= 0
36 && snapshot_end@ > log_end@,
37})]
38#[must_use]
39pub fn snapshot_install_decision(
40 downgrade_pending: bool,
41 snapshot_end: i64,
42 snapshot_epoch: i32,
43 log_end: i64,
44) -> SnapshotInstallDecision {
45 if downgrade_pending || snapshot_end < 0 || snapshot_epoch < 0 || log_end < 0 {
46 SnapshotInstallDecision::Reject
47 } else if snapshot_end <= log_end {
48 SnapshotInstallDecision::Stale
49 } else {
50 SnapshotInstallDecision::Install
51 }
52}
53
54#[ensures(result == (snapshot_end@ >= 0 && snapshot_end@ <= committed_end@))]
56#[must_use]
57pub fn snapshot_prune_admission(snapshot_end: i64, committed_end: i64) -> bool {
58 snapshot_end >= 0 && snapshot_end <= committed_end
59}
60
61#[ensures(match result {
63 SnapshotChunkDecision::Restart => !identity_matches
64 || received@ < 0
65 || chunk_len@ < 0
66 || max_size@ < 0
67 || declared_size@ < 0
68 || declared_size@ > max_size@
69 || position@ != received@
70 || match fixed_size {
71 Some(size) => size@ != declared_size@,
72 None => false,
73 }
74 || received@ + chunk_len@ > declared_size@,
75 SnapshotChunkDecision::Continue { next_position } => snapshot_chunk_shape_valid(identity_matches, fixed_size, received@, declared_size@, position@, chunk_len@, max_size@)
76 && next_position@ == received@ + chunk_len@
77 && next_position@ < declared_size@,
78 SnapshotChunkDecision::Complete => snapshot_chunk_shape_valid(identity_matches, fixed_size, received@, declared_size@, position@, chunk_len@, max_size@)
79 && received@ + chunk_len@ == declared_size@,
80})]
81#[must_use]
82pub fn snapshot_chunk_admission(
83 identity_matches: bool,
84 fixed_size: Option<i64>,
85 received: i64,
86 declared_size: i64,
87 position: i64,
88 chunk_len: i64,
89 max_size: i64,
90) -> SnapshotChunkDecision {
91 if !identity_matches
92 || received < 0
93 || chunk_len < 0
94 || max_size < 0
95 || declared_size < 0
96 || declared_size > max_size
97 || position != received
98 {
99 return SnapshotChunkDecision::Restart;
100 }
101 if let Some(size) = fixed_size
102 && size != declared_size
103 {
104 return SnapshotChunkDecision::Restart;
105 }
106 let Some(next_position) = received.checked_add(chunk_len) else {
107 return SnapshotChunkDecision::Restart;
108 };
109 match next_position.cmp(&declared_size) {
110 Ordering::Greater => SnapshotChunkDecision::Restart,
111 Ordering::Equal => SnapshotChunkDecision::Complete,
112 Ordering::Less => SnapshotChunkDecision::Continue { next_position },
113 }
114}
115
116#[cfg(creusot)]
118#[cfg_attr(test, mutants::skip)]
119#[logic(open)]
120fn snapshot_chunk_shape_valid(
122 identity_matches: bool,
123 fixed_size: Option<i64>,
124 received: Int,
125 declared_size: Int,
126 position: Int,
127 chunk_len: Int,
128 max_size: Int,
129) -> bool {
130 pearlite! { identity_matches && received >= 0 && chunk_len >= 0 && max_size >= 0
131 && declared_size >= 0 && declared_size <= max_size && position == received
132 && match fixed_size { Some(size) => size@ == declared_size, None => true, } }
133}
134
135#[cfg(test)]
136mod tests {
137 use {
138 super::{
139 SnapshotChunkDecision, SnapshotInstallDecision, snapshot_chunk_admission,
140 snapshot_install_decision, snapshot_prune_admission,
141 },
142 assert2::check,
143 };
144
145 #[yare::parameterized(
146 empty_stale = { false, 0, 0, 0, SnapshotInstallDecision::Stale },
147 after_empty = { false, 1, 0, 0, SnapshotInstallDecision::Install },
148 beyond_log_end = { false, 11, 3, 10, SnapshotInstallDecision::Install },
149 at_log_end = { false, 10, 3, 10, SnapshotInstallDecision::Stale },
150 before_log_end = { false, 9, 3, 10, SnapshotInstallDecision::Stale },
151 pending = { true, 11, 3, 10, SnapshotInstallDecision::Reject },
152 negative_end = { false, -1, 3, 10, SnapshotInstallDecision::Reject },
153 negative_epoch = { false, 11, -1, 10, SnapshotInstallDecision::Reject },
154 negative_log_end = { false, 11, 3, -1, SnapshotInstallDecision::Reject },
155 )]
156 fn install_admission_rejects_malformed_and_separates_stale_from_future(
157 pending: bool,
158 end: i64,
159 epoch: i32,
160 log_end: i64,
161 expected: SnapshotInstallDecision,
162 ) {
163 check!(snapshot_install_decision(pending, end, epoch, log_end) == expected);
164 }
165
166 #[test]
167 fn prune_admission_never_crosses_the_committed_frontier() {
168 check!(snapshot_prune_admission(10, 10));
169 check!(snapshot_prune_admission(9, 10));
170 check!(!snapshot_prune_admission(11, 10));
171 check!(!snapshot_prune_admission(-1, 10));
172 }
173
174 #[yare::parameterized(
175 next_chunk = {
176 true,
177 None,
178 0,
179 6,
180 0,
181 3,
182 10,
183 SnapshotChunkDecision::Continue { next_position: 3 },
184 },
185 final_chunk = { true, Some(6), 3, 6, 3, 3, 10, SnapshotChunkDecision::Complete },
186 empty_snapshot = { true, None, 0, 0, 0, 0, 10, SnapshotChunkDecision::Complete },
187 empty_snapshot_at_zero_cap = { true, None, 0, 0, 0, 0, 0, SnapshotChunkDecision::Complete },
188 exact_max_size = { true, None, 0, 10, 0, 10, 10, SnapshotChunkDecision::Complete },
189 identity_changed = { false, Some(6), 3, 6, 3, 3, 10, SnapshotChunkDecision::Restart },
190 fixed_size_changed = { true, Some(7), 3, 6, 3, 3, 10, SnapshotChunkDecision::Restart },
191 position_replayed = { true, Some(6), 3, 6, 0, 3, 10, SnapshotChunkDecision::Restart },
192 position_skipped = { true, Some(6), 3, 6, 2, 3, 10, SnapshotChunkDecision::Restart },
193 chunk_starts_after_position = {
194 true,
195 Some(6),
196 3,
197 6,
198 4,
199 2,
200 10,
201 SnapshotChunkDecision::Restart,
202 },
203 chunk_exceeds_declared_size = {
204 true,
205 Some(6),
206 3,
207 6,
208 3,
209 4,
210 10,
211 SnapshotChunkDecision::Restart,
212 },
213 negative_received = { true, None, -1, 6, -1, 3, 10, SnapshotChunkDecision::Restart },
214 negative_chunk_size = { true, None, 0, 6, 0, -1, 10, SnapshotChunkDecision::Restart },
215 negative_max_size = { true, None, 0, 6, 0, 3, -1, SnapshotChunkDecision::Restart },
216 negative_declared_size = { true, None, 0, -1, 0, 0, 10, SnapshotChunkDecision::Restart },
217 exceeds_max_size = { true, None, 0, 11, 0, 1, 10, SnapshotChunkDecision::Restart },
218 addition_overflow = {
219 true,
220 None,
221 i64::MAX,
222 i64::MAX,
223 i64::MAX,
224 1,
225 i64::MAX,
226 SnapshotChunkDecision::Restart,
227 },
228 )]
229 fn chunk_admission_covers_retries_order_bounds_and_completion(
230 identity_matches: bool,
231 fixed_size: Option<i64>,
232 received: i64,
233 declared_size: i64,
234 position: i64,
235 chunk_len: i64,
236 max_size: i64,
237 expected: SnapshotChunkDecision,
238 ) {
239 check!(
240 snapshot_chunk_admission(
241 identity_matches,
242 fixed_size,
243 received,
244 declared_size,
245 position,
246 chunk_len,
247 max_size
248 ) == expected
249 );
250 }
251}