Skip to content

g1t/crates/contracts/src/names.rs

151 lines5,136 bytesCodeBlame
1/// The site's own routes: first path segments that are never a workspace's,
2/// so nobody may register them, and no alias can be reached at them.
3const ROUTES: &[&str] = &[
4 "api",
5 "mcp",
6 "login",
7 "logout",
8 "register",
9 "new",
10 "settings",
11 "search",
12 "admin",
13 "auth",
14 // g1t.sh/integrations/github/setup: where GitHub returns after an install.
15 "integrations",
16 "pulls",
17 "issues",
18 "oauth",
19 "assets",
20 "avatars",
21 "docs",
22 "explore",
23 "about",
24 "pricing",
25 "terms",
26 "privacy",
27 "policies",
28 "security",
29 "help",
30 "support",
31 "status",
32 "blog",
33 "verify",
34 "forgot",
35 "reset",
36 "device",
37 "workspaces",
38 "u",
39 // g1t.sh/invite/<code>: invite links; and the waitlist.
40 "invite",
41 "invites",
42 "waitlist",
43 // g1t.sh/v2/: the container registry; `-` paths hold the others.
44 "v2",
45 // g1t.sh/inbox, and the name it might also go by.
46 "inbox",
47 "notifications",
48];
49
50/// g1t itself, and the name its agent once went by: everything g1t does is
51/// shown as `g1t`, so nobody else may be called either. Not routes: staff
52/// may point one at a workspace as an alias (identity's `aliases.rs`).
53const OWN: &[&str] = &["g1t", "g1t-agent"];
54
55/// Whether `value`, whatever its case, is a name nobody can register or
56/// rename a workspace to: a route, or g1t's own.
57pub fn is_reserved_name(value: &str) -> bool {
58 is_route_name(value) || OWN.iter().any(|own| own.eq_ignore_ascii_case(value.trim()))
59}
60
61/// Whether `value`, whatever its case, is one of the site's own routes.
62pub fn is_route_name(value: &str) -> bool {
63 let value = value.trim();
64 ROUTES.iter().any(|route| route.eq_ignore_ascii_case(value))
65}
66
67/// Whether `value` has a namespace's shape: letters, digits and single
68/// hyphens, not starting or ending with a hyphen, at most 39 characters.
69/// Reserved names have it too; see [`is_valid_namespace`].
70pub fn is_namespace_shaped(value: &str) -> bool {
71 let bytes = value.as_bytes();
72 !bytes.is_empty()
73 && bytes.len() <= 39
74 && bytes
75 .iter()
76 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
77 && !value.starts_with('-')
78 && !value.ends_with('-')
79 && !value.contains("--")
80}
81
82/// A workspace alias as staff typed it, trimmed and lowercased, if it can
83/// be one: shaped like a namespace and not one of the site's routes, which
84/// would always answer first. Reserved names such as `g1t` can be; whether
85/// a person or workspace already has it is identity's to check.
86pub fn aliasable_name(value: &str) -> Option<String> {
87 let value = value.trim().to_lowercase();
88 (is_namespace_shaped(&value) && !is_route_name(&value)).then_some(value)
89}
90
91/// A username or workspace slug as someone typed it, trimmed and
92/// lowercased, if it can be registered: what signing up, signing up with
93/// GitHub and creating a workspace each take. None when it is malformed or
94/// reserved.
95pub fn claimable_namespace(value: &str) -> Option<String> {
96 let value = value.trim().to_lowercase();
97 is_valid_namespace(&value).then_some(value)
98}
99
100/// Namespaces follow GitHub's rules: letters, digits and single hyphens,
101/// not starting or ending with a hyphen, at most 39 characters.
102pub fn is_valid_namespace(value: &str) -> bool {
103 is_namespace_shaped(value) && !is_reserved_name(value)
104}
105
106pub fn is_valid_repo_name(value: &str) -> bool {
107 !value.is_empty()
108 && value.len() <= 100
109 && value.bytes().all(|byte| {
110 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
111 })
112 && !value.starts_with('.')
113 && !value.ends_with(".git")
114 // `/<workspace>/-/…` holds the workspace's own pages.
115 && value != "-"
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn g1ts_own_names_are_reserved_in_any_case() {
124 for name in ["g1t", "G1T", "g1t-agent", "G1T-Agent", " g1t "] {
125 assert!(is_reserved_name(name), "{name}");
126 assert_eq!(claimable_namespace(name), None, "{name}");
127 }
128 assert!(!is_valid_namespace("g1t"));
129 assert!(!is_valid_namespace("g1t-agent"));
130 }
131
132 #[test]
133 fn names_near_g1ts_are_anyones() {
134 assert!(!is_reserved_name("g1t-agents"));
135 assert_eq!(claimable_namespace("G1T-Bot").as_deref(), Some("g1t-bot"));
136 assert_eq!(claimable_namespace(" Ana ").as_deref(), Some("ana"));
137 assert_eq!(claimable_namespace("an--a"), None);
138 }
139
140 #[test]
141 fn g1t_can_be_an_alias_but_a_route_cannot() {
142 assert_eq!(aliasable_name(" G1T ").as_deref(), Some("g1t"));
143 assert_eq!(aliasable_name("acme-corp").as_deref(), Some("acme-corp"));
144 for name in ["settings", "api", "login", "Explore", "-acme", "ac--me", "acme_inc", "", "a.b"] {
145 assert_eq!(aliasable_name(name), None, "{name}");
146 }
147 assert_eq!(aliasable_name(&"a".repeat(40)), None);
148 // Still nobody's to register.
149 assert!(is_reserved_name("g1t") && !is_route_name("g1t"));
150 }
151}