nu_plugin_secret 0.7.0

Production-grade secret handling plugin for Nushell with secure CustomValue types that prevent accidental exposure of sensitive data
Documentation
//! Memory optimization utilities for the secret plugin
//!
//! This module contains optimizations to reduce memory usage and improve
//! performance while maintaining security guarantees.

// Removed unnecessary wrapper functions that only added indirection
// Use crate::redaction functions directly instead

/// Memory pool for small allocations
/// This reduces allocation overhead for small secret values
pub struct SecretMemoryPool {
    small_strings: Vec<String>,
    medium_strings: Vec<String>,
    large_strings: Vec<String>,
}

impl SecretMemoryPool {
    const SMALL_SIZE: usize = 64;
    const MEDIUM_SIZE: usize = 1024;
    const POOL_INITIAL_CAPACITY: usize = 16;

    pub fn new() -> Self {
        Self {
            small_strings: Vec::with_capacity(Self::POOL_INITIAL_CAPACITY),
            medium_strings: Vec::with_capacity(Self::POOL_INITIAL_CAPACITY),
            large_strings: Vec::with_capacity(Self::POOL_INITIAL_CAPACITY),
        }
    }

    /// Get a pre-allocated string from the pool or create a new one
    pub fn get_string(&mut self, size_hint: usize) -> String {
        if size_hint <= Self::SMALL_SIZE {
            self.small_strings
                .pop()
                .unwrap_or_else(|| String::with_capacity(Self::SMALL_SIZE))
        } else if size_hint <= Self::MEDIUM_SIZE {
            self.medium_strings
                .pop()
                .unwrap_or_else(|| String::with_capacity(Self::MEDIUM_SIZE))
        } else {
            self.large_strings
                .pop()
                .unwrap_or_else(|| String::with_capacity(size_hint))
        }
    }

    /// Return a string to the pool for reuse
    pub fn return_string(&mut self, mut s: String) {
        // Clear content but keep capacity
        s.clear();

        let capacity = s.capacity();
        if capacity <= Self::SMALL_SIZE && self.small_strings.len() < Self::POOL_INITIAL_CAPACITY {
            self.small_strings.push(s);
        } else if capacity <= Self::MEDIUM_SIZE
            && self.medium_strings.len() < Self::POOL_INITIAL_CAPACITY
        {
            self.medium_strings.push(s);
        } else if self.large_strings.len() < Self::POOL_INITIAL_CAPACITY {
            self.large_strings.push(s);
        }
        // If pools are full, just drop the string
    }
}

impl Default for SecretMemoryPool {
    fn default() -> Self {
        Self::new()
    }
}

/// Optimize binary data storage for common patterns
pub mod binary_optimization {
    use std::borrow::Cow;
    use zeroize::Zeroize;

    /// Common binary patterns that can be stored more efficiently
    #[derive(Clone)]
    pub enum OptimizedBinary {
        /// All zeros pattern - store just the length
        Zeros(usize),
        /// All ones pattern - store just the length  
        Ones(usize),
        /// Repeating byte pattern
        Repeated(u8, usize),
        /// Small binary data (inline storage)
        Small([u8; 32], usize),
        /// Large binary data (heap allocated)
        Large(Vec<u8>),
    }

    impl OptimizedBinary {
        /// Create optimized binary from a slice
        pub fn from_slice(data: &[u8]) -> Self {
            let len = data.len();

            if len == 0 {
                return Self::Small([0; 32], 0);
            }

            // Check for common patterns
            let first_byte = data[0];

            if data.iter().all(|&b| b == 0) {
                Self::Zeros(len)
            } else if data.iter().all(|&b| b == 0xFF) {
                Self::Ones(len)
            } else if data.iter().all(|&b| b == first_byte) {
                Self::Repeated(first_byte, len)
            } else if len <= 32 {
                let mut small = [0; 32];
                small[..len].copy_from_slice(data);
                Self::Small(small, len)
            } else {
                Self::Large(data.to_vec())
            }
        }

        /// Get the data as a Cow (clone-on-write)
        pub fn as_bytes(&self) -> Cow<'_, [u8]> {
            match self {
                Self::Zeros(len) => Cow::Owned(vec![0; *len]),
                Self::Ones(len) => Cow::Owned(vec![0xFF; *len]),
                Self::Repeated(byte, len) => Cow::Owned(vec![*byte; *len]),
                Self::Small(data, len) => Cow::Borrowed(&data[..*len]),
                Self::Large(data) => Cow::Borrowed(data),
            }
        }

        /// Get the length without reconstructing the data
        pub fn len(&self) -> usize {
            match self {
                Self::Zeros(len) | Self::Ones(len) | Self::Repeated(_, len) => *len,
                Self::Small(_, len) => *len,
                Self::Large(data) => data.len(),
            }
        }

        /// Check if empty
        pub fn is_empty(&self) -> bool {
            self.len() == 0
        }
    }

    impl Zeroize for OptimizedBinary {
        fn zeroize(&mut self) {
            match self {
                Self::Zeros(_) | Self::Ones(_) | Self::Repeated(_, _) => {
                    // These patterns don't contain actual secret data
                }
                Self::Small(data, len) => {
                    // Zero the actual used portion
                    data[..*len].zeroize();
                    *len = 0;
                }
                Self::Large(data) => {
                    data.zeroize();
                }
            }
        }
    }
}

/// Memory usage statistics for monitoring
#[derive(Debug, Clone)]
pub struct MemoryStats {
    pub total_secrets: usize,
    pub string_secrets: usize,
    pub binary_secrets: usize,
    pub record_secrets: usize,
    pub list_secrets: usize,
    pub estimated_memory_kb: usize,
}

