use crate::error::AppError;
pub use vta_sdk::identifier::MAX_IDENTIFIER_LEN;
pub fn validate_identifier(label: &str, value: &str) -> Result<(), AppError> {
vta_sdk::identifier::validate_identifier(label, value).map_err(|e| AppError::Validation(e.0))
}
pub const MAX_DID_LEN: usize = 1024;
pub fn validate_did(label: &str, value: &str) -> Result<(), AppError> {
if value.is_empty() {
return Err(AppError::Validation(format!("{label} must not be empty")));
}
if value.len() > MAX_DID_LEN {
return Err(AppError::Validation(format!(
"{label} is {} bytes; maximum is {MAX_DID_LEN}",
value.len()
)));
}
if !value.starts_with("did:") {
return Err(AppError::Validation(format!(
"{label} is not a DID (must start with 'did:')"
)));
}
for (i, ch) in value.chars().enumerate() {
let ok = ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_' | ':' | '%');
if !ok {
return Err(AppError::Validation(format!(
"{label} contains an invalid character {ch:?} at position {i}"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_common_identifier_shapes() {
for ok in [
"myapp",
"My-App_1",
"context.v2",
"a",
"0",
"didcomm-mediator",
"_private",
"CamelCase",
] {
validate_identifier("id", ok).unwrap_or_else(|e| panic!("{ok:?} rejected: {e:?}"));
}
}
#[test]
fn rejects_empty() {
let err = validate_identifier("id", "").expect_err("empty must be rejected");
assert!(matches!(err, AppError::Validation(_)), "got {err:?}");
}
#[test]
fn rejects_separator_injection() {
for bad in [
"global:evil", "../../etc", "a:b:c", "my/ctx", "with space", "tab\there", "with\nnewline", "null\0byte", "unicode:§§", "quote\"injected", ] {
validate_identifier("id", bad).expect_err(&format!("{bad:?} must be rejected"));
}
}
#[test]
fn rejects_too_long() {
let long = "a".repeat(MAX_IDENTIFIER_LEN + 1);
let err = validate_identifier("id", &long).expect_err("overlong must be rejected");
assert!(matches!(err, AppError::Validation(_)), "got {err:?}");
}
#[test]
fn accepts_exactly_at_limit() {
let edge = "a".repeat(MAX_IDENTIFIER_LEN);
validate_identifier("id", &edge).expect("exactly-at-limit must pass");
}
#[test]
fn error_message_names_the_field() {
let err = validate_identifier("context_id", "bad:id").expect_err("rejected");
let msg = format!("{err:?}");
assert!(
msg.contains("context_id"),
"error must name the field it is validating — got {msg}"
);
}
#[test]
fn validate_did_accepts_real_dids() {
for ok in [
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"did:webvh:QmSCID:example.com",
"did:web:example.com:user:alice",
"did:peer:2.Ez6LS",
] {
validate_did("did", ok).unwrap_or_else(|e| panic!("{ok:?} rejected: {e:?}"));
}
}
#[test]
fn validate_did_rejects_malicious_or_malformed() {
for bad in [
"", "z6MkNotADid", "did:key:z\0null", "did:key:a/b", "did:key:a b", "did:key:tab\there", "did:key:§§", ] {
validate_did("did", bad).expect_err(&format!("{bad:?} must be rejected"));
}
let long = format!("did:key:{}", "a".repeat(MAX_DID_LEN));
validate_did("did", &long).expect_err("overlong must be rejected");
}
}