use crate::error::DomainError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MemoryKey(String);
impl MemoryKey {
pub fn from_raw(s: &str) -> Result<Self, DomainError> {
if is_safe_memory_key(s) {
Ok(Self(s.to_string()))
} else {
Err(DomainError::UnsafeMemoryKey(s.to_string()))
}
}
pub fn shared() -> Self {
Self("shared".to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
fn is_safe_memory_key(s: &str) -> bool {
if s.is_empty() || s.len() > 64 {
return false;
}
let mut chars = s.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_alphanumeric() {
return false;
}
if s.contains("..") || s.contains('/') || s.contains('\\') {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-'))
}
impl std::fmt::Display for MemoryKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::DomainError;
#[test]
fn accepts_simple_alphanumeric() {
assert!(MemoryKey::from_raw("origa").is_ok());
}
#[test]
fn accepts_shared_keyword() {
assert_eq!(MemoryKey::from_raw("shared").unwrap().as_str(), "shared");
}
#[test]
fn accepts_dotted_and_dashed_names() {
assert!(MemoryKey::from_raw("my-project_v2").is_ok());
assert!(MemoryKey::from_raw("analog.finder").is_ok());
assert!(MemoryKey::from_raw("a1-b2.c3").is_ok());
}
#[test]
fn rejects_empty() {
assert!(matches!(
MemoryKey::from_raw(""),
Err(DomainError::UnsafeMemoryKey(_))
));
}
#[test]
fn rejects_dot_dot() {
assert!(MemoryKey::from_raw("..").is_err());
}
#[test]
fn rejects_path_traversal_with_slash() {
assert!(MemoryKey::from_raw("a/b").is_err());
assert!(MemoryKey::from_raw("/etc/passwd").is_err());
}
#[test]
fn rejects_path_traversal_with_backslash() {
assert!(MemoryKey::from_raw("a\\b").is_err());
}
#[test]
fn rejects_embedded_dot_dot() {
assert!(MemoryKey::from_raw("a..b").is_err());
}
#[test]
fn rejects_leading_dot() {
assert!(MemoryKey::from_raw(".hidden").is_err());
}
#[test]
fn rejects_leading_dash() {
assert!(MemoryKey::from_raw("-dash").is_err());
}
#[test]
fn rejects_spaces_and_special() {
assert!(MemoryKey::from_raw("hello world").is_err());
assert!(MemoryKey::from_raw("origa!").is_err());
}
#[test]
fn shared_default_is_canonical() {
assert_eq!(MemoryKey::shared().as_str(), "shared");
}
#[test]
fn display_matches_as_str() {
let key = MemoryKey::from_raw("origa").unwrap();
assert_eq!(key.to_string(), "origa");
}
}