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.