fakecloud_amplify/
shared.rs1use base64::Engine as _;
7use uuid::Uuid;
8
9pub fn app_arn(region: &str, account: &str, app_id: &str) -> String {
11 format!("arn:aws:amplify:{region}:{account}:apps/{app_id}")
12}
13
14pub fn branch_arn(region: &str, account: &str, app_id: &str, branch: &str) -> String {
16 format!("arn:aws:amplify:{region}:{account}:apps/{app_id}/branches/{branch}")
17}
18
19pub fn domain_arn(region: &str, account: &str, app_id: &str, domain: &str) -> String {
21 format!("arn:aws:amplify:{region}:{account}:apps/{app_id}/domains/{domain}")
22}
23
24pub fn webhook_arn(region: &str, account: &str, app_id: &str, webhook_id: &str) -> String {
26 format!("arn:aws:amplify:{region}:{account}:apps/{app_id}/webhooks/{webhook_id}")
27}
28
29pub fn backend_environment_arn(region: &str, account: &str, app_id: &str, name: &str) -> String {
31 format!("arn:aws:amplify:{region}:{account}:apps/{app_id}/backendenvironments/{name}")
32}
33
34pub fn job_arn(region: &str, account: &str, app_id: &str, branch: &str, job_id: &str) -> String {
36 format!("arn:aws:amplify:{region}:{account}:apps/{app_id}/branches/{branch}/jobs/{job_id}")
37}
38
39pub fn hash_str(s: &str) -> u64 {
42 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
43 for b in s.as_bytes() {
44 h ^= u64::from(*b);
45 h = h.wrapping_mul(0x0000_0100_0000_01b3);
46 }
47 h
48}
49
50pub fn new_app_id() -> String {
54 let raw = Uuid::new_v4().simple().to_string();
55 format!("d{}", &raw[..13])
57}
58
59pub fn new_webhook_id() -> String {
62 Uuid::new_v4().to_string()
63}
64
65pub fn default_domain(app_id: &str) -> String {
67 format!("{app_id}.amplifyapp.com")
68}
69
70pub fn webhook_url(region: &str, webhook_id: &str) -> String {
73 let token = Uuid::new_v4().simple().to_string();
74 format!(
75 "https://webhooks.amplify.{region}.amazonaws.com/prod/webhooks?id={webhook_id}&token={token}&operation=startbuild"
76 )
77}
78
79pub fn thumbnail_url(region: &str, app_id: &str, branch: &str) -> String {
81 let h = hash_str(&format!("{app_id}/{branch}"));
82 format!("https://aws-amplify-prod-{region}-artifacts.s3.{region}.amazonaws.com/{app_id}/{branch}/thumbnail-{h:016x}.png")
83}
84
85pub fn subdomain_dns_record(app_id: &str) -> String {
88 format!("CNAME {}", default_domain(app_id))
89}
90
91pub fn certificate_verification_dns_record(domain: &str) -> String {
94 let h = hash_str(domain);
95 format!("_{h:016x}.{domain}. CNAME _{h:016x}.acm-validations.aws.")
96}
97
98pub fn upload_url(region: &str, app_id: &str, branch: &str, key: &str) -> String {
100 let h = hash_str(&format!("{app_id}/{branch}/{key}"));
101 format!("https://aws-amplify-prod-{region}-deployments.s3.{region}.amazonaws.com/{app_id}/{branch}/{key}?X-Amz-Signature={h:016x}")
102}
103
104pub fn access_log_url(region: &str, app_id: &str, domain: &str) -> String {
106 let h = hash_str(&format!("{app_id}/{domain}"));
107 format!("https://aws-amplify-prod-{region}-accesslogs.s3.{region}.amazonaws.com/{app_id}/{domain}/access-logs-{h:016x}.csv?X-Amz-Signature={h:016x}")
108}
109
110pub fn encode_artifact_id(app_id: &str, branch: &str, job_id: &str, file_name: &str) -> String {
114 let raw = format!("{app_id}\x1f{branch}\x1f{job_id}\x1f{file_name}");
115 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(raw.as_bytes())
116}
117
118pub fn decode_artifact_id(id: &str) -> Option<(String, String, String, String)> {
120 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
121 .decode(id.as_bytes())
122 .ok()?;
123 let s = String::from_utf8(bytes).ok()?;
124 let parts: Vec<&str> = s.split('\x1f').collect();
125 if parts.len() != 4 {
126 return None;
127 }
128 Some((
129 parts[0].to_string(),
130 parts[1].to_string(),
131 parts[2].to_string(),
132 parts[3].to_string(),
133 ))
134}
135
136pub fn artifact_url(region: &str, app_id: &str, branch: &str, job_id: &str, file: &str) -> String {
138 let h = hash_str(&format!("{app_id}/{branch}/{job_id}/{file}"));
139 format!("https://aws-amplify-prod-{region}-artifacts.s3.{region}.amazonaws.com/{app_id}/{branch}/{job_id}/{file}?X-Amz-Signature={h:016x}")
140}
141
142pub fn now_epoch() -> f64 {
146 chrono::Utc::now().timestamp_millis() as f64 / 1000.0
147}
148
149pub fn parse_resource_arn(arn: &str) -> Option<(String, Option<String>)> {
155 let (_, rest) = arn.split_once(":apps/")?;
156 if rest.is_empty() {
157 return None;
158 }
159 match rest.split_once("/branches/") {
160 Some((app_id, branch)) if !app_id.is_empty() && !branch.is_empty() => {
161 Some((app_id.to_string(), Some(branch.to_string())))
162 }
163 Some(_) => None,
164 None => {
165 if rest.contains('/') {
167 None
168 } else {
169 Some((rest.to_string(), None))
170 }
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn app_id_matches_aws_pattern() {
181 let id = new_app_id();
182 assert!(id.starts_with('d'));
183 assert!(!id.is_empty() && id.len() <= 20);
184 assert!(id
185 .chars()
186 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
187 }
188
189 #[test]
190 fn artifact_id_round_trips() {
191 let id = encode_artifact_id("d123", "main", "5", "buildLogs.txt");
192 assert_eq!(
193 decode_artifact_id(&id),
194 Some((
195 "d123".to_string(),
196 "main".to_string(),
197 "5".to_string(),
198 "buildLogs.txt".to_string()
199 ))
200 );
201 assert_eq!(decode_artifact_id("not-a-real-id!!!"), None);
202 }
203
204 #[test]
205 fn parse_resource_arn_app_and_branch() {
206 assert_eq!(
207 parse_resource_arn("arn:aws:amplify:us-east-1:000000000000:apps/d123"),
208 Some(("d123".to_string(), None))
209 );
210 assert_eq!(
211 parse_resource_arn("arn:aws:amplify:us-east-1:000000000000:apps/d123/branches/main"),
212 Some(("d123".to_string(), Some("main".to_string())))
213 );
214 assert_eq!(parse_resource_arn("arn:aws:s3:::bucket"), None);
215 }
216}