impl MemoryStats {
    pub fn new() -> Self {
        Self {
            total_secrets: 0,
            string_secrets: 0,
            binary_secrets: 0,
            record_secrets: 0,
            list_secrets: 0,
            estimated_memory_kb: 0,
        }
    }

    pub fn add_string_secret(&mut self, size: usize) {
        self.total_secrets += 1;
        self.string_secrets += 1;
        // Round up to ensure non-zero for small allocations
        self.estimated_memory_kb += (size + std::mem::size_of::<String>()).div_ceil(1024);
    }

    pub fn add_binary_secret(&mut self, size: usize) {
        self.total_secrets += 1;
        self.binary_secrets += 1;
        // Round up to ensure non-zero for small allocations
        self.estimated_memory_kb += (size + std::mem::size_of::<Vec<u8>>()).div_ceil(1024);
    }

    pub fn memory_efficiency_ratio(&self) -> f64 {
        if self.total_secrets == 0 {
            return 1.0;
        }
        // Simple heuristic: secrets should be efficient compared to plain storage
        let baseline_kb = self.total_secrets * 64 / 1024; // Assume 64 bytes per secret baseline
        if baseline_kb == 0 {
            1.0
        } else {
            baseline_kb as f64 / self.estimated_memory_kb.max(1) as f64
        }
    }
}

impl Default for MemoryStats {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_redacted_string_templating() {
        let s1 = crate::redaction::get_cached_redacted_string(None, "string");
        let s2 = crate::redaction::get_cached_redacted_string(None, "string");

        // Should return correct Tera-templated format
        assert_eq!(s1, "<redacted:string>");
        assert_eq!(s2, "<redacted:string>");

        // Test other types
        assert_eq!(
            crate::redaction::get_cached_redacted_string(None, "int"),
            "<redacted:int>"
        );
        assert_eq!(
            crate::redaction::get_cached_redacted_string(None, "float"),
            "<redacted:float>"
        );
        assert_eq!(
            crate::redaction::get_cached_redacted_string(None, "bool"),
            "<redacted:bool>"
        );
    }

    #[test]
    fn test_binary_optimization() {
        use binary_optimization::OptimizedBinary;

        // Test zeros optimization
        let zeros = vec![0; 1000];
        let opt_zeros = OptimizedBinary::from_slice(&zeros);
        assert!(matches!(opt_zeros, OptimizedBinary::Zeros(1000)));
        assert_eq!(opt_zeros.len(), 1000);

        // Test ones optimization
        let ones = vec![0xFF; 500];
        let opt_ones = OptimizedBinary::from_slice(&ones);
        assert!(matches!(opt_ones, OptimizedBinary::Ones(500)));

        // Test repeated pattern optimization
        let repeated = vec![0xAA; 200];
        let opt_repeated = OptimizedBinary::from_slice(&repeated);
        assert!(matches!(opt_repeated, OptimizedBinary::Repeated(0xAA, 200)));

        // Test small data optimization
        let small_data = vec![1, 2, 3, 4, 5];
        let opt_small = OptimizedBinary::from_slice(&small_data);
        assert!(matches!(opt_small, OptimizedBinary::Small(_, 5)));
    }

    #[test]
    fn test_memory_pool() {
        let mut pool = SecretMemoryPool::new();

        // Get some strings
        let s1 = pool.get_string(32);
        let s2 = pool.get_string(100);
        let s3 = pool.get_string(2000);

        assert!(s1.capacity() >= 32);
        assert!(s2.capacity() >= 100);
        assert!(s3.capacity() >= 2000);

        // Return them to the pool
        pool.return_string(s1);
        pool.return_string(s2);
        pool.return_string(s3);

        // Get them back (should reuse)
        let s4 = pool.get_string(30);
        assert!(s4.capacity() >= 30);
    }

    #[test]
    fn test_memory_stats() {
        let mut stats = MemoryStats::new();
        assert_eq!(stats.total_secrets, 0);

        stats.add_string_secret(100);
        stats.add_binary_secret(200);

        assert_eq!(stats.total_secrets, 2);
        assert_eq!(stats.string_secrets, 1);
        assert_eq!(stats.binary_secrets, 1);
        assert!(stats.estimated_memory_kb > 0);
    }

    #[test]
    fn test_configurable_redacted_string_with_value() {
        use crate::config::RedactionContext;

        // Test without configuration - should fallback to static strings
        let result = crate::redaction::get_redacted_string_with_value(
            "string",
            RedactionContext::Display,
            Some("secret_value"),
        );
        assert!(result.contains("redacted"));

        // Test with None value - should still return redacted text
        let result = crate::redaction::get_redacted_string_with_value::<String>(
            "string",
            RedactionContext::Display,
            None,
        );
        assert!(result.contains("redacted"));
    }

    #[test]
    fn test_configurable_redacted_string_with_generic_value() {
        use crate::config::RedactionContext;

        // Test with integer
        let result = crate::redaction::get_redacted_string_with_value(
            "int",
            RedactionContext::Display,
            Some(&42),
        );
        assert!(result.contains("redacted"));

        // Test with boolean
        let result = crate::redaction::get_redacted_string_with_value(
            "bool",
            RedactionContext::Display,
            Some(&true),
        );
        assert!(result.contains("redacted"));

        // Test with float
        let result = crate::redaction::get_redacted_string_with_value(
            "float",
            RedactionContext::Display,
            Some(&2.5),
        );
        assert!(result.contains("redacted"));
    }

    #[test]
    fn test_redacted_string_with_length() {
        // Test the new length-aware function
        let result = crate::redaction::get_redacted_string_with_length("password", Some(12));
        assert_eq!(result, "<redacted:password>");

        // Test without length
        let result = crate::redaction::get_redacted_string_with_length("token", None);
        assert_eq!(result, "<redacted:token>");
    }
}