offline-license-validator 0.1.1

Offline license validation library using Ed25519 signatures
Documentation
use std::fs;
use std::path::PathBuf;

use crate::types::ValidLicenseInfo;

/// Get the license storage directory
///
/// Returns `~/.offline-license/` on Unix or `%USERPROFILE%\.offline-license\` on Windows
fn get_license_directory() -> Result<PathBuf, std::io::Error> {
    let home_dir = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .map_err(|_| std::io::Error::new(std::io::ErrorKind::NotFound, "Home directory not found"))?;

    let license_dir = PathBuf::from(home_dir).join(".offline-license");

    // Create directory if it doesn't exist
    if !license_dir.exists() {
        fs::create_dir_all(&license_dir)?;
    }

    Ok(license_dir)
}

/// Save validated license to disk
///
/// License is saved to `~/.offline-license/license.json`
pub fn save_license(license_info: &ValidLicenseInfo, license_string: &str) -> Result<(), std::io::Error> {
    let license_dir = get_license_directory()?;
    let license_path = license_dir.join("license.json");

    let data = serde_json::json!({
        "license_string": license_string,
        "license_type": license_info.license_type,
        "issued_at": license_info.issued_at,
        "expires_at": license_info.expires_at,
        "hwid": license_info.hwid,
        "metadata": license_info.metadata,
        "validated_at": chrono::Utc::now(),
    });

    let json = serde_json::to_string_pretty(&data)?;
    fs::write(license_path, json)?;

    Ok(())
}

/// Load saved license from disk
///
/// Returns `None` if no license is saved
pub fn load_saved_license() -> Result<Option<(String, ValidLicenseInfo)>, std::io::Error> {
    let license_dir = get_license_directory()?;
    let license_path = license_dir.join("license.json");

    if !license_path.exists() {
        return Ok(None);
    }

    let content = fs::read_to_string(license_path)?;
    let data: serde_json::Value = serde_json::from_str(&content)?;

    let license_string = data["license_string"]
        .as_str()
        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing license_string"))?
        .to_string();

    let license_info = ValidLicenseInfo {
        license_type: data["license_type"]
            .as_str()
            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing license_type"))?
            .to_string(),
        issued_at: serde_json::from_value(data["issued_at"].clone())
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?,
        expires_at: serde_json::from_value(data["expires_at"].clone())
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?,
        hwid: data["hwid"]
            .as_str()
            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing hwid"))?
            .to_string(),
        metadata: data["metadata"].clone().into(),
    };

    Ok(Some((license_string, license_info)))
}

/// Delete saved license from disk
pub fn delete_saved_license() -> Result<(), std::io::Error> {
    let license_dir = get_license_directory()?;
    let license_path = license_dir.join("license.json");

    if license_path.exists() {
        fs::remove_file(license_path)?;
    }

    Ok(())
}

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

    #[test]
    fn test_license_directory() {
        let dir = get_license_directory().unwrap();
        assert!(dir.to_string_lossy().contains(".offline-license"));
    }

    #[test]
    fn test_save_and_load_license() {
        let license_info = ValidLicenseInfo {
            license_type: "PRO".to_string(),
            issued_at: Utc::now(),
            expires_at: Utc::now() + chrono::Duration::days(365),
            hwid: "test-hwid".to_string(),
            metadata: None,
        };

        let license_string = "test_license_string";

        // Save
        save_license(&license_info, license_string).unwrap();

        // Load
        let loaded = load_saved_license().unwrap();
        assert!(loaded.is_some());

        let (loaded_string, loaded_info) = loaded.unwrap();
        assert_eq!(loaded_string, license_string);
        assert_eq!(loaded_info.license_type, "PRO");

        // Cleanup
        delete_saved_license().unwrap();
    }
}