1use std::fmt;
32use std::str::FromStr;
33
34use serde::de::{self, Deserializer, Visitor};
35use serde::{Deserialize, Serialize, Serializer};
36
37include!("generated/services.rs");
38
39pub const DOMAIN: &str = "origincloudapis.eu";
41
42#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
51pub struct FullName {
52 full: String,
54 service_len: usize,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
61#[non_exhaustive]
62pub enum FullNameError {
63 #[error("an empty string names no resource")]
65 Empty,
66 #[error(
68 "{0:?} is a URN; a resource is named by its full resource name, //{{service}}.{DOMAIN}/{{name}}"
69 )]
70 Urn(String),
71 #[error("{0:?} is a URL; a full resource name has no scheme, //{{service}}.{DOMAIN}/{{name}}")]
73 Scheme(String),
74 #[error("{0:?} is a relative resource name; a full one starts with //{{service}}.{DOMAIN}/")]
76 Relative(String),
77 #[error(
79 "{0:?} is an id; a resource is named by its full resource name, //{{service}}.{DOMAIN}/{{name}}"
80 )]
81 BareId(String),
82 #[error("{name:?} names no resource: {host} is not the host of one of the platform's APIs")]
84 UnknownService {
85 name: String,
87 host: String,
89 },
90 #[error(
92 "{name:?} names {host}; a full resource name has the service's global host, {service}.{DOMAIN}"
93 )]
94 NotGlobalHost {
95 name: String,
97 host: String,
99 service: String,
101 },
102 #[error("{name:?} carries the API version {version:?}; a full resource name has none")]
104 Version {
105 name: String,
107 version: String,
109 },
110 #[error("{0:?} has no relative name after its service")]
112 NoRelativeName(String),
113 #[error("{0:?} is a full resource name; a relative one is expected here")]
115 NotRelative(String),
116 #[error("{0:?} ends with a '/'")]
118 TrailingSlash(String),
119 #[error("{0:?} has an empty segment")]
121 EmptySegment(String),
122 #[error("{name:?} has the segment {segment:?}, which a resource name can't")]
124 DotSegment {
125 name: String,
127 segment: String,
129 },
130 #[error(
132 "{name:?} has the segment {segment:?}: a segment is letters, digits and '-', '.', '_' or '~'"
133 )]
134 InvalidSegment {
135 name: String,
137 segment: String,
139 },
140 #[error("{0:?} is not an API version, such as v1 or v1beta1")]
142 NotAVersion(String),
143}
144
145impl FullName {
146 pub fn parse(name: &str) -> Result<FullName, FullNameError> {
154 let Some(rest) = name.strip_prefix("//") else {
155 return Err(not_full(name));
156 };
157 let Some((host, relative)) = rest.split_once('/') else {
158 return Err(match service_of_host(name, rest) {
159 Ok(_) => FullNameError::NoRelativeName(name.to_owned()),
160 Err(e) => e,
161 });
162 };
163 let service = service_of_host(name, host)?;
164 if relative.is_empty() {
165 return Err(FullNameError::NoRelativeName(name.to_owned()));
166 }
167 check_relative(name, relative)?;
168 Ok(FullName {
169 full: name.to_owned(),
170 service_len: service.len(),
171 })
172 }
173
174 pub fn from_relative(service: &str, relative: &str) -> Result<FullName, FullNameError> {
183 if relative.starts_with("//") {
184 return Err(FullNameError::NotRelative(relative.to_owned()));
185 }
186 let host = format!("{service}.{DOMAIN}");
187 if !RESOURCE_SERVICES.contains(&service) {
188 return Err(FullNameError::UnknownService {
189 name: relative.to_owned(),
190 host,
191 });
192 }
193 if relative.is_empty() {
194 return Err(FullNameError::NoRelativeName(relative.to_owned()));
195 }
196 check_relative(relative, relative)?;
197 Ok(FullName {
198 full: format!("//{host}/{relative}"),
199 service_len: service.len(),
200 })
201 }
202
203 pub fn service(&self) -> &str {
205 &self.full[2..2 + self.service_len]
206 }
207
208 pub fn host(&self) -> &str {
210 &self.full[2..self.host_end()]
211 }
212
213 pub fn relative(&self) -> &str {
216 &self.full[self.host_end() + 1..]
217 }
218
219 pub fn as_str(&self) -> &str {
221 &self.full
222 }
223
224 pub fn url(&self, version: &str) -> Result<String, FullNameError> {
228 if !is_version(version) {
229 return Err(FullNameError::NotAVersion(version.to_owned()));
230 }
231 Ok(format!(
232 "https://{}/{version}/{}",
233 self.host(),
234 self.relative()
235 ))
236 }
237
238 pub fn principal(&self) -> Option<Principal<'_>> {
241 if self.service() != "iam" {
242 return None;
243 }
244 let segments: Vec<&str> = self.relative().split('/').collect();
245 match segments.as_slice() {
246 ["users", user] => Some(Principal::User { user }),
247 [
248 "organizations",
249 organization,
250 "serviceAccounts",
251 service_account,
252 ] => Some(Principal::ServiceAccount {
253 organization,
254 service_account,
255 }),
256 ["serviceAgents", service_agent] => Some(Principal::ServiceAgent { service_agent }),
257 ["services", service] => Some(Principal::Service { service }),
258 _ => None,
259 }
260 }
261
262 pub fn role(&self) -> Option<&str> {
266 if self.service() != "iam" {
267 return None;
268 }
269 self.relative()
270 .strip_prefix("roles/")
271 .filter(|role| !role.contains('/'))
272 }
273
274 pub fn of_role(role: &str) -> Result<FullName, FullNameError> {
277 if role.contains('/') {
278 return Err(FullNameError::InvalidSegment {
279 name: role.to_owned(),
280 segment: role.to_owned(),
281 });
282 }
283 FullName::from_relative("iam", &format!("roles/{role}"))
284 }
285
286 fn host_end(&self) -> usize {
288 2 + self.service_len + 1 + DOMAIN.len()
289 }
290}
291
292#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
298#[non_exhaustive]
299pub enum Principal<'a> {
300 User {
302 user: &'a str,
304 },
305 ServiceAccount {
308 organization: &'a str,
310 service_account: &'a str,
312 },
313 ServiceAgent {
316 service_agent: &'a str,
318 },
319 Service {
322 service: &'a str,
324 },
325}
326
327impl Principal<'_> {
328 pub fn full_name(&self) -> Result<FullName, FullNameError> {
330 let (relative, parts) = match *self {
331 Principal::User { user } => (format!("users/{user}"), vec![user]),
332 Principal::ServiceAccount {
333 organization,
334 service_account,
335 } => (
336 format!("organizations/{organization}/serviceAccounts/{service_account}"),
337 vec![organization, service_account],
338 ),
339 Principal::ServiceAgent { service_agent } => (
340 format!("serviceAgents/{service_agent}"),
341 vec![service_agent],
342 ),
343 Principal::Service { service } => (format!("services/{service}"), vec![service]),
344 };
345 if let Some(part) = parts.iter().find(|p| p.contains('/')) {
347 return Err(FullNameError::InvalidSegment {
348 name: relative.clone(),
349 segment: (*part).to_owned(),
350 });
351 }
352 FullName::from_relative("iam", &relative)
353 }
354}
355
356fn service_of_host<'h>(name: &str, host: &'h str) -> Result<&'h str, FullNameError> {
359 let code = host.strip_suffix(DOMAIN).and_then(|h| h.strip_suffix('.'));
360 if let Some(code) = code.filter(|c| RESOURCE_SERVICES.contains(c)) {
361 return Ok(code);
362 }
363 let front_door = code
366 .and_then(|h| h.split_once('.'))
367 .filter(|(_, location)| !location.is_empty())
368 .map(|(code, _)| code)
369 .filter(|code| RESOURCE_SERVICES.contains(code));
370 Err(match front_door {
371 Some(service) => FullNameError::NotGlobalHost {
372 name: name.to_owned(),
373 host: host.to_owned(),
374 service: service.to_owned(),
375 },
376 None => FullNameError::UnknownService {
377 name: name.to_owned(),
378 host: host.to_owned(),
379 },
380 })
381}
382
383fn not_full(name: &str) -> FullNameError {
385 let owned = name.to_owned();
386 if name.is_empty() {
387 FullNameError::Empty
388 } else if name
389 .get(..4)
390 .is_some_and(|p| p.eq_ignore_ascii_case("urn:"))
391 {
392 FullNameError::Urn(owned)
393 } else if name.contains("://") {
394 FullNameError::Scheme(owned)
395 } else if name.contains('/') {
396 FullNameError::Relative(owned)
397 } else {
398 FullNameError::BareId(owned)
399 }
400}
401
402fn check_relative(name: &str, relative: &str) -> Result<(), FullNameError> {
404 if relative.ends_with('/') {
405 return Err(FullNameError::TrailingSlash(name.to_owned()));
406 }
407 let mut segments = relative.split('/').peekable();
408 if let Some(first) = segments.peek()
409 && is_version(first)
410 {
411 return Err(FullNameError::Version {
412 name: name.to_owned(),
413 version: (*first).to_owned(),
414 });
415 }
416 for segment in segments {
417 check_segment(name, segment)?;
418 }
419 Ok(())
420}
421
422fn check_segment(name: &str, segment: &str) -> Result<(), FullNameError> {
425 if segment.is_empty() {
426 return Err(FullNameError::EmptySegment(name.to_owned()));
427 }
428 if segment == "." || segment == ".." {
429 return Err(FullNameError::DotSegment {
430 name: name.to_owned(),
431 segment: segment.to_owned(),
432 });
433 }
434 let unreserved = |c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_' | '~');
435 if !segment.chars().all(unreserved) {
436 return Err(FullNameError::InvalidSegment {
437 name: name.to_owned(),
438 segment: segment.to_owned(),
439 });
440 }
441 Ok(())
442}
443
444fn is_version(segment: &str) -> bool {
447 let Some(rest) = segment.strip_prefix('v') else {
448 return false;
449 };
450 let major_len = rest.chars().take_while(char::is_ascii_digit).count();
451 if major_len == 0 || rest.starts_with('0') {
452 return false;
453 }
454 let stability = &rest[major_len..];
455 let number = stability
456 .strip_prefix("alpha")
457 .or_else(|| stability.strip_prefix("beta"));
458 match number {
459 None => stability.is_empty(),
460 Some(number) => number.chars().all(|c| c.is_ascii_digit()),
461 }
462}
463
464pub const ANY_SERVICE: &str = "*";
467
468pub(crate) fn path_name<'v>(
477 own: &str,
478 services: &[&str],
479 field: &str,
480 value: &'v str,
481) -> Result<&'v str, String> {
482 let own = [own];
483 let services = if services.is_empty() {
484 &own[..]
485 } else {
486 services
487 };
488 let refused = |e: FullNameError| format!("{field} must be a name on {}: {e}", hosts(services));
489 if !value.starts_with("//") {
490 return match not_full(value) {
491 e @ (FullNameError::Urn(_) | FullNameError::Scheme(_)) => Err(refused(e)),
492 _ => Ok(value),
493 };
494 }
495 let name = FullName::parse(value).map_err(refused)?;
496 if !services.contains(&ANY_SERVICE) && !services.contains(&name.service()) {
497 return Err(format!(
498 "{field} must be a name on {}, got {value:?}, a name on {}",
499 hosts(services),
500 name.host()
501 ));
502 }
503 Ok(&value[value.len() - name.relative().len()..])
504}
505
506fn hosts(services: &[&str]) -> String {
510 if services.contains(&ANY_SERVICE) {
511 return "the host of one of the platform's APIs".to_owned();
512 }
513 let hosts: Vec<String> = services.iter().map(|s| format!("{s}.{DOMAIN}")).collect();
514 match hosts.as_slice() {
515 [one] => one.clone(),
516 _ => format!("one of {}", hosts.join(", ")),
517 }
518}
519
520impl fmt::Display for FullName {
521 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522 f.write_str(&self.full)
523 }
524}
525
526impl fmt::Debug for FullName {
527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528 f.debug_tuple("FullName").field(&self.full).finish()
529 }
530}
531
532impl FromStr for FullName {
533 type Err = FullNameError;
534
535 fn from_str(s: &str) -> Result<Self, Self::Err> {
536 FullName::parse(s)
537 }
538}
539
540impl AsRef<str> for FullName {
541 fn as_ref(&self) -> &str {
542 &self.full
543 }
544}
545
546impl From<FullName> for String {
547 fn from(name: FullName) -> String {
548 name.full
549 }
550}
551
552impl Serialize for FullName {
554 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
555 serializer.serialize_str(&self.full)
556 }
557}
558
559impl<'de> Deserialize<'de> for FullName {
562 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
563 struct FullNameVisitor;
564
565 impl Visitor<'_> for FullNameVisitor {
566 type Value = FullName;
567
568 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 f.write_str("a full resource name, //{service}.origincloudapis.eu/{name}")
570 }
571
572 fn visit_str<E: de::Error>(self, value: &str) -> Result<FullName, E> {
573 FullName::parse(value).map_err(E::custom)
574 }
575 }
576
577 deserializer.deserialize_str(FullNameVisitor)
578 }
579}
580
581#[cfg(test)]
582mod tests;