rust-pipe 0.1.1

Lightweight typed task dispatch from Rust to polyglot workers (TypeScript, Python, Go, Java, C#, Ruby, Elixir, Swift, PHP)
Documentation
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
    #[error(
        "Invalid worker ID '{value}': must be alphanumeric with dots, hyphens, or underscores"
    )]
    InvalidWorkerId { value: String },
    #[error("Invalid Docker image name '{value}'")]
    InvalidImageName { value: String },
    #[error("Invalid hostname '{value}'")]
    InvalidHostname { value: String },
    #[error("Invalid username '{value}'")]
    InvalidUsername { value: String },
    #[error("Dangerous value rejected '{value}': {reason}")]
    DangerousValue { value: String, reason: String },
}

fn is_safe_identifier_char(c: char) -> bool {
    c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_'
}

pub fn validate_worker_id(id: &str) -> Result<(), ValidationError> {
    if id.is_empty()
        || id.len() > 128
        || !id.starts_with(|c: char| c.is_ascii_alphanumeric())
        || !id.chars().all(is_safe_identifier_char)
    {
        return Err(ValidationError::InvalidWorkerId {
            value: id.to_string(),
        });
    }
    Ok(())
}

pub fn validate_docker_image(image: &str) -> Result<(), ValidationError> {
    if image.is_empty() || image.len() > 256 {
        return Err(ValidationError::InvalidImageName {
            value: image.to_string(),
        });
    }
    let valid = image.chars().all(|c| {
        c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' || c == '/' || c == ':'
    });
    if !valid || !image.starts_with(|c: char| c.is_ascii_alphanumeric()) || image.contains("..") {
        return Err(ValidationError::InvalidImageName {
            value: image.to_string(),
        });
    }
    Ok(())
}

pub fn validate_hostname(host: &str) -> Result<(), ValidationError> {
    if host.is_empty()
        || host.len() > 253
        || !host.starts_with(|c: char| c.is_ascii_alphanumeric())
        || !host
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
    {
        return Err(ValidationError::InvalidHostname {
            value: host.to_string(),
        });
    }
    Ok(())
}

pub fn validate_username(user: &str) -> Result<(), ValidationError> {
    if user.is_empty()
        || user.len() > 64
        || !user.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
        || !user
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
    {
        return Err(ValidationError::InvalidUsername {
            value: user.to_string(),
        });
    }
    Ok(())
}

pub fn validate_no_shell_metacharacters(
    value: &str,
    field_name: &str,
) -> Result<(), ValidationError> {
    const DANGEROUS: &[char] = &[
        '`', '$', '(', ')', '{', '}', ';', '|', '&', '<', '>', '\n', '\r', '\0',
    ];
    if value.chars().any(|c| DANGEROUS.contains(&c)) {
        return Err(ValidationError::DangerousValue {
            value: value.to_string(),
            reason: format!("'{}' contains shell metacharacters", field_name),
        });
    }
    Ok(())
}

pub fn validate_file_path(path: &str, field_name: &str) -> Result<(), ValidationError> {
    validate_no_shell_metacharacters(path, field_name)?;
    if path.contains("..") {
        return Err(ValidationError::DangerousValue {
            value: path.to_string(),
            reason: format!("'{}' contains path traversal", field_name),
        });
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_valid_worker_ids() {
        assert!(validate_worker_id("worker-1").is_ok());
        assert!(validate_worker_id("my.worker_v2").is_ok());
        assert!(validate_worker_id("abc123").is_ok());
    }

    #[test]
    fn test_invalid_worker_ids() {
        assert!(validate_worker_id("").is_err());
        assert!(validate_worker_id("-bad").is_err());
        assert!(validate_worker_id("has space").is_err());
        assert!(validate_worker_id("has;semicolon").is_err());
        assert!(validate_worker_id("$(inject)").is_err());
    }

    #[test]
    fn test_valid_docker_images() {
        assert!(validate_docker_image("nginx:latest").is_ok());
        assert!(validate_docker_image("registry.io/org/image:v1.2.3").is_ok());
        assert!(validate_docker_image("ubuntu").is_ok());
    }

    #[test]
    fn test_invalid_docker_images() {
        assert!(validate_docker_image("").is_err());
        assert!(validate_docker_image("--privileged").is_err());
        assert!(validate_docker_image("img;rm -rf /").is_err());
        assert!(validate_docker_image("../escape").is_err());
    }

    #[test]
    fn test_valid_hostnames() {
        assert!(validate_hostname("example.com").is_ok());
        assert!(validate_hostname("10.0.0.1").is_ok());
        assert!(validate_hostname("my-host.internal").is_ok());
    }

    #[test]
    fn test_invalid_hostnames() {
        assert!(validate_hostname("").is_err());
        assert!(validate_hostname("-bad.com").is_err());
        assert!(validate_hostname("host;evil").is_err());
    }

    #[test]
    fn test_valid_usernames() {
        assert!(validate_username("root").is_ok());
        assert!(validate_username("deploy_user").is_ok());
        assert!(validate_username("_service").is_ok());
    }

    #[test]
    fn test_invalid_usernames() {
        assert!(validate_username("").is_err());
        assert!(validate_username("-bad").is_err());
        assert!(validate_username("user;evil").is_err());
        assert!(validate_username("$(whoami)").is_err());
    }

    #[test]
    fn test_shell_metacharacter_rejection() {
        assert!(validate_no_shell_metacharacters("safe-value_123", "test").is_ok());
        assert!(validate_no_shell_metacharacters("$(inject)", "test").is_err());
        assert!(validate_no_shell_metacharacters("a;b", "test").is_err());
        assert!(validate_no_shell_metacharacters("a|b", "test").is_err());
        assert!(validate_no_shell_metacharacters("a`b`", "test").is_err());
    }

    #[test]
    fn test_path_traversal_rejection() {
        assert!(validate_file_path("/home/user/.ssh/id_rsa", "key").is_ok());
        assert!(validate_file_path("../../etc/passwd", "key").is_err());
        assert!(validate_file_path("/path/$(cmd)/file", "key").is_err());
    }
}