pub const MAX_STRING_LEN: usize = 512;
pub const MAX_NOTES_LEN: usize = 4096;
pub const MAX_JSON_LEN: usize = 65_536;
pub const MAX_INFERENCE_CONTENT_LEN: usize = 32_768;
pub fn validate_string_len(field: &str, value: &str, max: usize) -> Result<(), String> {
if value.len() > max {
Err(format!("{}_too_long: {} chars (max {})", field, value.len(), max))
} else {
Ok(())
}
}
pub fn validate_range_i64(field: &str, value: i64, min: i64, max: i64) -> Result<(), String> {
if value < min || value > max {
Err(format!(
"{}_out_of_range: {} (expected {}..={})",
field, value, min, max
))
} else {
Ok(())
}
}
pub fn validate_range_u64(field: &str, value: u64, min: u64, max: u64) -> Result<(), String> {
if value < min || value > max {
Err(format!(
"{}_out_of_range: {} (expected {}..={})",
field, value, min, max
))
} else {
Ok(())
}
}
pub fn validate_non_empty(field: &str, value: &str) -> Result<(), String> {
if value.is_empty() {
Err(format!("{}_required", field))
} else {
Ok(())
}
}
pub fn validate_range_i32(field: &str, value: i32, min: i32, max: i32) -> Result<(), String> {
if value < min || value > max {
Err(format!(
"{}_out_of_range: {} (expected {}..={})",
field, value, min, max
))
} else {
Ok(())
}
}
pub fn sanitize_inference_input(input: &str, max_len: usize) -> Result<String, String> {
if input.len() > max_len {
return Err(format!(
"inference_input_too_long: {} chars (max {})",
input.len(),
max_len
));
}
let sanitized: String = input
.chars()
.filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
.collect();
Ok(sanitized)
}
pub const MAX_ID_LEN: usize = 128;
pub fn validate_entity_id(field: &str, value: &str) -> Result<(), String> {
if value.is_empty() {
return Err(format!("validation_empty:{field}"));
}
if value.len() > MAX_ID_LEN {
return Err(format!(
"validation_too_long:{field} (max {MAX_ID_LEN}, got {})",
value.len()
));
}
if !value.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
return Err(format!(
"validation_invalid_chars:{field} (alphanumeric, underscore, hyphen only)"
));
}
Ok(())
}
pub fn validate_topology_id(field: &str, value: &str) -> Result<(), String> {
validate_entity_id(field, value)
}
pub const ENTITY_COMPONENT_TABLES: &[&str] = &[
"positions",
"vitals",
"attributes",
"wallets",
"progressions",
"alignments",
"combat_states",
"equipment",
"survival_states",
"active_layers",
"npc_behaviors",
"pvp_states",
"vocations",
"mount_states",
"npc_combat_states",
"collision_profiles",
"agent_accounts",
];
pub fn validate_topology_chain(world_id: &str, region_id: &str, area_id: &str) -> Result<(), String> {
validate_topology_id("world_id", world_id)?;
validate_topology_id("region_id", region_id)?;
validate_topology_id("area_id", area_id)?;
Ok(())
}
pub fn validate_scope_key(scope_key: &str) -> Result<(), String> {
if scope_key.is_empty() {
return Ok(());
}
for segment in scope_key.split('/') {
if segment.is_empty() {
return Err("empty segment in scope_key".into());
}
let parts: Vec<&str> = segment.splitn(2, ':').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
return Err(format!("segment '{}' has empty key or value", segment));
}
let key = parts[0];
let value = parts[1];
if !key
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
return Err(format!("invalid key chars in '{}'", key));
}
if !value
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(format!("invalid value chars in '{}'", value));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_len_ok_within_limit() {
assert!(validate_string_len("name", "hello", 10).is_ok());
}
#[test]
fn string_len_ok_exact_limit() {
assert!(validate_string_len("name", "abc", 3).is_ok());
}
#[test]
fn string_len_err_over_limit() {
let err = validate_string_len("name", "abcd", 3).unwrap_err();
assert!(err.contains("name_too_long"));
assert!(err.contains("4 chars"));
assert!(err.contains("max 3"));
}
#[test]
fn string_len_ok_empty() {
assert!(validate_string_len("field", "", 0).is_ok());
}
#[test]
fn string_len_max_string_len_constant() {
let s = "a".repeat(MAX_STRING_LEN);
assert!(validate_string_len("f", &s, MAX_STRING_LEN).is_ok());
let s2 = "a".repeat(MAX_STRING_LEN + 1);
assert!(validate_string_len("f", &s2, MAX_STRING_LEN).is_err());
}
#[test]
fn range_i64_within() {
assert!(validate_range_i64("hp", 50, 0, 100).is_ok());
}
#[test]
fn range_i64_at_min() {
assert!(validate_range_i64("hp", 0, 0, 100).is_ok());
}
#[test]
fn range_i64_at_max() {
assert!(validate_range_i64("hp", 100, 0, 100).is_ok());
}
#[test]
fn range_i64_below_min() {
let err = validate_range_i64("hp", -1, 0, 100).unwrap_err();
assert!(err.contains("hp_out_of_range"));
}
#[test]
fn range_i64_above_max() {
let err = validate_range_i64("hp", 101, 0, 100).unwrap_err();
assert!(err.contains("hp_out_of_range"));
}
#[test]
fn range_i64_negative_range() {
assert!(validate_range_i64("temp", -50, -100, -10).is_ok());
assert!(validate_range_i64("temp", -5, -100, -10).is_err());
}
#[test]
fn range_u64_within() {
assert!(validate_range_u64("id", 50, 0, 100).is_ok());
}
#[test]
fn range_u64_at_bounds() {
assert!(validate_range_u64("id", 0, 0, 100).is_ok());
assert!(validate_range_u64("id", 100, 0, 100).is_ok());
}
#[test]
fn range_u64_above_max() {
let err = validate_range_u64("id", 101, 0, 100).unwrap_err();
assert!(err.contains("id_out_of_range"));
}
#[test]
fn range_u64_below_min() {
let err = validate_range_u64("id", 0, 1, 100).unwrap_err();
assert!(err.contains("id_out_of_range"));
}
#[test]
fn range_i32_within() {
assert!(validate_range_i32("level", 5, 1, 99).is_ok());
}
#[test]
fn range_i32_at_bounds() {
assert!(validate_range_i32("level", 1, 1, 99).is_ok());
assert!(validate_range_i32("level", 99, 1, 99).is_ok());
}
#[test]
fn range_i32_below_min() {
let err = validate_range_i32("level", 0, 1, 99).unwrap_err();
assert!(err.contains("level_out_of_range"));
}
#[test]
fn range_i32_above_max() {
let err = validate_range_i32("level", 100, 1, 99).unwrap_err();
assert!(err.contains("level_out_of_range"));
}
#[test]
fn non_empty_ok() {
assert!(validate_non_empty("name", "hello").is_ok());
}
#[test]
fn non_empty_err() {
let err = validate_non_empty("name", "").unwrap_err();
assert!(err.contains("name_required"));
}
#[test]
fn non_empty_whitespace_is_ok() {
assert!(validate_non_empty("name", " ").is_ok());
}
#[test]
fn sanitize_ok_normal_input() {
let result = sanitize_inference_input("hello world", 100).unwrap();
assert_eq!(result, "hello world");
}
#[test]
fn sanitize_strips_control_chars() {
let input = "hello\x00world\x07";
let result = sanitize_inference_input(input, 100).unwrap();
assert_eq!(result, "helloworld");
}
#[test]
fn sanitize_preserves_newlines_and_tabs() {
let input = "line1\nline2\tindented";
let result = sanitize_inference_input(input, 100).unwrap();
assert_eq!(result, "line1\nline2\tindented");
}
#[test]
fn sanitize_err_too_long() {
let input = "a".repeat(101);
let err = sanitize_inference_input(&input, 100).unwrap_err();
assert!(err.contains("inference_input_too_long"));
assert!(err.contains("101 chars"));
assert!(err.contains("max 100"));
}
#[test]
fn sanitize_exact_max_len() {
let input = "a".repeat(100);
assert!(sanitize_inference_input(&input, 100).is_ok());
}
#[test]
fn sanitize_empty_input() {
let result = sanitize_inference_input("", 100).unwrap();
assert_eq!(result, "");
}
#[test]
fn sanitize_max_inference_content_len_constant() {
assert_eq!(MAX_INFERENCE_CONTENT_LEN, 32_768);
}
#[test]
fn scope_key_empty_ok() {
assert!(validate_scope_key("").is_ok());
}
#[test]
fn scope_key_single_segment() {
assert!(validate_scope_key("world:ayora").is_ok());
}
#[test]
fn scope_key_multiple_segments() {
assert!(validate_scope_key("world:ayora/region:stormveil/area:market").is_ok());
}
#[test]
fn scope_key_numeric_values() {
assert!(validate_scope_key("layer:3/id:12345").is_ok());
}
#[test]
fn scope_key_value_with_dashes_and_dots() {
assert!(validate_scope_key("ns:com.example.pack-v1").is_ok());
}
#[test]
fn scope_key_value_rejects_colons() {
assert!(validate_scope_key("ns:com.example.pack-v1:beta").is_err());
}
#[test]
fn scope_key_underscore_in_key() {
assert!(validate_scope_key("world_id:ayora").is_ok());
}
#[test]
fn scope_key_err_empty_segment() {
let err = validate_scope_key("world:ayora//area:market").unwrap_err();
assert!(err.contains("empty segment"));
}
#[test]
fn scope_key_err_no_colon() {
let err = validate_scope_key("worldayora").unwrap_err();
assert!(err.contains("empty key or value"));
}
#[test]
fn scope_key_err_empty_key() {
let err = validate_scope_key(":ayora").unwrap_err();
assert!(err.contains("empty key or value"));
}
#[test]
fn scope_key_err_empty_value() {
let err = validate_scope_key("world:").unwrap_err();
assert!(err.contains("empty key or value"));
}
#[test]
fn scope_key_err_uppercase_in_key() {
let err = validate_scope_key("World:ayora").unwrap_err();
assert!(err.contains("invalid key chars"));
}
#[test]
fn scope_key_err_special_chars_in_key() {
let err = validate_scope_key("world!:ayora").unwrap_err();
assert!(err.contains("invalid key chars"));
}
#[test]
fn scope_key_err_invalid_value_chars() {
let err = validate_scope_key("world:ayo ra").unwrap_err();
assert!(err.contains("invalid value chars"));
}
#[test]
fn scope_key_err_trailing_slash() {
let err = validate_scope_key("world:ayora/").unwrap_err();
assert!(err.contains("empty segment"));
}
#[test]
fn scope_key_err_leading_slash() {
let err = validate_scope_key("/world:ayora").unwrap_err();
assert!(err.contains("empty segment"));
}
#[test]
fn entity_id_valid_alphanumeric() {
assert!(validate_entity_id("id", "player123").is_ok());
}
#[test]
fn entity_id_valid_with_underscore() {
assert!(validate_entity_id("id", "npc_guard_01").is_ok());
}
#[test]
fn entity_id_valid_with_hyphen() {
assert!(validate_entity_id("id", "item-sword-42").is_ok());
}
#[test]
fn entity_id_valid_single_char() {
assert!(validate_entity_id("id", "x").is_ok());
}
#[test]
fn entity_id_valid_at_max_length() {
let id = "a".repeat(MAX_ID_LEN);
assert!(validate_entity_id("id", &id).is_ok());
}
#[test]
fn entity_id_err_empty() {
let err = validate_entity_id("entity_id", "").unwrap_err();
assert!(err.contains("validation_empty:entity_id"));
}
#[test]
fn entity_id_err_too_long() {
let id = "a".repeat(MAX_ID_LEN + 1);
let err = validate_entity_id("entity_id", &id).unwrap_err();
assert!(err.contains("validation_too_long:entity_id"));
assert!(err.contains("max 128"));
assert!(err.contains("got 129"));
}
#[test]
fn entity_id_err_spaces() {
let err = validate_entity_id("id", "player 1").unwrap_err();
assert!(err.contains("validation_invalid_chars:id"));
}
#[test]
fn entity_id_err_special_chars() {
let err = validate_entity_id("id", "item@sword").unwrap_err();
assert!(err.contains("validation_invalid_chars:id"));
}
#[test]
fn entity_id_err_dot() {
let err = validate_entity_id("id", "npc.guard").unwrap_err();
assert!(err.contains("validation_invalid_chars:id"));
}
#[test]
fn entity_id_err_slash() {
let err = validate_entity_id("id", "world/region").unwrap_err();
assert!(err.contains("validation_invalid_chars:id"));
}
#[test]
fn topology_id_delegates_to_entity_id() {
assert!(validate_topology_id("world_id", "ayora").is_ok());
assert!(validate_topology_id("region_id", "").is_err());
assert!(validate_topology_id("area_id", "market-district").is_ok());
}
#[test]
fn topology_chain_all_valid() {
assert!(validate_topology_chain("ayora", "stormveil", "market-01").is_ok());
}
#[test]
fn topology_chain_err_empty_world() {
let err = validate_topology_chain("", "stormveil", "market").unwrap_err();
assert!(err.contains("validation_empty:world_id"));
}
#[test]
fn topology_chain_err_empty_region() {
let err = validate_topology_chain("ayora", "", "market").unwrap_err();
assert!(err.contains("validation_empty:region_id"));
}
#[test]
fn topology_chain_err_empty_area() {
let err = validate_topology_chain("ayora", "stormveil", "").unwrap_err();
assert!(err.contains("validation_empty:area_id"));
}
#[test]
fn topology_chain_err_invalid_chars_in_region() {
let err = validate_topology_chain("ayora", "storm veil", "market").unwrap_err();
assert!(err.contains("validation_invalid_chars:region_id"));
}
#[test]
fn entity_component_tables_not_empty() {
assert!(!ENTITY_COMPONENT_TABLES.is_empty());
}
#[test]
fn entity_component_tables_contains_positions() {
assert!(ENTITY_COMPONENT_TABLES.contains(&"positions"));
}
#[test]
fn entity_component_tables_contains_vitals() {
assert!(ENTITY_COMPONENT_TABLES.contains(&"vitals"));
}
#[test]
fn entity_component_tables_count() {
assert_eq!(ENTITY_COMPONENT_TABLES.len(), 17);
}
#[test]
fn constants_are_reasonable() {
assert_eq!(MAX_STRING_LEN, 512);
assert_eq!(MAX_NOTES_LEN, 4096);
assert_eq!(MAX_JSON_LEN, 65_536);
assert_eq!(MAX_INFERENCE_CONTENT_LEN, 32_768);
}
}