1use mountpoint_s3_crt_sys::*;
4use std::{
5 ffi::{OsStr, OsString},
6 os::unix::prelude::OsStrExt,
7 ptr::{self, NonNull},
8};
9use thiserror::Error;
10
11use crate::{
12 CrtError, ToAwsByteCursor, aws_byte_cursor_as_slice,
13 common::{allocator::Allocator, error::Error},
14};
15
16use super::s3_library_init;
17
18#[derive(Debug, Error, PartialEq, Eq)]
20pub enum ResolverError {
21 #[error("{0}")]
23 EndpointNotResolved(String),
24
25 #[error("CRT error: {0}")]
27 CrtError(#[from] Error),
28}
29
30#[derive(Debug)]
32pub struct RuleEngine {
33 inner: NonNull<aws_endpoints_rule_engine>,
34}
35
36impl RuleEngine {
37 pub fn new(allocator: &Allocator) -> Result<Self, Error> {
39 s3_library_init(allocator);
40 let inner = unsafe { aws_s3_endpoint_resolver_new(allocator.inner.as_ptr()).ok_or_last_error()? };
43 Ok(Self { inner })
44 }
45
46 pub fn resolve(&self, context: RequestContext) -> Result<ResolvedEndpoint, ResolverError> {
48 unsafe {
54 let mut out_endpoint: *mut aws_endpoints_resolved_endpoint = ptr::null_mut();
55 aws_endpoints_rule_engine_resolve(self.inner.as_ptr(), context.inner.as_ptr(), &mut out_endpoint)
56 .ok_or_last_error()?;
57 match aws_endpoints_resolved_endpoint_get_type(out_endpoint) {
58 aws_endpoints_resolved_endpoint_type::AWS_ENDPOINTS_RESOLVED_ERROR => {
59 let mut out_error: aws_byte_cursor = Default::default();
60 aws_endpoints_resolved_endpoint_get_error(out_endpoint, &mut out_error);
61 let resolved_endpoint_error = std::str::from_utf8(aws_byte_cursor_as_slice(&out_error))
62 .expect("endpoint errors are always valid UTF-8")
63 .to_owned();
64 aws_endpoints_resolved_endpoint_release(out_endpoint);
65 Err(ResolverError::EndpointNotResolved(resolved_endpoint_error))
66 }
67 aws_endpoints_resolved_endpoint_type::AWS_ENDPOINTS_RESOLVED_ENDPOINT => Ok(ResolvedEndpoint {
68 inner: NonNull::new_unchecked(out_endpoint),
69 }),
70 _ => unreachable!("Invalid Resolved Endpoint type"),
71 }
72 }
73 }
74}
75
76impl Drop for RuleEngine {
77 fn drop(&mut self) {
78 unsafe {
81 aws_endpoints_rule_engine_release(self.inner.as_ptr());
82 }
83 }
84}
85
86unsafe impl Send for RuleEngine {}
88unsafe impl Sync for RuleEngine {}
90
91#[derive(Debug)]
93pub struct RequestContext {
94 inner: NonNull<aws_endpoints_request_context>,
95}
96
97impl RequestContext {
98 pub fn new(allocator: &Allocator) -> Result<Self, Error> {
100 s3_library_init(allocator);
101 let inner = unsafe { aws_endpoints_request_context_new(allocator.inner.as_ptr()).ok_or_last_error()? };
104 Ok(Self { inner })
105 }
106
107 pub fn add_string(
109 &mut self,
110 allocator: &Allocator,
111 name: impl AsRef<OsStr>,
112 value: impl AsRef<OsStr>,
113 ) -> Result<(), Error> {
114 unsafe {
117 aws_endpoints_request_context_add_string(
118 allocator.inner.as_ptr(),
119 self.inner.as_ptr(),
120 name.as_aws_byte_cursor(),
121 value.as_aws_byte_cursor(),
122 )
123 .ok_or_last_error()
124 }
125 }
126
127 pub fn add_boolean(&mut self, allocator: &Allocator, name: impl AsRef<OsStr>, value: bool) -> Result<(), Error> {
129 unsafe {
132 aws_endpoints_request_context_add_boolean(
133 allocator.inner.as_ptr(),
134 self.inner.as_ptr(),
135 name.as_aws_byte_cursor(),
136 value,
137 )
138 .ok_or_last_error()
139 }
140 }
141}
142
143impl Drop for RequestContext {
144 fn drop(&mut self) {
145 unsafe {
148 aws_endpoints_request_context_release(self.inner.as_ptr());
149 }
150 }
151}
152
153#[derive(Debug)]
155pub struct ResolvedEndpoint {
156 inner: NonNull<aws_endpoints_resolved_endpoint>,
157}
158
159impl ResolvedEndpoint {
160 pub fn get_url(&self) -> OsString {
162 let mut url: aws_byte_cursor = Default::default();
163 unsafe {
166 aws_endpoints_resolved_endpoint_get_url(self.inner.as_ptr(), &mut url);
167 }
168 unsafe {
170 let uri = aws_byte_cursor_as_slice(&url);
171 OsStr::from_bytes(uri).to_os_string()
172 }
173 }
174
175 pub fn get_properties(&self) -> OsString {
177 let mut properties: aws_byte_cursor = Default::default();
178 unsafe {
181 aws_endpoints_resolved_endpoint_get_properties(self.inner.as_ptr(), &mut properties);
182 }
183 unsafe {
185 let endpoint_properties = aws_byte_cursor_as_slice(&properties);
186 OsStr::from_bytes(endpoint_properties).to_os_string()
187 }
188 }
189}
190
191impl Drop for ResolvedEndpoint {
192 fn drop(&mut self) {
193 unsafe {
196 aws_endpoints_resolved_endpoint_release(self.inner.as_ptr());
197 }
198 }
199}
200
201#[cfg(test)]
202mod test {
203 use crate::common::allocator::Allocator;
204
205 use super::{RequestContext, ResolverError, RuleEngine};
206
207 use test_case::test_case;
208
209 fn test_endpoint_resolver_init(
210 bucket: &str,
211 region: &str,
212 allocator: &Allocator,
213 request_context: &mut RequestContext,
214 ) {
215 request_context.add_string(allocator, "Bucket", bucket).unwrap();
216 request_context.add_string(allocator, "Region", region).unwrap();
217 }
218
219 #[test_case("s3-bucket-test", "cn-north-1", "https://s3-bucket-test.s3.cn-north-1.amazonaws.com.cn"; "regions outside aws partition")]
220 #[test_case("s3-bucket-test", "eu-west-1", "https://s3-bucket-test.s3.eu-west-1.amazonaws.com"; "regions within aws partition")]
221 #[test_case("s3-bucket-test", "us-gov-west-1", "https://s3-bucket-test.s3.us-gov-west-1.amazonaws.com"; "regions in aws-us-gov partition")]
222 #[test_case("s3-bucket.test", "eu-west-1", "https://s3.eu-west-1.amazonaws.com/s3-bucket.test"; "bucket name with . to check default behviour for aliases")]
223 #[test_case("mountpoint-o-o000s3-bucket-test0000000000000000000000000--op-s3", "us-east-1", "https://mountpoint-o-o000s3-bucket-test0000000000000000000000000--op-s3.op-000s3-bucket-test.s3-outposts.us-east-1.amazonaws.com"; "Outpost Access Point alias")]
224 #[test_case("arn:aws:s3::accountID:accesspoint/s3-bucket-test.mrap", "eu-west-1", "https://s3-bucket-test.mrap.accesspoint.s3-global.amazonaws.com"; "ARN as bucket name")]
225 #[test_case("s3-bucket-test", "invalid-region", "https://s3-bucket-test.s3.invalid-region.amazonaws.com"; "invalid region name")]
226 fn test_default_regions_and_buckets_setting(bucket: &str, region: &str, resolved_endpoint: &str) {
227 let new_allocator = Allocator::default();
228 let endpoint_rule_engine = RuleEngine::new(&new_allocator).unwrap();
229 let mut endpoint_request_context = RequestContext::new(&new_allocator).unwrap();
230 test_endpoint_resolver_init(bucket, region, &new_allocator, &mut endpoint_request_context);
231 let endpoint_resolved = endpoint_rule_engine
232 .resolve(endpoint_request_context)
233 .expect("endpoint should resolve as rules should match context");
234 let endpoint_uri = endpoint_resolved.get_url();
235 assert_eq!(endpoint_uri, resolved_endpoint);
236 }
237
238 #[test_case("UseFIPS", "https://s3-bucket-test.s3-fips.us-east-1.amazonaws.com"; "Using FIPS option")]
239 #[test_case("UseDualStack", "https://s3-bucket-test.s3.dualstack.us-east-1.amazonaws.com"; "Using Dual Stack (IPv6) option")]
240 #[test_case("Accelerate", "https://s3-bucket-test.s3-accelerate.amazonaws.com"; "Using transfer acceleration option")]
241 #[test_case("ForcePathStyle", "https://s3.us-east-1.amazonaws.com/s3-bucket-test"; "Addressing style set to Path style")]
242 #[test_case("InvalidOption", "https://s3-bucket-test.s3.us-east-1.amazonaws.com"; "Invalid option is ignored")]
243 fn test_optional_settings(mount_option: &str, resolved_endpoint: &str) {
244 let new_allocator = Allocator::default();
245 let endpoint_rule_engine = RuleEngine::new(&new_allocator).unwrap();
246 let mut endpoint_request_context = RequestContext::new(&new_allocator).unwrap();
247 test_endpoint_resolver_init(
248 "s3-bucket-test",
249 "us-east-1",
250 &new_allocator,
251 &mut endpoint_request_context,
252 );
253 endpoint_request_context
254 .add_boolean(&new_allocator, mount_option, true)
255 .unwrap();
256 let endpoint_resolved = endpoint_rule_engine
257 .resolve(endpoint_request_context)
258 .expect("endpoint should resolve as rules should match context");
259 let endpoint_uri = endpoint_resolved.get_url();
260 assert_eq!(endpoint_uri, resolved_endpoint);
261 }
262
263 #[test_case(vec!["UseDualStack".to_string(), "Accelerate".to_string()], "https://s3-bucket-test.s3-accelerate.dualstack.amazonaws.com"; "Using Dual Stack and transfer acceleration")]
264 #[test_case(vec!["UseDualStack".to_string(), "UseFIPS".to_string()], "https://s3-bucket-test.s3-fips.dualstack.us-east-1.amazonaws.com"; "Using Dual Stack and FIPS settings")]
265 fn test_options_combination_setting(mount_options: Vec<String>, resolved_endpoint: &str) {
266 let new_allocator = Allocator::default();
267 let endpoint_rule_engine = RuleEngine::new(&new_allocator).unwrap();
268 let mut endpoint_request_context = RequestContext::new(&new_allocator).unwrap();
269 test_endpoint_resolver_init(
270 "s3-bucket-test",
271 "us-east-1",
272 &new_allocator,
273 &mut endpoint_request_context,
274 );
275 for options in mount_options.iter() {
276 endpoint_request_context
277 .add_boolean(&new_allocator, options, true)
278 .unwrap();
279 }
280 let endpoint_resolved = endpoint_rule_engine
281 .resolve(endpoint_request_context)
282 .expect("endpoint should resolve as rules should match context");
283 let endpoint_uri = endpoint_resolved.get_url();
284 assert_eq!(endpoint_uri, resolved_endpoint);
285 }
286
287 #[test]
288 fn test_incorrect_transfer_acceleration_path_style_combination() {
289 let new_allocator = Allocator::default();
290 let endpoint_rule_engine = RuleEngine::new(&new_allocator).unwrap();
291 let mut endpoint_request_context = RequestContext::new(&new_allocator).unwrap();
292 test_endpoint_resolver_init(
293 "s3-bucket-test",
294 "us-east-1",
295 &new_allocator,
296 &mut endpoint_request_context,
297 );
298 endpoint_request_context
299 .add_boolean(&new_allocator, "ForcePathStyle", true)
300 .unwrap();
301 endpoint_request_context
302 .add_boolean(&new_allocator, "Accelerate", true)
303 .unwrap();
304 let err = endpoint_rule_engine
305 .resolve(endpoint_request_context)
306 .expect_err("Transfer acceleration should not work with Path style addressing");
307 assert_eq!(
308 err,
309 ResolverError::EndpointNotResolved("Path-style addressing cannot be used with S3 Accelerate".to_owned())
310 );
311 }
312
313 #[test]
314 fn test_resolver_without_region_failure() {
315 let new_allocator = Allocator::default();
316 let endpoint_rule_engine = RuleEngine::new(&new_allocator).unwrap();
317 let mut endpoint_request_context = RequestContext::new(&new_allocator).unwrap();
318 endpoint_request_context
319 .add_string(&new_allocator, "Bucket", "test-bucket")
320 .unwrap();
321 let err = endpoint_rule_engine
322 .resolve(endpoint_request_context)
323 .expect_err("Ednpoint should not be formed without region specified");
324 assert_eq!(
325 err,
326 ResolverError::EndpointNotResolved("A region must be set when sending requests to S3.".to_owned())
327 );
328 }
329
330 #[test_case("arn:aws:s3-object-lambda:eu-west-1:AccountID:accesspoint/olap-bucket-test", "{\"authSchemes\":[{\"disableDoubleEncoding\":true,\"name\":\"sigv4\",\"signingName\":\"s3-object-lambda\",\"signingRegion\":\"eu-west-1\"}]}"; "OLAP ARN")]
331 #[test_case("arn:aws:s3:us-east-2:AccountID:accesspoint/access-point-test", "{\"authSchemes\":[{\"disableDoubleEncoding\":true,\"name\":\"sigv4\",\"signingName\":\"s3\",\"signingRegion\":\"us-east-2\"}]}"; "Access Point ARN")]
332 #[test_case("arn:aws:s3::accountID:accesspoint/s3-bucket-test.mrap", "{\"authSchemes\":[{\"disableDoubleEncoding\":true,\"name\":\"sigv4a\",\"signingName\":\"s3\",\"signingRegionSet\":[\"*\"]}]}"; "MRAP")]
333 fn test_endpoint_properties_for_arn(arn: &str, endpoint_property: &str) {
334 let new_allocator = Allocator::default();
335 let endpoint_rule_engine = RuleEngine::new(&new_allocator).unwrap();
336 let mut endpoint_request_context = RequestContext::new(&new_allocator).unwrap();
337 test_endpoint_resolver_init(arn, "us-east-1", &new_allocator, &mut endpoint_request_context);
338 let resolved_endpoint = endpoint_rule_engine.resolve(endpoint_request_context).unwrap();
339 let property = resolved_endpoint.get_properties();
340 assert_eq!(property, endpoint_property);
341 }
342
343 #[test_case("my-access-point-random-s3alias", "{\"authSchemes\":[{\"disableDoubleEncoding\":true,\"name\":\"sigv4\",\"signingName\":\"s3\",\"signingRegion\":\"us-east-2\"}]}"; "AccessPoint Alias")]
344 #[test_case("my-object-lambda-acc-random-alias--ol-s3", "{\"authSchemes\":[{\"disableDoubleEncoding\":true,\"name\":\"sigv4\",\"signingName\":\"s3\",\"signingRegion\":\"us-east-2\"}]}"; "ObjectLambda Alias")]
345 #[test_case("test-bucket", "{\"authSchemes\":[{\"disableDoubleEncoding\":true,\"name\":\"sigv4\",\"signingName\":\"s3\",\"signingRegion\":\"us-east-2\"}]}"; "Normal bucket")]
346 fn test_endpoint_properties_for_bucket_alias(bucket: &str, endpoint_property: &str) {
347 let new_allocator = Allocator::default();
348 let endpoint_rule_engine = RuleEngine::new(&new_allocator).unwrap();
349 let mut endpoint_request_context = RequestContext::new(&new_allocator).unwrap();
350 test_endpoint_resolver_init(bucket, "us-east-2", &new_allocator, &mut endpoint_request_context);
351 let resolved_endpoint = endpoint_rule_engine.resolve(endpoint_request_context).unwrap();
352 let property = resolved_endpoint.get_properties();
353 assert_eq!(property, endpoint_property);
354 }
355}