scratchstack_arn/utils.rs
1use crate::ArnError;
2
3/// Verify that a partition name meets the naming requirements.
4///
5/// AWS does not publish a formal specification for partition names. In this validator, we require:
6///
7/// * The partition must be composed of Unicode alphabetic non-uppercase characters, ASCII numeric
8/// characters, or `-` (codepoint `\u{0x002d}`).
9/// * The partition must have between 1 and 32 characters.
10/// * A `-` cannot appear in the first or last position, nor can it appear in two consecutive characters.
11///
12/// "Non-uppercase" is the same as "lowercase" for most Western scripts, but other scripts do not have a concept of
13/// uppercase and lowercase.
14///
15/// The value must be in NFKC normalized form for validation on accented characters to succeed. For example, `ç`
16/// represented as the codepoint `\u{0231}` ("Latin small letter c with cedilla") is valid, but `\u{0063}\u{0327}`
17/// ("Latin small letter c" followed by "combining cedilla") is not.
18///
19/// Examples of valid partition names:
20///
21/// * `aws`
22/// * `local`
23/// * `1`
24/// * `intranet-1`
25/// * `aws-中国`
26/// * `việtnam`
27///
28/// If `partition` meets the requirements, Ok is returned. Otherwise, a [ArnError::InvalidPartition] error is returned.
29pub fn validate_partition(partition: &str) -> Result<(), ArnError> {
30 if partition.is_empty() {
31 return Err(ArnError::InvalidPartition(partition.to_string()));
32 }
33
34 let mut last_was_dash = true;
35 for (i, c) in partition.char_indices() {
36 if i == 32 {
37 return Err(ArnError::InvalidPartition(partition.to_string()));
38 }
39
40 if (c.is_alphabetic() && !c.is_uppercase()) || c.is_ascii_digit() {
41 last_was_dash = false;
42 } else if c == '-' {
43 if last_was_dash {
44 return Err(ArnError::InvalidPartition(partition.to_string()));
45 }
46
47 last_was_dash = true;
48 } else {
49 return Err(ArnError::InvalidPartition(partition.to_string()));
50 }
51 }
52
53 if last_was_dash {
54 Err(ArnError::InvalidPartition(partition.to_string()))
55 } else {
56 Ok(())
57 }
58}
59
60/// Verify that an account id meets AWS requirements.
61///
62/// An account id must be 12 ASCII digits or the string `aws`.
63///
64/// If `account_id` meets this requirement, Ok is returned. Otherwise, a [ArnError::InvalidAccountId] error is
65/// returned.
66pub fn validate_account_id(account_id: &str) -> Result<(), ArnError> {
67 if account_id != "aws" {
68 let a_bytes = account_id.as_bytes();
69
70 if a_bytes.len() != 12 {
71 return Err(ArnError::InvalidAccountId(account_id.to_string()));
72 }
73
74 for c in a_bytes.iter() {
75 if !c.is_ascii_digit() {
76 return Err(ArnError::InvalidAccountId(account_id.to_string()));
77 }
78 }
79 }
80
81 Ok(())
82}
83
84#[derive(PartialEq)]
85enum RegionParseState {
86 Start,
87 LastWasAlpha,
88 LastWasDash,
89 LastWasDigit,
90}
91
92enum RegionParseSection {
93 Region,
94 LocalRegion,
95}
96
97/// Verify that a region name meets the naming requirements.
98///
99/// AWS does not publish a formal specification for region names. In this validator, we require:
100///
101/// * The region must be composed of Unicode alphabetic non-uppercase characters or `-` (codepoint 45/0x002d),
102/// followed by a `-` and one or more ASCII digits, or the name `local`.
103/// * The region can have a local region appended to it: after the region, a '-', one or more Unicode alphabetic
104/// non-uppercase characters or `-`, followed by a `-` and one or more ASCII digits.
105/// * A `-` cannot appear in the first or last position, nor can it appear in two consecutive characters.
106///
107/// "Non-uppercase" is the same as "lowercase" for most Western scripts, but other scripts do not have a concept of
108/// uppercase and lowercase.
109///
110/// Examples of valid region names:
111/// * `test-1`
112/// * `prod-west-1`
113/// * `prod-east-1-dca-2`
114/// * `sverige-söder-1`
115/// * `ap-southeast-7-hòa-hiệp-bắc-3`
116/// * `日本-東京-1`
117///
118/// If `region` meets the requirements, Ok is returned. Otherwise, a [ArnError::InvalidRegion] error is returned.
119pub fn validate_region(region: &str) -> Result<(), ArnError> {
120 // As a special case, we accept the region "local"
121 if region == "local" {
122 return Ok(());
123 }
124
125 let mut section = RegionParseSection::Region;
126 let mut state = RegionParseState::Start;
127
128 for c in region.chars() {
129 if c == '-' {
130 match state {
131 RegionParseState::Start | RegionParseState::LastWasDash => {
132 return Err(ArnError::InvalidRegion(region.to_string()));
133 }
134 RegionParseState::LastWasAlpha => {
135 state = RegionParseState::LastWasDash;
136 }
137 RegionParseState::LastWasDigit => match section {
138 RegionParseSection::Region => {
139 section = RegionParseSection::LocalRegion;
140 state = RegionParseState::LastWasDash;
141 }
142 RegionParseSection::LocalRegion => {
143 return Err(ArnError::InvalidRegion(region.to_string()));
144 }
145 },
146 }
147 } else if c.is_alphabetic() && !c.is_uppercase() {
148 match state {
149 RegionParseState::Start | RegionParseState::LastWasDash | RegionParseState::LastWasAlpha => {
150 state = RegionParseState::LastWasAlpha;
151 }
152 _ => {
153 return Err(ArnError::InvalidRegion(region.to_string()));
154 }
155 }
156 } else if c.is_ascii_digit() {
157 match state {
158 RegionParseState::LastWasDash | RegionParseState::LastWasDigit => {
159 state = RegionParseState::LastWasDigit;
160 }
161 _ => {
162 return Err(ArnError::InvalidRegion(region.to_string()));
163 }
164 }
165 } else {
166 return Err(ArnError::InvalidRegion(region.to_string()));
167 }
168 }
169
170 if state == RegionParseState::LastWasDigit {
171 Ok(())
172 } else {
173 Err(ArnError::InvalidRegion(region.to_string()))
174 }
175}
176
177/// Verify that a service name meets the naming requirements.
178///
179/// AWS does not publish a formal specification for service names. In this validator, we specify:
180/// * The service must be composed of at least one or more Unicode non-uppercase alphabetic characeters, numeric
181/// * characters, or `-`.
182/// * A `-` cannot appear in the first or last position, nor can it appear in two consecutive characters.
183///
184/// "Non-uppercase" is the same as "lowercase" for most Western scripts, but other scripts do not have a concept of
185/// uppercase and lowercase.
186///
187/// If `service` meets the requirements, Ok is returned. Otherwise, a [ArnError::InvalidService] error is
188/// returned.
189pub fn validate_service(service: &str) -> Result<(), ArnError> {
190 if service.is_empty() {
191 return Err(ArnError::InvalidService(service.to_string()));
192 }
193
194 let mut last_was_dash = true;
195
196 for c in service.chars() {
197 if c.is_alphanumeric() && !c.is_uppercase() {
198 last_was_dash = false;
199 } else if c == '-' {
200 if last_was_dash {
201 return Err(ArnError::InvalidService(service.to_string()));
202 }
203
204 last_was_dash = true;
205 } else {
206 return Err(ArnError::InvalidService(service.to_string()));
207 }
208 }
209
210 if last_was_dash {
211 Err(ArnError::InvalidService(service.to_string()))
212 } else {
213 Ok(())
214 }
215}
216
217#[cfg(test)]
218mod test {
219 #[test]
220 fn check_valid_services() {
221 assert!(super::validate_service("s3").is_ok());
222 assert!(super::validate_service("kafka-cluster").is_ok());
223 assert!(super::validate_service("execute-api").is_ok());
224 }
225}
226// end tests -- do not delete; needed for coverage.