Skip to content

Commit 652c12b

Browse files
authored
Merge pull request #5569 from Turbo87/crate-scope
Implement `CrateScope` struct
2 parents c20c096 + 9be6942 commit 652c12b

File tree

2 files changed

+134
-1
lines changed

2 files changed

+134
-1
lines changed

src/models/token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct ApiToken {
2727
pub revoked: bool,
2828
/// `None` or a list of crate scope patterns (see RFC #2947)
2929
#[serde(skip)]
30-
pub crate_scopes: Option<Vec<String>>,
30+
pub crate_scopes: Option<Vec<scopes::CrateScope>>,
3131
/// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947)
3232
#[serde(skip)]
3333
pub endpoint_scopes: Option<Vec<scopes::EndpointScope>>,

src/models/token/scopes.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::models::Crate;
12
use diesel::deserialize::{self, FromSql};
23
use diesel::pg::Pg;
34
use diesel::serialize::{self, IsNull, Output, ToSql};
@@ -50,3 +51,135 @@ impl FromSql<Text, Pg> for EndpointScope {
5051
Ok(EndpointScope::try_from(not_none!(bytes))?)
5152
}
5253
}
54+
55+
#[derive(Clone, Debug, PartialEq, Eq)]
56+
pub struct CrateScope {
57+
pattern: String,
58+
}
59+
60+
impl TryFrom<&str> for CrateScope {
61+
type Error = String;
62+
63+
fn try_from(pattern: &str) -> Result<Self, Self::Error> {
64+
match CrateScope::is_valid_pattern(pattern) {
65+
true => Ok(CrateScope {
66+
pattern: pattern.to_string(),
67+
}),
68+
false => Err("Invalid crate scope pattern".to_string()),
69+
}
70+
}
71+
}
72+
73+
impl TryFrom<String> for CrateScope {
74+
type Error = String;
75+
76+
fn try_from(pattern: String) -> Result<Self, Self::Error> {
77+
match CrateScope::is_valid_pattern(&pattern) {
78+
true => Ok(CrateScope { pattern }),
79+
false => Err("Invalid crate scope pattern".to_string()),
80+
}
81+
}
82+
}
83+
84+
impl FromSql<Text, Pg> for CrateScope {
85+
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
86+
let value = <String as FromSql<Text, Pg>>::from_sql(bytes)?;
87+
Ok(CrateScope::try_from(value)?)
88+
}
89+
}
90+
91+
impl ToSql<Text, Pg> for CrateScope {
92+
fn to_sql<W: Write>(&self, out: &mut Output<'_, W, Pg>) -> serialize::Result {
93+
<String as ToSql<Text, Pg>>::to_sql(&self.pattern, out)
94+
}
95+
}
96+
97+
impl CrateScope {
98+
fn is_valid_pattern(pattern: &str) -> bool {
99+
if pattern.is_empty() {
100+
return false;
101+
}
102+
103+
if pattern == "*" {
104+
return true;
105+
}
106+
107+
let name_without_wildcard = pattern.strip_suffix('*').unwrap_or(pattern);
108+
Crate::valid_name(name_without_wildcard)
109+
}
110+
111+
pub fn matches(&self, crate_name: &str) -> bool {
112+
let canonicalize = |name: &str| name.replace('-', "_");
113+
114+
if self.pattern == "*" {
115+
return true;
116+
}
117+
118+
return match self.pattern.strip_suffix('*') {
119+
Some(prefix) => {
120+
crate_name.starts_with(prefix)
121+
|| canonicalize(crate_name).starts_with(&canonicalize(prefix))
122+
}
123+
None => {
124+
crate_name == self.pattern
125+
|| canonicalize(crate_name) == canonicalize(&self.pattern)
126+
}
127+
};
128+
}
129+
}
130+
131+
#[cfg(test)]
132+
mod tests {
133+
use super::*;
134+
135+
#[test]
136+
fn crate_scope_validation() {
137+
assert_ok!(CrateScope::try_from("foo"));
138+
139+
// wildcards
140+
assert_ok!(CrateScope::try_from("foo*"));
141+
assert_ok!(CrateScope::try_from("f*"));
142+
assert_ok!(CrateScope::try_from("*"));
143+
assert_err!(CrateScope::try_from("te*st"));
144+
145+
// hyphens and underscores
146+
assert_ok!(CrateScope::try_from("foo-bar"));
147+
assert_ok!(CrateScope::try_from("foo_bar"));
148+
149+
// empty string
150+
assert_err!(CrateScope::try_from(""));
151+
152+
// invalid characters
153+
assert_err!(CrateScope::try_from("test#"));
154+
}
155+
156+
#[test]
157+
fn crate_scope_matching() {
158+
let scope = |pattern: &str| CrateScope::try_from(pattern).unwrap();
159+
160+
assert!(scope("foo").matches("foo"));
161+
assert!(!scope("foo").matches("bar"));
162+
assert!(!scope("foo").matches("fo"));
163+
assert!(!scope("foo").matches("fooo"));
164+
165+
// wildcards
166+
assert!(scope("foo*").matches("foo"));
167+
assert!(!scope("foo*").matches("bar"));
168+
assert!(scope("foo*").matches("foo-bar"));
169+
assert!(scope("foo*").matches("foo_bar"));
170+
assert!(scope("f*").matches("foo"));
171+
assert!(scope("*").matches("foo"));
172+
173+
// hyphens and underscores
174+
assert!(!scope("foo").matches("foo-bar"));
175+
assert!(!scope("foo").matches("foo_bar"));
176+
assert!(scope("foo-bar").matches("foo-bar"));
177+
assert!(scope("foo-bar").matches("foo_bar"));
178+
assert!(scope("foo_bar").matches("foo-bar"));
179+
assert!(scope("foo_bar").matches("foo_bar"));
180+
assert!(scope("foo-*").matches("foo-bar"));
181+
assert!(scope("foo-*").matches("foo_bar"));
182+
assert!(scope("foo_*").matches("foo-bar"));
183+
assert!(scope("foo_*").matches("foo_bar"));
184+
}
185+
}

0 commit comments

Comments
 (0)