naru-config 0.7.0

A security-first configuration manager with encryption and audit logging
Documentation
use std::path::Path;

pub fn is_symlink(path: &Path) -> bool {
    match std::fs::symlink_metadata(path) {
        Ok(meta) => meta.file_type().is_symlink(),
        Err(_) => false,
    }
}

pub fn resolve_and_validate_path(path: &Path, base_dir: &Path) -> Result<PathBuf, &'static str> {
    if is_symlink(path) {
        let resolved = std::fs::read_link(path).map_err(|_| "Cannot resolve symlink")?;

        let resolved_abs = if resolved.is_absolute() {
            resolved
        } else {
            if let Some(parent) = path.parent() {
                parent.join(&resolved)
            } else {
                resolved
            }
        };

        let canonical_base =
            std::fs::canonicalize(base_dir).map_err(|_| "Cannot access base directory")?;
        let canonical_resolved =
            std::fs::canonicalize(&resolved_abs).map_err(|_| "Cannot resolve symlink target")?;

        if !canonical_resolved.starts_with(&canonical_base) {
            return Err("Symlink points outside allowed directory");
        }

        return Ok(canonical_resolved);
    }

    std::fs::canonicalize(path).map_err(|_| "Cannot access path")
}

pub fn check_file_size(path: &Path, max_size: u64) -> Result<(), &'static str> {
    let metadata = std::fs::metadata(path).map_err(|_| "Could not get file metadata")?;

    if metadata.len() > max_size {
        return Err("File exceeds maximum allowed size");
    }

    Ok(())
}

pub fn is_file_readable(path: &Path) -> bool {
    std::fs::read_to_string(path).is_ok()
}

pub fn is_file_writable(path: &Path) -> bool {
    if path.exists() {
        std::fs::metadata(path)
            .map(|m| m.permissions().readonly() == false)
            .unwrap_or(false)
    } else {
        if let Some(parent) = path.parent() {
            parent.exists() && is_file_writable(parent)
        } else {
            false
        }
    }
}

pub fn get_file_size(path: &Path) -> Result<u64, &'static str> {
    std::fs::metadata(path)
        .map(|m| m.len())
        .map_err(|_| "Could not get file metadata")
}

use std::path::PathBuf;

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

    #[test]
    fn test_check_file_size() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        std::fs::write(&file_path, "some data").unwrap();

        assert!(check_file_size(&file_path, 100).is_ok());
        assert!(check_file_size(&file_path, 5).is_err());
    }

    #[test]
    fn test_get_file_size() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        std::fs::write(&file_path, "test data").unwrap();

        let size = get_file_size(&file_path).unwrap();
        assert!(size > 0);
    }

    #[test]
    fn test_is_file_readable() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        std::fs::write(&file_path, "content").unwrap();

        assert!(is_file_readable(&file_path));
    }

    #[test]
    fn test_is_symlink() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("file.txt");
        let link_path = temp_dir.path().join("link.txt");

        std::fs::write(&file_path, "content").unwrap();

        #[cfg(unix)]
        std::os::unix::fs::symlink(&file_path, &link_path).unwrap();

        #[cfg(not(unix))]
        {
            use std::process::Command;
            let _ = Command::new("cmd")
                .args(&[
                    "/c",
                    "mklink",
                    &link_path.to_string_lossy(),
                    &file_path.to_string_lossy(),
                ])
                .output();
        }

        assert!(is_symlink(&link_path) || !link_path.exists());
    }
}