qubit_fs/copy/
copy_outcome.rs1use crate::copy::CopyConflictPolicy;
11use crate::copy::CopyMethod;
12use crate::copy::CopyMode;
13use crate::copy::CopyOptions;
14use crate::copy::CopyStats;
15use crate::copy::MetadataPreservePolicy;
16use crate::copy::ServerSidePreference;
17use crate::metadata::AchievedAtomicity;
18use crate::metadata::NonSensitiveMetadata;
19use crate::metadata::ResourceVersion;
20use crate::metadata::UserMetadata;
21
22#[derive(Clone, Debug, PartialEq)]
34pub struct CopyOutcome {
35 stats: CopyStats,
37 method: CopyMethod,
39 atomicity: AchievedAtomicity,
41 durable: bool,
43 metadata: MetadataPreservePolicy,
45 target_version: Option<ResourceVersion>,
47 used_fallback: bool,
49 diagnostics: NonSensitiveMetadata,
51}
52
53impl CopyOutcome {
54 #[inline]
64 #[must_use]
65 pub fn new(stats: CopyStats, method: CopyMethod, atomicity: AchievedAtomicity) -> Self {
66 Self {
67 stats,
68 method,
69 atomicity,
70 durable: false,
71 metadata: MetadataPreservePolicy::None,
72 target_version: None,
73 used_fallback: false,
74 diagnostics: NonSensitiveMetadata::new(),
75 }
76 }
77
78 #[inline]
81 #[must_use]
82 pub fn with_diagnostics(mut self, diagnostics: UserMetadata) -> Self {
83 self.diagnostics = NonSensitiveMetadata::from(diagnostics);
84 self
85 }
86
87 #[inline]
89 #[must_use]
90 pub const fn stats(&self) -> &CopyStats {
91 &self.stats
92 }
93 #[inline]
95 #[must_use]
96 pub const fn method(&self) -> CopyMethod {
97 self.method
98 }
99 #[inline]
101 #[must_use]
102 pub const fn atomicity(&self) -> AchievedAtomicity {
103 self.atomicity
104 }
105 #[inline]
107 #[must_use]
108 pub const fn durable(&self) -> bool {
109 self.durable
110 }
111 #[inline]
113 #[must_use]
114 pub fn with_durable(mut self, durable: bool) -> Self {
115 self.durable = durable;
116 self
117 }
118
119 #[inline]
122 #[must_use]
123 pub fn with_metadata(mut self, metadata: MetadataPreservePolicy) -> Self {
124 self.metadata = metadata;
125 self
126 }
127
128 #[inline]
130 #[must_use]
131 pub fn with_target_version(mut self, target_version: ResourceVersion) -> Self {
132 self.target_version = Some(target_version);
133 self
134 }
135 #[inline]
137 #[must_use]
138 pub const fn metadata(&self) -> MetadataPreservePolicy {
139 self.metadata
140 }
141 #[inline]
143 #[must_use]
144 pub const fn target_version(&self) -> Option<&ResourceVersion> {
145 self.target_version.as_ref()
146 }
147 #[inline]
150 #[must_use]
151 pub const fn used_fallback(&self) -> bool {
152 self.used_fallback
153 }
154 #[inline]
156 #[must_use]
157 pub const fn diagnostics(&self) -> &NonSensitiveMetadata {
158 &self.diagnostics
159 }
160 pub(crate) fn streamed_fallback(stats: CopyStats, atomicity: AchievedAtomicity, durable: bool) -> Self {
162 Self {
163 stats,
164 method: CopyMethod::Streamed,
165 atomicity,
166 durable,
167 metadata: MetadataPreservePolicy::None,
168 target_version: None,
169 used_fallback: true,
170 diagnostics: NonSensitiveMetadata::new(),
171 }
172 }
173
174 pub(crate) fn contract_violation(&self, options: &CopyOptions) -> Option<&'static str> {
177 if self.used_fallback || self.method == CopyMethod::Streamed {
178 return Some("provider returned a facade streamed-fallback outcome as native success");
179 }
180 if options.atomicity() == crate::metadata::AtomicityRequirement::Required
181 && self.atomicity != AchievedAtomicity::Atomic
182 {
183 return Some("provider reported non-atomic success for an atomic-required copy");
184 }
185 if options.durability() == crate::metadata::DurabilityRequirement::Required && !self.durable {
186 return Some("provider reported non-durable success for a durability-required copy");
187 }
188 if options.server_side() == ServerSidePreference::Require && self.method != CopyMethod::ServerSide {
189 return Some("provider reported a non-server-side success for a server-side-required copy");
190 }
191 if options.server_side() == ServerSidePreference::Disable && self.method == CopyMethod::ServerSide {
192 return Some("provider reported a server-side success for a server-side-disabled copy");
193 }
194 if self.metadata != options.preserve_metadata() {
195 return Some("provider reported metadata preservation different from the copy request");
196 }
197 if !options.continue_on_error() && self.stats.failed != 0 {
198 return Some("provider reported failed copy entries without continue-on-error");
199 }
200 if options.conflict() != CopyConflictPolicy::Skip && self.stats.skipped != 0 {
201 return Some("provider reported skipped copy entries without a skip conflict policy");
202 }
203 if options.conflict() != CopyConflictPolicy::Overwrite && self.stats.overwritten != 0 {
204 return Some("provider reported overwritten copy entries without an overwrite conflict policy");
205 }
206 if options.max_bytes().is_some_and(|maximum| self.stats.bytes > maximum) {
207 return Some("provider reported copy bytes beyond the requested limit");
208 }
209 let entries = self
210 .stats
211 .files
212 .checked_add(self.stats.directories)
213 .and_then(|value| value.checked_add(self.stats.symlinks))
214 .and_then(|value| value.checked_add(self.stats.objects))
215 .and_then(|value| value.checked_add(self.stats.prefixes));
216 let valid_skipped_file = options.mode() == CopyMode::File
217 && options.conflict() == CopyConflictPolicy::Skip
218 && self.stats.skipped == 1
219 && entries == Some(0);
220 let valid_copied_file = entries == Some(1) && self.stats.directories == 0 && self.stats.prefixes == 0;
221 if options.mode() == CopyMode::File && !valid_copied_file && !valid_skipped_file {
222 return Some("provider reported a file-mode copy without exactly one resource");
223 }
224 if entries.is_none()
225 || options.max_entries().is_some_and(|maximum| {
226 u64::try_from(maximum).is_ok_and(|maximum| entries.is_some_and(|entries| entries > maximum))
227 })
228 {
229 return Some("provider reported copy entries beyond the requested limit");
230 }
231 None
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use std::hint::black_box;
238
239 use super::CopyOutcome;
240 use crate::copy::CopyMethod;
241 use crate::copy::CopyOptions;
242 use crate::copy::CopyStats;
243 use crate::copy::MetadataPreservePolicy;
244 use crate::metadata::AchievedAtomicity;
245 use crate::metadata::NonSensitiveMetadata;
246 use crate::metadata::ResourceVersion;
247
248 #[test]
249 fn outcome_accessors_are_executed_at_runtime() {
250 let constructor: fn(CopyStats, CopyMethod, AchievedAtomicity) -> CopyOutcome = black_box(CopyOutcome::new);
251 let metadata: fn(&CopyOutcome) -> MetadataPreservePolicy = black_box(CopyOutcome::metadata);
252 let target_version: for<'a> fn(&'a CopyOutcome) -> Option<&'a ResourceVersion> =
253 black_box(CopyOutcome::target_version);
254 let diagnostics: fn(&CopyOutcome) -> &NonSensitiveMetadata = black_box(CopyOutcome::diagnostics);
255 let with_target_version: fn(CopyOutcome, ResourceVersion) -> CopyOutcome =
256 black_box(CopyOutcome::with_target_version);
257 let with_diagnostics: fn(CopyOutcome, crate::metadata::UserMetadata) -> CopyOutcome =
258 black_box(CopyOutcome::with_diagnostics);
259
260 let outcome = with_diagnostics(
261 with_target_version(
262 constructor(CopyStats::default(), CopyMethod::Native, AchievedAtomicity::Atomic),
263 ResourceVersion::new("generation-7"),
264 ),
265 crate::metadata::UserMetadata::new(),
266 );
267 assert_eq!(MetadataPreservePolicy::None, metadata(&outcome));
268 assert_eq!(
269 Some("generation-7"),
270 target_version(&outcome).map(ResourceVersion::as_str)
271 );
272 assert!(diagnostics(&outcome).is_empty());
273
274 let bytes_exceeded = CopyOutcome::new(
275 CopyStats {
276 bytes: 11,
277 ..CopyStats::default()
278 },
279 CopyMethod::Native,
280 AchievedAtomicity::Atomic,
281 );
282 assert!(
283 bytes_exceeded
284 .contract_violation(&CopyOptions::default().with_max_bytes(Some(10)))
285 .is_some()
286 );
287
288 let entries_exceeded = CopyOutcome::new(
289 CopyStats {
290 files: 2,
291 ..CopyStats::default()
292 },
293 CopyMethod::Native,
294 AchievedAtomicity::Atomic,
295 );
296 assert!(
297 entries_exceeded
298 .contract_violation(&CopyOptions::tree().with_max_entries(Some(1)))
299 .is_some()
300 );
301 }
302}