minikv 2.0.1

Distributed key-value and object store with Raft-replicated metadata, two-phase commit to volume servers, and write-ahead logs.
//! Common utilities for encoding, formatting, and tracing.

use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

/// Percent-encoding set for keys (includes /, %, and control chars)
const KEY_ENCODE_SET: &AsciiSet = &CONTROLS
    .add(b'/')
    .add(b'%')
    .add(b' ')
    .add(b'?')
    .add(b'#')
    .add(b'&');

pub fn encode_key(key: &str) -> String {
    utf8_percent_encode(key, KEY_ENCODE_SET).to_string()
}

#[allow(clippy::result_large_err)]
pub fn decode_key(encoded: &str) -> crate::Result<String> {
    percent_decode_str(encoded)
        .decode_utf8()
        .map(|s| s.to_string())
        .map_err(|e| crate::Error::Other(format!("Failed to decode key: {}", e)))
}

pub fn format_bytes(bytes: u64) -> String {
    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
    let mut size = bytes as f64;
    let mut unit_idx = 0;

    while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
        size /= 1024.0;
        unit_idx += 1;
    }

    format!("{:.2} {}", size, UNITS[unit_idx])
}

/// Parse duration string (e.g., "30s", "5m", "1h", "7d")
#[allow(clippy::result_large_err)]
pub fn parse_duration(s: &str) -> crate::Result<std::time::Duration> {
    let s = s.trim();
    if s.is_empty() {
        return Err(crate::Error::InvalidConfig("empty duration".into()));
    }

    let (num_str, unit) = if let Some(stripped) = s.strip_suffix("ms") {
        (stripped, "ms")
    } else {
        let _unit = s.chars().last().unwrap();
        (&s[..s.len() - 1], &s[s.len() - 1..])
    };

    let num: u64 = num_str
        .parse()
        .map_err(|_| crate::Error::InvalidConfig(format!("invalid duration: {}", s)))?;

    let duration = match unit {
        "ms" => std::time::Duration::from_millis(num),
        "s" => std::time::Duration::from_secs(num),
        "m" => std::time::Duration::from_secs(num * 60),
        "h" => std::time::Duration::from_secs(num * 3600),
        "d" => std::time::Duration::from_secs(num * 86400),
        _ => {
            return Err(crate::Error::InvalidConfig(format!(
                "unknown duration unit: {}",
                unit
            )))
        }
    };

    Ok(duration)
}

pub fn timestamp_now() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs()
}

pub fn timestamp_now_millis() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NodeState {
    Alive,
    Suspect,
    Dead,
    Draining,
}

impl NodeState {
    pub fn is_healthy(&self) -> bool {
        matches!(self, NodeState::Alive)
    }

    pub fn can_write(&self) -> bool {
        matches!(self, NodeState::Alive)
    }

    pub fn can_read(&self) -> bool {
        matches!(self, NodeState::Alive | NodeState::Draining)
    }
}

impl std::fmt::Display for NodeState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NodeState::Alive => write!(f, "alive"),
            NodeState::Suspect => write!(f, "suspect"),
            NodeState::Dead => write!(f, "dead"),
            NodeState::Draining => write!(f, "draining"),
        }
    }
}

pub async fn retry_with_backoff<F, Fut, T>(
    mut f: F,
    max_retries: usize,
    initial_delay: std::time::Duration,
) -> crate::Result<T>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = crate::Result<T>>,
{
    let mut delay = initial_delay;

    for attempt in 0..max_retries {
        match f().await {
            Ok(result) => return Ok(result),
            Err(e) if e.is_retryable() && attempt < max_retries - 1 => {
                tracing::warn!(
                    "Retry attempt {} failed: {}, retrying in {:?}",
                    attempt + 1,
                    e,
                    delay
                );
                tokio::time::sleep(delay).await;
                delay *= 2;
            }
            Err(e) => return Err(e),
        }
    }

    Err(crate::Error::Internal("Max retries exceeded".into()))
}

pub fn generate_upload_id() -> String {
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(0);

    let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
    let timestamp = timestamp_now_millis();
    format!("{}-{}", timestamp, counter)
}

pub fn crc32(data: &[u8]) -> u32 {
    crc32fast::hash(data)
}

#[allow(clippy::result_large_err)]
pub fn validate_key(key: &str) -> crate::Result<()> {
    if key.is_empty() {
        return Err(crate::Error::InvalidConfig("key cannot be empty".into()));
    }

    if key.len() > 1024 {
        return Err(crate::Error::InvalidConfig(
            "key too long (max 1024 bytes)".into(),
        ));
    }

    if key.chars().any(|c| c.is_control()) {
        return Err(crate::Error::InvalidConfig(
            "key contains invalid characters".into(),
        ));
    }

    Ok(())
}

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

    #[test]
    fn test_encode_decode_key() {
        let key = "my/path/to/file.txt";
        let encoded = encode_key(key);
        assert!(encoded.contains("%2F"));

        let decoded = decode_key(&encoded).unwrap();
        assert_eq!(decoded, key);
    }
}