use super::policy::{Id, Kind, Reason, Refusal, Verdict, names_scheme};
const LENGTH: usize = 21;
pub(crate) fn classify(token: &str, key: Option<&str>) -> Verdict {
if !is_random_looking(token) {
return Verdict::Ignored;
}
let base64url = token.bytes().any(|byte| byte == b'-' || byte == b'_');
if !base64url && !names_scheme(key, "nanoid") {
return Verdict::Refused(Refusal::new(
None,
Reason::AmbiguousKind,
"21 base62 characters are a NanoID, a short token and a truncated hash in equal \
measure; only a `-` or `_`, or a key naming the scheme, separates them"
.to_string(),
));
}
Verdict::Named(Id::of(Kind::NanoId))
}
fn is_random_looking(token: &str) -> bool {
token.len() == LENGTH
&& token.bytes().any(|byte| byte.is_ascii_digit())
&& token.bytes().any(|byte| byte.is_ascii_uppercase())
&& token.bytes().any(|byte| byte.is_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
const BASE64URL: &str = "V1StGXR8_Z5jdHi6B-myT";
const BASE62: &str = "V1StGXR8xZ5jdHi6BxmyT";
fn refused(token: &str, key: Option<&str>) -> Refusal {
match classify(token, key) {
Verdict::Refused(refusal) => refusal,
other => panic!("{token} was {other:?}, not a refusal"),
}
}
#[test]
fn a_base64url_character_is_the_evidence() {
assert_eq!(
classify(BASE64URL, None),
Verdict::Named(Id::of(Kind::NanoId))
);
let refusal = refused(BASE62, None);
assert_eq!(refusal.reason, Reason::AmbiguousKind);
assert_eq!(refusal.kind, None, "no kind was named");
}
#[test]
fn either_base64url_character_settles_it() {
assert_eq!(
classify("V1StGXR8_Z5jdHi6BxmyT", None),
Verdict::Named(Id::of(Kind::NanoId))
);
assert_eq!(
classify("V1StGXR8xZ5jdHi6B-myT", None),
Verdict::Named(Id::of(Kind::NanoId))
);
}
#[test]
fn a_key_naming_the_scheme_settles_a_base62_run() {
assert_eq!(
classify(BASE62, Some("session.nanoId")),
Verdict::Named(Id::of(Kind::NanoId))
);
assert_eq!(
refused(BASE62, Some("session.token")).reason,
Reason::AmbiguousKind
);
}
#[test]
fn only_the_leaf_of_the_key_path_names_the_scheme() {
assert_eq!(
refused(BASE62, Some("nanoids.count")).reason,
Reason::AmbiguousKind
);
assert_eq!(
classify(BASE62, Some("nanoids.session_nanoid")),
Verdict::Named(Id::of(Kind::NanoId))
);
}
#[test]
fn a_nanoid_carries_no_version_and_no_timestamp() {
let Verdict::Named(id) = classify(BASE64URL, None) else {
panic!("a NanoID");
};
assert_eq!(id.version, None);
assert_eq!(id.variant, None);
assert_eq!(id.timestamp, None, "a NanoID has no clock in it");
}
#[test]
fn hand_written_names_of_the_same_length_are_ignored() {
for token in [
"retry-after-2-seconds",
"SOME_CONSTANT_NAME_ER",
"abcdefghijklmnopqrstu",
"handler_for_the_thing",
] {
assert_eq!(classify(token, None), Verdict::Ignored, "{token}");
}
}
#[test]
fn the_wrong_length_is_not_a_nanoid() {
assert_eq!(classify("V1StGXR8_Z5jdHi6B-my", None), Verdict::Ignored);
assert_eq!(classify("V1StGXR8_Z5jdHi6B-myTx", None), Verdict::Ignored);
}
}