1use crate::{
5 S3BucketVersioning, S3Checksums, S3CopyCondition, S3CopyPublication, S3Listing,
6 S3MultipartTimestamp, S3Profile, S3ResponseHeaders, S3Revision, S3Settings, S3Signing,
7 S3SinglePut, StorageCheck, StorageCheckError, StorageConfig, StorageError, StorageFailure,
8};
9use std::sync::Arc;
10
11#[derive(Clone, Debug)]
13pub struct S3Detection {
14 pub versioning: Result<S3BucketVersioning, Arc<StorageError>>,
16 pub settings: S3Settings,
18 pub results: Vec<S3ProbeResult>,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct S3DetectionRuns {
28 pub run: uuid::Uuid,
30 pub earlier: Vec<uuid::Uuid>,
32}
33
34#[derive(Debug)]
36pub struct S3DetectionRun {
37 pub detection: Result<S3Detection, StorageError>,
39 pub cleaned: Vec<uuid::Uuid>,
43}
44
45impl S3DetectionRun {
46 pub fn unwritten(runs: &S3DetectionRuns, detection: Result<S3Detection, StorageError>) -> Self {
49 Self { detection, cleaned: vec![runs.run] }
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct S3ProbeResult {
56 pub probe: S3Probe,
58 pub held: bool,
60 pub detail: String,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum S3Probe {
67 Signing(S3Signing),
69 SinglePut(S3SinglePut),
71 PartialUploadHidden,
73 ConditionalCompletion,
75 CompletionTimestamp,
77 CopyPublication(S3CopyCondition),
79 CopyTimestamp,
81 Listing(S3Listing),
83 ChecksumMode,
85 PassiveChecksum,
87 ChecksumReturned,
89 ChecksumScopeStated,
91 GoogleGeneration,
93 GoogleHeaders,
95 BucketList,
98}
99
100pub(crate) const SIGNERS: [S3Signing; 2] = [S3Signing::GoogleHmac, S3Signing::AwsSigV4];
102pub(crate) const SINGLE_PUTS: [S3SinglePut; 2] =
103 [S3SinglePut::IfNoneMatch, S3SinglePut::GenerationMatch];
104pub(crate) const COPY_CONDITIONS: [S3CopyCondition; 3] = [
105 S3CopyCondition::GenerationMatch,
106 S3CopyCondition::IfNoneMatch,
107 S3CopyCondition::CloudflareIfNoneMatch,
108];
109
110impl S3Detection {
111 pub(crate) fn derive(
115 results: Vec<S3ProbeResult>,
116 plain: S3Settings,
117 versioning: Result<S3BucketVersioning, Arc<StorageError>>,
118 ) -> Self {
119 let detection = Self { versioning, settings: plain, results };
120 let held = |probe| detection.held(probe);
121 let first = |candidates: &[S3Probe]| candidates.iter().copied().find(|probe| held(*probe));
122 let mut settings = plain;
123 if let Some(S3Probe::Signing(signing)) = first(&SIGNERS.map(S3Probe::Signing)) {
124 settings.signing = signing;
125 }
126 if let Some(S3Probe::SinglePut(condition)) = first(&SINGLE_PUTS.map(S3Probe::SinglePut)) {
127 settings.single_put = condition;
128 }
129 settings.multipart_timestamp = if held(S3Probe::CompletionTimestamp) {
130 S3MultipartTimestamp::Completion
131 } else {
132 S3MultipartTimestamp::Start
133 };
134 settings.copy_publication = if held(S3Probe::ConditionalCompletion)
137 && held(S3Probe::PartialUploadHidden)
138 && held(S3Probe::CompletionTimestamp)
139 {
140 S3CopyPublication::Never
141 } else if let Some(S3Probe::CopyPublication(condition)) =
142 first(©_CONDITIONS.map(S3Probe::CopyPublication))
143 {
144 S3CopyPublication::Multipart(condition)
145 } else {
146 S3CopyPublication::Never
147 };
148 settings.listing =
149 if held(S3Probe::Listing(S3Listing::V2)) { S3Listing::V2 } else { S3Listing::V1 };
150 settings.checksums = if !held(S3Probe::ChecksumReturned) {
151 S3Checksums::Absent
152 } else if !held(S3Probe::ChecksumScopeStated) {
153 S3Checksums::UnstatedIgnored
154 } else {
155 S3Checksums::ScopeStated
156 };
157 settings.request_checksums = held(S3Probe::ChecksumMode)
159 && !held(S3Probe::PassiveChecksum)
160 && settings.checksums != S3Checksums::Absent;
161 settings.revision =
162 if held(S3Probe::GoogleGeneration) { S3Revision::Generation } else { S3Revision::ETag };
163 settings.response_headers = if held(S3Probe::GoogleHeaders) {
164 S3ResponseHeaders::Google
165 } else {
166 S3ResponseHeaders::S3
167 };
168 Self { settings, ..detection }
169 }
170
171 pub fn held(&self, probe: S3Probe) -> bool {
173 self.results.iter().any(|observation| observation.probe == probe && observation.held)
174 }
175
176 pub fn check(&self, settings: &S3Settings) -> Result<(), StorageCheckError> {
180 let fail = |check, reason: &'static str| {
181 Err(StorageCheckError::ProviderCheck {
182 check,
183 source: StorageFailure::Protocol.with_source(reason),
184 })
185 };
186 let held = |probe| self.held(probe);
187 if !held(S3Probe::SinglePut(settings.single_put)) {
188 return fail(
189 StorageCheck::CreateOnce,
190 "the single-PUT condition did not refuse an occupied name with its bytes and time unchanged",
191 );
192 }
193 let (create_once, dated) = match settings.copy_publication.multipart() {
194 None => (
195 held(S3Probe::ConditionalCompletion) && held(S3Probe::PartialUploadHidden),
196 held(S3Probe::CompletionTimestamp),
197 ),
198 Some(condition) => {
199 (held(S3Probe::CopyPublication(condition)), held(S3Probe::CopyTimestamp))
200 }
201 };
202 if !create_once {
203 return fail(
204 StorageCheck::MultipartCreateOnce,
205 "multipart publication did not refuse an occupied name with its bytes and time unchanged, or showed an unfinished upload",
206 );
207 }
208 if !dated {
209 return fail(
210 StorageCheck::PublicationTime,
211 "multipart publication is not dated by complete publication",
212 );
213 }
214 let observed = if held(S3Probe::CompletionTimestamp) {
215 S3MultipartTimestamp::Completion
216 } else {
217 S3MultipartTimestamp::Start
218 };
219 if settings.multipart_timestamp != observed {
220 return fail(
221 StorageCheck::PublicationTime,
222 "the multipart timestamp setting contradicts the observed timestamp",
223 );
224 }
225 let behavior = [
226 (held(S3Probe::Signing(settings.signing)), "the service refused this signer"),
227 (
228 held(S3Probe::Listing(settings.listing)),
229 "this listing protocol failed or disagreed with V1",
230 ),
231 (
232 match settings.checksums {
233 S3Checksums::Absent => !held(S3Probe::ChecksumReturned),
234 S3Checksums::ScopeStated => held(S3Probe::ChecksumScopeStated),
235 S3Checksums::UnstatedIgnored => true,
236 },
237 "the checksum policy contradicts the returned checksums",
238 ),
239 (
240 !settings.request_checksums || held(S3Probe::ChecksumMode),
241 "the service refused checksum mode",
242 ),
243 (
244 settings.revision == S3Revision::ETag || held(S3Probe::GoogleGeneration),
245 "generation revisions need GCS's generation header",
246 ),
247 (
248 (settings.response_headers == S3ResponseHeaders::Google)
249 == held(S3Probe::GoogleHeaders),
250 "the response-header setting contradicts the headers returned",
251 ),
252 (held(S3Probe::BucketList), "the service refused the zero-entry bucket listing"),
253 ];
254 match behavior.into_iter().find(|(agrees, _)| !agrees) {
255 Some((_, reason)) => fail(StorageCheck::Behavior, reason),
256 None => Ok(()),
257 }
258 }
259
260 pub fn resolve(&self, location: StorageConfig) -> Result<StorageConfig, StorageCheckError> {
264 let StorageConfig::S3 { bucket, region, endpoint, profile } = location else {
265 return Err(StorageFailure::InvalidConfiguration
266 .with_source("expected S3 configuration")
267 .into());
268 };
269 let (profile, settings) =
270 match crate::providers::s3_settings(endpoint.as_ref(), profile.as_ref())? {
271 Some(settings) => (profile, settings),
272 None => (Some(S3Profile::Custom(self.settings)), self.settings),
273 };
274 crate::providers::check_s3_versioning(
275 endpoint.as_ref(),
276 profile.as_ref(),
277 &self.versioning,
278 )?;
279 self.check(&settings)?;
280 Ok(StorageConfig::S3 { bucket, region, endpoint, profile })
281 }
282}
283
284#[cfg(test)]
285#[path = "s3_detection_tests.rs"]
286mod tests;