use crate::CryptoError;
use crate::error::FormatDefect;
pub(crate) const TYPE_NAME_MAX_LEN: usize = 255;
const RESERVED_NATIVE_PREFIXES: &[&str] = &["mlkem", "pq", "hpke", "tag", "xwing", "kem"];
const RESERVED_NATIVE_SUFFIX: &str = "tag";
pub(crate) fn validate_type_name_grammar(name: &str) -> Result<(), CryptoError> {
let malformed = || CryptoError::InvalidFormat(FormatDefect::MalformedTypeName);
let bytes = name.as_bytes();
if bytes.is_empty() || bytes.len() > TYPE_NAME_MAX_LEN {
return Err(malformed());
}
for &b in bytes {
let allowed = matches!(
b,
b'a'..=b'z' | b'0'..=b'9' | b'.' | b'_' | b'+' | b'-' | b'/'
);
if !allowed {
return Err(malformed());
}
}
let first = bytes[0];
let last = bytes[bytes.len() - 1];
for &edge in &[first, last] {
if matches!(edge, b'.' | b'_' | b'+' | b'-' | b'/') {
return Err(malformed());
}
}
for window in bytes.windows(2) {
if window == b".." || window == b"//" {
return Err(malformed());
}
}
Ok(())
}
pub(crate) fn is_reserved_native_name(name: &str) -> bool {
!name.contains('/')
&& (RESERVED_NATIVE_PREFIXES.iter().any(|p| name.starts_with(p))
|| name.ends_with(RESERVED_NATIVE_SUFFIX))
}
#[allow(dead_code)]
pub(crate) fn validate_external_type_name(name: &str) -> Result<(), CryptoError> {
validate_type_name_grammar(name)?;
if !name.contains('/') || is_reserved_native_name(name) {
return Err(CryptoError::InvalidFormat(FormatDefect::MalformedTypeName));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::recipient::native::{argon2id, x25519};
#[test]
fn validate_type_name_grammar_accepts_canonical_natives() {
validate_type_name_grammar(argon2id::TYPE_NAME).unwrap();
validate_type_name_grammar(x25519::TYPE_NAME).unwrap();
}
#[test]
fn validate_type_name_grammar_accepts_fqn_plugin_names() {
validate_type_name_grammar("example.com/enigma").unwrap();
validate_type_name_grammar("com.example/foo").unwrap();
validate_type_name_grammar("a.b.c/d").unwrap();
}
#[test]
fn validate_type_name_grammar_rejects_empty() {
match validate_type_name_grammar("") {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedTypeName)) => {}
other => panic!("expected MalformedTypeName for empty, got {other:?}"),
}
}
#[test]
fn validate_type_name_grammar_rejects_overlong() {
let long = "a".repeat(256);
match validate_type_name_grammar(&long) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedTypeName)) => {}
other => panic!("expected MalformedTypeName for 256-byte name, got {other:?}"),
}
}
#[test]
fn validate_type_name_grammar_accepts_max_length() {
let max = "a".repeat(255);
validate_type_name_grammar(&max).unwrap();
}
#[test]
fn validate_type_name_grammar_rejects_uppercase() {
match validate_type_name_grammar("Argon2id") {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedTypeName)) => {}
other => panic!("expected MalformedTypeName for uppercase, got {other:?}"),
}
}
#[test]
fn validate_type_name_grammar_rejects_invalid_characters() {
for bad in &["foo bar", "foo!", "foo:bar", "foo*", "foo\nbar"] {
match validate_type_name_grammar(bad) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedTypeName)) => {}
other => panic!("expected MalformedTypeName for `{bad}`, got {other:?}"),
}
}
}
#[test]
fn validate_type_name_grammar_rejects_edge_punctuation() {
for bad in &[
".foo", "_foo", "+foo", "-foo", "/foo", "foo.", "foo_", "foo+", "foo-", "foo/",
] {
match validate_type_name_grammar(bad) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedTypeName)) => {}
other => panic!("expected MalformedTypeName for `{bad}`, got {other:?}"),
}
}
}
#[test]
fn validate_type_name_grammar_rejects_consecutive_punctuation() {
for bad in &["foo..bar", "foo//bar", "a.b..c", "a/b//c"] {
match validate_type_name_grammar(bad) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedTypeName)) => {}
other => panic!("expected MalformedTypeName for `{bad}`, got {other:?}"),
}
}
}
#[test]
fn is_reserved_native_name_covers_all_reserved_prefixes_and_tag_suffix() {
for reserved in &[
"mlkem", "mlkem768", "pq", "pqfoo", "hpke", "hpkex", "tag", "tagfoo", "xwing",
"xwing256", "kem", "kem768", "footag", "mytag",
] {
assert!(
is_reserved_native_name(reserved),
"expected `{reserved}` to be reserved"
);
}
for not_reserved in &["argon2id", "x25519", "future", "ed25519"] {
assert!(
!is_reserved_native_name(not_reserved),
"expected `{not_reserved}` to be unreserved"
);
}
for qualified in &["example.com/mlkem768", "example.com/footag"] {
assert!(
!is_reserved_native_name(qualified),
"qualified `{qualified}` MUST NOT be reserved"
);
}
}
#[test]
fn external_type_name_rejects_unqualified_native_looking_names() {
for bad in ["future", "mlkem768", "pqfoo", "xwing", "kem768", "mytag"] {
assert!(validate_external_type_name(bad).is_err(), "{bad}");
}
assert!(validate_external_type_name("example.com/future").is_ok());
}
}