1use {
2 crate::{
3 utils::{validate_account_id, validate_partition, validate_region, validate_service},
4 ArnError,
5 },
6 serde::{de, Deserialize, Serialize},
7 std::{
8 cmp::Ordering,
9 fmt::{Display, Formatter, Result as FmtResult},
10 hash::Hash,
11 str::FromStr,
12 },
13};
14
15const PARTITION_START: usize = 4;
16
17#[derive(Debug, Clone, Eq, Hash, PartialEq)]
24pub struct Arn {
25 arn: String,
26 service_start: usize,
27 region_start: usize,
28 account_id_start: usize,
29 resource_start: usize,
30}
31
32impl Arn {
33 pub fn new(
57 partition: &str,
58 service: &str,
59 region: &str,
60 account_id: &str,
61 resource: &str,
62 ) -> Result<Self, ArnError> {
63 validate_partition(partition)?;
64 validate_service(service)?;
65 if !region.is_empty() {
66 validate_region(region)?
67 }
68 if !account_id.is_empty() {
69 validate_account_id(account_id)?
70 }
71
72 unsafe { Ok(Self::new_unchecked(partition, service, region, account_id, resource)) }
74 }
75
76 pub unsafe fn new_unchecked(
88 partition: &str,
89 service: &str,
90 region: &str,
91 account_id: &str,
92 resource: &str,
93 ) -> Self {
94 let arn = format!("arn:{partition}:{service}:{region}:{account_id}:{resource}");
95 let service_start = PARTITION_START + partition.len() + 1;
96 let region_start = service_start + service.len() + 1;
97 let account_id_start = region_start + region.len() + 1;
98 let resource_start = account_id_start + account_id.len() + 1;
99
100 Self {
101 arn,
102 service_start,
103 region_start,
104 account_id_start,
105 resource_start,
106 }
107 }
108
109 #[inline]
111 pub fn partition(&self) -> &str {
112 &self.arn[PARTITION_START..self.service_start - 1]
113 }
114
115 #[inline]
117 pub fn service(&self) -> &str {
118 &self.arn[self.service_start..self.region_start - 1]
119 }
120
121 #[inline]
123 pub fn region(&self) -> &str {
124 &self.arn[self.region_start..self.account_id_start - 1]
125 }
126
127 #[inline]
129 pub fn account_id(&self) -> &str {
130 &self.arn[self.account_id_start..self.resource_start - 1]
131 }
132
133 #[inline]
135 pub fn resource(&self) -> &str {
136 &self.arn[self.resource_start..]
137 }
138}
139
140impl Display for Arn {
141 fn fmt(&self, f: &mut Formatter) -> FmtResult {
143 f.write_str(&self.arn)
144 }
145}
146
147impl FromStr for Arn {
149 type Err = ArnError;
151
152 fn from_str(s: &str) -> Result<Self, ArnError> {
163 let parts: Vec<&str> = s.splitn(6, ':').collect();
164 if parts.len() != 6 {
165 return Err(ArnError::InvalidArn(s.to_string()));
166 }
167
168 if parts[0] != "arn" {
169 return Err(ArnError::InvalidScheme(parts[0].to_string()));
170 }
171
172 Self::new(parts[1], parts[2], parts[3], parts[4], parts[5])
173 }
174}
175
176impl PartialOrd for Arn {
178 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
180 Some(self.cmp(other))
181 }
182}
183
184impl Ord for Arn {
186 fn cmp(&self, other: &Self) -> Ordering {
188 match self.partition().cmp(other.partition()) {
189 Ordering::Equal => match self.service().cmp(other.service()) {
190 Ordering::Equal => match self.region().cmp(other.region()) {
191 Ordering::Equal => match self.account_id().cmp(other.account_id()) {
192 Ordering::Equal => self.resource().cmp(other.resource()),
193 x => x,
194 },
195 x => x,
196 },
197 x => x,
198 },
199 x => x,
200 }
201 }
202}
203
204impl<'de> Deserialize<'de> for Arn {
205 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
206 where
207 D: serde::Deserializer<'de>,
208 {
209 let s = String::deserialize(deserializer)?;
210 Self::from_str(&s).map_err(de::Error::custom)
211 }
212}
213
214impl Serialize for Arn {
215 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
216 where
217 S: serde::Serializer,
218 {
219 serializer.serialize_str(&self.arn)
220 }
221}
222
223#[cfg(test)]
224mod test {
225 use {
226 super::Arn,
227 crate::{
228 utils::{validate_account_id, validate_region},
229 ArnError,
230 },
231 pretty_assertions::assert_eq,
232 std::{
233 collections::hash_map::DefaultHasher,
234 hash::{Hash, Hasher},
235 str::FromStr,
236 },
237 };
238
239 #[test]
240 fn check_arn_derived() {
241 let arn1a = Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap();
242 let arn1b = Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap();
243 let arn2 = Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef1").unwrap();
244 let arn3 = Arn::from_str("arn:aws:ec2:us-east-1:123456789013:instance/i-1234567890abcdef0").unwrap();
245 let arn4 = Arn::from_str("arn:aws:ec2:us-east-2:123456789012:instance/i-1234567890abcdef0").unwrap();
246 let arn5 = Arn::from_str("arn:aws:ec3:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap();
247 let arn6 = Arn::from_str("arn:awt:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap();
248
249 assert_eq!(arn1a, arn1b);
250 assert!(arn1a < arn2);
251 assert!(arn2 < arn3);
252 assert!(arn3 < arn4);
253 assert!(arn4 < arn5);
254 assert!(arn5 < arn6);
255
256 assert_eq!(arn1a, arn1a.clone());
257
258 assert!(arn1a <= arn1b);
260 assert!(arn1a < arn2);
261 assert!(arn2 > arn1a);
262 assert!(arn2 < arn3);
263 assert!(arn1a < arn3);
264 assert!(arn3 > arn2);
265 assert!(arn3 > arn1a);
266 assert!(arn3 < arn4);
267 assert!(arn4 > arn3);
268 assert!(arn4 < arn5);
269 assert!(arn5 > arn4);
270 assert!(arn5 < arn6);
271 assert!(arn6 > arn5);
272
273 assert!(arn3.clone().min(arn4.clone()) == arn3);
274 assert!(arn4.clone().max(arn3) == arn4);
275
276 let mut h1a = DefaultHasher::new();
278 let mut h1b = DefaultHasher::new();
279 arn1a.hash(&mut h1a);
280 arn1b.hash(&mut h1b);
281 assert_eq!(h1a.finish(), h1b.finish());
282
283 let mut h2 = DefaultHasher::new();
284 arn2.hash(&mut h2);
285
286 _ = format!("{arn1a:?}");
288
289 assert_eq!(arn1a.to_string(), "arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0".to_string());
290 }
291
292 #[test]
293 fn check_arn_components() {
294 let arn = Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap();
295 assert_eq!(arn.partition(), "aws");
296 assert_eq!(arn.service(), "ec2");
297 assert_eq!(arn.region(), "us-east-1");
298 assert_eq!(arn.account_id(), "123456789012");
299 assert_eq!(arn.resource(), "instance/i-1234567890abcdef0");
300 }
301
302 #[test]
303 fn check_arn_empty() {
304 let arn1 = Arn::from_str("arn:aws:s3:::bucket").unwrap();
305 let arn2 = Arn::from_str("arn:aws:s3:us-east-1::bucket").unwrap();
306 let arn3 = Arn::from_str("arn:aws:s3:us-east-1:123456789012:bucket").unwrap();
307
308 assert!(arn1 < arn2);
309 assert!(arn2 < arn3);
310 assert!(arn1 < arn3);
311 }
312
313 #[test]
314 fn check_unicode() {
315 let arn = Arn::from_str("arn:aws-中国:één:日本-東京-1:123456789012:instance/i-1234567890abcdef0").unwrap();
316 assert_eq!(arn.partition(), "aws-中国");
317 assert_eq!(arn.service(), "één");
318 assert_eq!(arn.region(), "日本-東京-1");
319
320 let arn = Arn::from_str(
321 "arn:việtnam:nœrøyfjorden:ap-southeast-7-hòa-hiệp-bắc-3:123456789012:instance/i-1234567890abcdef0",
322 )
323 .unwrap();
324 assert_eq!(arn.partition(), "việtnam");
325 assert_eq!(arn.service(), "nœrøyfjorden");
326 assert_eq!(arn.region(), "ap-southeast-7-hòa-hiệp-bắc-3");
327 }
328
329 #[test]
330 fn check_malformed_arns() {
331 let wrong_parts =
332 vec!["arn", "arn:aws", "arn:aws:ec2", "arn:aws:ec2:us-east-1", "arn:aws:ec2:us-east-1:123456789012"];
333 for wrong_part in wrong_parts {
334 assert_eq!(Arn::from_str(wrong_part).unwrap_err(), ArnError::InvalidArn(wrong_part.to_string()));
335 }
336 }
337
338 #[test]
339 fn check_invalid_scheme() {
340 let err = Arn::from_str("http:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
341 assert_eq!(err.to_string(), r#"Invalid scheme: "http""#.to_string());
342 }
343
344 #[test]
345 fn check_invalid_partition() {
346 let err = Arn::from_str("arn:Aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
347 assert_eq!(err.to_string(), r#"Invalid partition: "Aws""#.to_string());
348
349 let err = Arn::from_str("arn:local-:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
350 assert_eq!(err.to_string(), r#"Invalid partition: "local-""#.to_string());
351
352 let err = Arn::from_str("arn::ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
353 assert_eq!(err.to_string(), r#"Invalid partition: """#.to_string());
354
355 let err = Arn::from_str("arn:-local:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
356 assert_eq!(err.to_string(), r#"Invalid partition: "-local""#.to_string());
357
358 let err = Arn::from_str("arn:aws--1:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
359 assert_eq!(err.to_string(), r#"Invalid partition: "aws--1""#.to_string());
360
361 let err = Arn::from_str(
362 "arn:this-partition-has-too-many-chars:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
363 )
364 .unwrap_err();
365 assert_eq!(err.to_string(), r#"Invalid partition: "this-partition-has-too-many-chars""#.to_string());
366
367 let err = Arn::from_str("arn:🦀:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
368 assert_eq!(err.to_string(), r#"Invalid partition: "🦀""#.to_string());
369 }
370
371 #[test]
372 fn check_invalid_services() {
373 let err = Arn::from_str("arn:aws:ec2-:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
374 assert_eq!(err.to_string(), r#"Invalid service name: "ec2-""#.to_string());
375
376 let err = Arn::from_str("arn:aws:-ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
377 assert_eq!(err.to_string(), r#"Invalid service name: "-ec2""#.to_string());
378
379 let err = Arn::from_str("arn:aws:Ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
380 assert_eq!(err.to_string(), r#"Invalid service name: "Ec2""#.to_string());
381
382 let err = Arn::from_str("arn:aws:ec--2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
383 assert_eq!(err.to_string(), r#"Invalid service name: "ec--2""#.to_string());
384
385 let err = Arn::from_str("arn:aws:🦀:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
386 assert_eq!(err.to_string(), r#"Invalid service name: "🦀""#.to_string());
387
388 let err = Arn::from_str("arn:aws::us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
389 assert_eq!(err.to_string(), r#"Invalid service name: """#.to_string());
390 }
391
392 #[test]
393 fn check_valid_regions() {
394 let arn = Arn::from_str("arn:aws:ec2:local:123456789012:instance/i-1234567890abcdef0").unwrap();
395 assert_eq!(arn.region(), "local");
396
397 let arn = Arn::from_str("arn:aws:ec2:us-east-1-bos-1:123456789012:instance/i-1234567890abcdef0").unwrap();
398 assert_eq!(arn.region(), "us-east-1-bos-1");
399 }
400
401 #[test]
402 fn check_invalid_region() {
403 let err = Arn::from_str("arn:aws:ec2:us-east-1-:123456789012:instance/i-1234567890abcdef0").unwrap_err();
404 assert_eq!(err.to_string(), r#"Invalid region: "us-east-1-""#.to_string());
405
406 let err = Arn::from_str("arn:aws:ec2:us-east-1a:123456789012:instance/i-1234567890abcdef0").unwrap_err();
407 assert_eq!(err.to_string(), r#"Invalid region: "us-east-1a""#.to_string());
408
409 let err = Arn::from_str("arn:aws:ec2:us-east1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
410 assert_eq!(err.to_string(), r#"Invalid region: "us-east1""#.to_string());
411
412 let err = Arn::from_str("arn:aws:ec2:-us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
413 assert_eq!(err.to_string(), r#"Invalid region: "-us-east-1""#.to_string());
414
415 let err = Arn::from_str("arn:aws:ec2:us-east:123456789012:instance/i-1234567890abcdef0").unwrap_err();
416 assert_eq!(err.to_string(), r#"Invalid region: "us-east""#.to_string());
417
418 let err = Arn::from_str("arn:aws:ec2:Us-East-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
419 assert_eq!(err.to_string(), r#"Invalid region: "Us-East-1""#.to_string());
420
421 let err = Arn::from_str("arn:aws:ec2:us-east--1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
422 assert_eq!(err.to_string(), r#"Invalid region: "us-east--1""#.to_string());
423
424 let err =
425 Arn::from_str("arn:aws:ec2:us-east-1-bos-1-lax-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
426 assert_eq!(err.to_string(), r#"Invalid region: "us-east-1-bos-1-lax-1""#.to_string());
427
428 let err = Arn::from_str("arn:aws:ec2:us-east-🦀:123456789012:instance/i-1234567890abcdef0").unwrap_err();
429 assert_eq!(err.to_string(), r#"Invalid region: "us-east-🦀""#.to_string());
430
431 let err = validate_region("").unwrap_err();
432 assert_eq!(err, ArnError::InvalidRegion("".to_string()));
433 }
434
435 #[test]
436 fn check_valid_account_ids() {
437 let arn = Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap();
438 assert_eq!(arn.account_id(), "123456789012");
439
440 let arn = Arn::from_str("arn:aws:ec2:us-east-1:aws:instance/i-1234567890abcdef0").unwrap();
441 assert_eq!(arn.account_id(), "aws");
442 }
443
444 #[test]
445 fn check_invalid_account_ids() {
446 let err = Arn::from_str("arn:aws:ec2:us-east-1:1234567890123:instance/i-1234567890abcdef0").unwrap_err();
447 assert_eq!(err.to_string(), r#"Invalid account id: "1234567890123""#.to_string());
448
449 let err = Arn::from_str("arn:aws:ec2:us-east-1:12345678901:instance/i-1234567890abcdef0").unwrap_err();
450 assert_eq!(err.to_string(), r#"Invalid account id: "12345678901""#.to_string());
451
452 let err = Arn::from_str("arn:aws:ec2:us-east-1:12345678901a:instance/i-1234567890abcdef0").unwrap_err();
453 assert_eq!(err.to_string(), r#"Invalid account id: "12345678901a""#.to_string());
454
455 let err = validate_account_id("").unwrap_err();
456 assert_eq!(err, ArnError::InvalidAccountId("".to_string()));
457 }
458
459 #[test]
460 fn check_serialization() {
461 let arn: Arn =
462 serde_json::from_str(r#""arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0""#).unwrap();
463 assert_eq!(arn.partition(), "aws");
464 assert_eq!(arn.service(), "ec2");
465 assert_eq!(arn.region(), "us-east-1");
466 assert_eq!(arn.account_id(), "123456789012");
467 assert_eq!(arn.resource(), "instance/i-1234567890abcdef0");
468
469 let arn_str = serde_json::to_string(&arn).unwrap();
470 assert_eq!(arn_str, r#""arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0""#);
471
472 let arn_err = serde_json::from_str::<Arn>(r#""arn:aws:ec2:us-east-1""#).unwrap_err();
473 assert_eq!(arn_err.to_string(), r#"Invalid ARN: "arn:aws:ec2:us-east-1""#);
474
475 let arn_err = serde_json::from_str::<Arn>(r#"{}"#);
476 assert_eq!(arn_err.unwrap_err().to_string(), "invalid type: map, expected a string at line 1 column 0");
477 }
478}