use chrono::{DateTime, Utc};
use semver::Version;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
pub const LICENSE_FORMAT_VERSION: u32 = 1;
pub const MIN_SUPPORTED_LICENSE_VERSION: u32 = 1;
pub const MAX_SUPPORTED_LICENSE_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LicensePayload {
#[serde(rename = "v")]
pub format_version: u32,
#[serde(rename = "id")]
pub license_id: String,
#[serde(rename = "customer")]
pub customer_id: String,
#[serde(rename = "customer_name", skip_serializing_if = "Option::is_none")]
pub customer_name: Option<String>,
#[serde(rename = "issued_at")]
pub issued_at: DateTime<Utc>,
#[serde(rename = "constraints")]
pub constraints: LicenseConstraints,
#[serde(rename = "metadata", skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl LicensePayload {
pub fn id(&self) -> &str {
&self.license_id
}
pub fn customer(&self) -> &str {
&self.customer_id
}
pub fn is_version_supported(&self) -> bool {
self.format_version >= MIN_SUPPORTED_LICENSE_VERSION
&& self.format_version <= MAX_SUPPORTED_LICENSE_VERSION
}
pub fn get_value(&self, key: &str) -> Option<&serde_json::Value> {
self.metadata.as_ref().and_then(|m| m.get(key))
}
pub fn get_value_or<'a>(
&'a self,
key: &str,
default: &'a serde_json::Value,
) -> &'a serde_json::Value {
self.get_value(key).unwrap_or(default)
}
pub fn get_string(&self, key: &str) -> Option<&str> {
self.get_value(key).and_then(|v| v.as_str())
}
pub fn get_string_or<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
self.get_string(key).unwrap_or(default)
}
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.get_value(key).and_then(|v| v.as_i64())
}
pub fn get_i64_or(&self, key: &str, default: i64) -> i64 {
self.get_i64(key).unwrap_or(default)
}
pub fn get_u64(&self, key: &str) -> Option<u64> {
self.get_value(key).and_then(|v| v.as_u64())
}
pub fn get_u64_or(&self, key: &str, default: u64) -> u64 {
self.get_u64(key).unwrap_or(default)
}
pub fn get_f64(&self, key: &str) -> Option<f64> {
self.get_value(key).and_then(|v| v.as_f64())
}
pub fn get_f64_or(&self, key: &str, default: f64) -> f64 {
self.get_f64(key).unwrap_or(default)
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.get_value(key).and_then(|v| v.as_bool())
}
pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
self.get_bool(key).unwrap_or(default)
}
pub fn get_array(&self, key: &str) -> Option<&Vec<serde_json::Value>> {
self.get_value(key).and_then(|v| v.as_array())
}
pub fn get_string_array(&self, key: &str) -> Option<Vec<&str>> {
self.get_array(key)
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
}
pub fn get_object(&self, key: &str) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.get_value(key).and_then(|v| v.as_object())
}
pub fn has_key(&self, key: &str) -> bool {
self.metadata
.as_ref()
.map(|m| m.contains_key(key))
.unwrap_or(false)
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.metadata.iter().flat_map(|m| m.keys())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct LicenseConstraints {
#[serde(rename = "expires_at", skip_serializing_if = "Option::is_none")]
pub expiration_date: Option<DateTime<Utc>>,
#[serde(rename = "valid_from", skip_serializing_if = "Option::is_none")]
pub valid_from: Option<DateTime<Utc>>,
#[serde(rename = "allowed_features", skip_serializing_if = "Option::is_none")]
pub allowed_features: Option<HashSet<String>>,
#[serde(rename = "denied_features", skip_serializing_if = "Option::is_none")]
pub denied_features: Option<HashSet<String>>,
#[serde(rename = "max_connections", skip_serializing_if = "Option::is_none")]
pub max_connections: Option<u32>,
#[serde(rename = "allowed_hostnames", skip_serializing_if = "Option::is_none")]
pub allowed_hostnames: Option<HashSet<String>>,
#[serde(
rename = "allowed_machine_ids",
skip_serializing_if = "Option::is_none"
)]
pub allowed_machine_ids: Option<HashSet<String>>,
#[serde(rename = "min_version", skip_serializing_if = "Option::is_none")]
pub minimum_software_version: Option<Version>,
#[serde(rename = "max_version", skip_serializing_if = "Option::is_none")]
pub maximum_software_version: Option<Version>,
#[serde(rename = "custom", skip_serializing_if = "Option::is_none")]
pub custom_constraints: Option<HashMap<String, serde_json::Value>>,
}
impl LicenseConstraints {
pub fn new() -> Self {
Self::default()
}
pub fn is_feature_allowed(&self, feature: &str) -> bool {
if let Some(ref denied) = self.denied_features {
if denied.contains(feature) {
return false;
}
}
match &self.allowed_features {
None => true, Some(allowed) => allowed.contains(feature),
}
}
pub fn is_hostname_allowed(&self, hostname: &str) -> bool {
match &self.allowed_hostnames {
None => true,
Some(allowed) => allowed.contains(hostname),
}
}
pub fn is_machine_id_allowed(&self, machine_id: &str) -> bool {
match &self.allowed_machine_ids {
None => true,
Some(allowed) => allowed.contains(machine_id),
}
}
pub fn check_version_compatibility(&self, version: &Version) -> Result<(), String> {
if let Some(ref min_version) = self.minimum_software_version {
if version < min_version {
return Err(format!(
"version {} is below minimum required version {}",
version, min_version
));
}
}
if let Some(ref max_version) = self.maximum_software_version {
if version > max_version {
return Err(format!(
"version {} exceeds maximum allowed version {}",
version, max_version
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedLicense {
#[serde(rename = "payload")]
pub encoded_payload: String,
#[serde(rename = "signature")]
pub encoded_signature: String,
}
impl SignedLicense {
pub fn new(encoded_payload: String, encoded_signature: String) -> Self {
Self {
encoded_payload,
encoded_signature,
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
#[derive(Debug, Clone, Default)]
pub struct ValidationContext {
pub current_time: Option<DateTime<Utc>>,
pub current_hostname: Option<String>,
pub current_machine_id: Option<String>,
pub current_software_version: Option<Version>,
pub current_connection_count: Option<u32>,
pub requested_features: Vec<String>,
pub custom_values: HashMap<String, serde_json::Value>,
}
impl ValidationContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_time(mut self, time: DateTime<Utc>) -> Self {
self.current_time = Some(time);
self
}
pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
self.current_hostname = Some(hostname.into());
self
}
pub fn with_machine_id(mut self, machine_id: impl Into<String>) -> Self {
self.current_machine_id = Some(machine_id.into());
self
}
pub fn with_software_version(mut self, version: Version) -> Self {
self.current_software_version = Some(version);
self
}
pub fn with_connection_count(mut self, count: u32) -> Self {
self.current_connection_count = Some(count);
self
}
pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
self.requested_features.push(feature.into());
self
}
pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.requested_features
.extend(features.into_iter().map(Into::into));
self
}
pub fn with_custom_value(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.custom_values.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub is_valid: bool,
pub payload: Option<LicensePayload>,
pub failures: Vec<crate::error::ValidationFailure>,
pub time_remaining: Option<chrono::Duration>,
pub allowed_features: Option<HashSet<String>>,
pub denied_features: Option<HashSet<String>>,
}
impl ValidationResult {
pub fn success(payload: LicensePayload) -> Self {
let time_remaining = payload
.constraints
.expiration_date
.map(|exp| exp.signed_duration_since(Utc::now()));
let allowed_features = payload.constraints.allowed_features.clone();
let denied_features = payload.constraints.denied_features.clone();
Self {
is_valid: true,
payload: Some(payload),
failures: Vec::new(),
time_remaining,
allowed_features,
denied_features,
}
}
pub fn failure(failures: Vec<crate::error::ValidationFailure>) -> Self {
Self {
is_valid: false,
payload: None,
failures,
time_remaining: None,
allowed_features: None,
denied_features: None,
}
}
pub fn add_failure(&mut self, failure: crate::error::ValidationFailure) {
self.is_valid = false;
self.failures.push(failure);
}
pub fn is_active(&self) -> bool {
self.is_valid
&& self
.time_remaining
.map(|d| d.num_seconds() > 0)
.unwrap_or(true)
}
pub fn days_remaining(&self) -> Option<i64> {
self.time_remaining.map(|d| d.num_days())
}
pub fn is_feature_allowed(&self, feature: &str) -> bool {
if !self.is_valid {
return false;
}
if let Some(ref denied) = self.denied_features {
if denied.contains(feature) {
return false;
}
}
match &self.allowed_features {
None => true,
Some(allowed) => allowed.contains(feature),
}
}
pub fn get_value(&self, key: &str) -> Option<&serde_json::Value> {
self.payload.as_ref().and_then(|p| p.get_value(key))
}
pub fn get_value_or<'a>(
&'a self,
key: &str,
default: &'a serde_json::Value,
) -> &'a serde_json::Value {
self.get_value(key).unwrap_or(default)
}
pub fn get_string(&self, key: &str) -> Option<&str> {
self.payload.as_ref().and_then(|p| p.get_string(key))
}
pub fn get_string_or<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
self.get_string(key).unwrap_or(default)
}
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.payload.as_ref().and_then(|p| p.get_i64(key))
}
pub fn get_i64_or(&self, key: &str, default: i64) -> i64 {
self.get_i64(key).unwrap_or(default)
}
pub fn get_u64(&self, key: &str) -> Option<u64> {
self.payload.as_ref().and_then(|p| p.get_u64(key))
}
pub fn get_u64_or(&self, key: &str, default: u64) -> u64 {
self.get_u64(key).unwrap_or(default)
}
pub fn get_f64(&self, key: &str) -> Option<f64> {
self.payload.as_ref().and_then(|p| p.get_f64(key))
}
pub fn get_f64_or(&self, key: &str, default: f64) -> f64 {
self.get_f64(key).unwrap_or(default)
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.payload.as_ref().and_then(|p| p.get_bool(key))
}
pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
self.get_bool(key).unwrap_or(default)
}
pub fn get_array(&self, key: &str) -> Option<&Vec<serde_json::Value>> {
self.payload.as_ref().and_then(|p| p.get_array(key))
}
pub fn get_string_array(&self, key: &str) -> Option<Vec<&str>> {
self.payload.as_ref().and_then(|p| p.get_string_array(key))
}
pub fn get_object(&self, key: &str) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.payload.as_ref().and_then(|p| p.get_object(key))
}
pub fn has_key(&self, key: &str) -> bool {
self.payload
.as_ref()
.map(|p| p.has_key(key))
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_license_constraints_feature_allowed() {
let mut constraints = LicenseConstraints::new();
assert!(constraints.is_feature_allowed("any_feature"));
constraints.allowed_features = Some(HashSet::from([
"feature_a".to_string(),
"feature_b".to_string(),
]));
assert!(constraints.is_feature_allowed("feature_a"));
assert!(!constraints.is_feature_allowed("feature_c"));
constraints.denied_features = Some(HashSet::from(["feature_a".to_string()]));
assert!(!constraints.is_feature_allowed("feature_a"));
assert!(constraints.is_feature_allowed("feature_b"));
}
#[test]
fn test_license_constraints_version_compatibility() {
let mut constraints = LicenseConstraints::new();
constraints.minimum_software_version = Some(Version::new(1, 0, 0));
constraints.maximum_software_version = Some(Version::new(2, 0, 0));
assert!(constraints
.check_version_compatibility(&Version::new(1, 5, 0))
.is_ok());
assert!(constraints
.check_version_compatibility(&Version::new(0, 9, 0))
.is_err());
assert!(constraints
.check_version_compatibility(&Version::new(2, 1, 0))
.is_err());
}
#[test]
fn test_validation_context_builder() {
let context = ValidationContext::new()
.with_hostname("server.example.com")
.with_software_version(Version::new(1, 2, 3))
.with_feature("premium")
.with_features(vec!["analytics", "reports"]);
assert_eq!(
context.current_hostname.as_deref(),
Some("server.example.com")
);
assert_eq!(
context.current_software_version,
Some(Version::new(1, 2, 3))
);
assert_eq!(context.requested_features.len(), 3);
}
#[test]
fn test_license_payload_version_check() {
let payload = LicensePayload {
format_version: LICENSE_FORMAT_VERSION,
license_id: "test".to_string(),
customer_id: "customer".to_string(),
customer_name: None,
issued_at: Utc::now(),
constraints: LicenseConstraints::new(),
metadata: None,
};
assert!(payload.is_version_supported());
}
#[test]
fn test_signed_license_json_roundtrip() {
let license = SignedLicense::new(
"encoded_payload".to_string(),
"encoded_signature".to_string(),
);
let json = license.to_json().unwrap();
let parsed = SignedLicense::from_json(&json).unwrap();
assert_eq!(license.encoded_payload, parsed.encoded_payload);
assert_eq!(license.encoded_signature, parsed.encoded_signature);
}
#[test]
fn test_license_payload_get_value() {
let mut metadata = HashMap::new();
metadata.insert("tier".to_string(), serde_json::json!("enterprise"));
metadata.insert("max_users".to_string(), serde_json::json!(100));
metadata.insert("is_beta".to_string(), serde_json::json!(true));
metadata.insert(
"modules".to_string(),
serde_json::json!(["core", "analytics"]),
);
let payload = LicensePayload {
format_version: LICENSE_FORMAT_VERSION,
license_id: "test".to_string(),
customer_id: "customer".to_string(),
customer_name: None,
issued_at: Utc::now(),
constraints: LicenseConstraints::new(),
metadata: Some(metadata),
};
assert!(payload.get_value("tier").is_some());
assert!(payload.get_value("nonexistent").is_none());
assert_eq!(payload.get_string("tier"), Some("enterprise"));
assert_eq!(payload.get_string("max_users"), None); assert_eq!(payload.get_string_or("tier", "basic"), "enterprise");
assert_eq!(payload.get_string_or("nonexistent", "default"), "default");
assert_eq!(payload.get_i64("max_users"), Some(100));
assert_eq!(payload.get_i64("tier"), None); assert_eq!(payload.get_i64_or("max_users", 50), 100);
assert_eq!(payload.get_i64_or("nonexistent", 50), 50);
assert_eq!(payload.get_bool("is_beta"), Some(true));
assert_eq!(payload.get_bool("tier"), None); assert_eq!(payload.get_bool_or("is_beta", false), true);
assert_eq!(payload.get_bool_or("nonexistent", false), false);
assert!(payload.get_array("modules").is_some());
assert_eq!(payload.get_array("modules").unwrap().len(), 2);
let modules = payload.get_string_array("modules").unwrap();
assert_eq!(modules, vec!["core", "analytics"]);
assert!(payload.has_key("tier"));
assert!(!payload.has_key("nonexistent"));
let keys: Vec<_> = payload.keys().collect();
assert_eq!(keys.len(), 4);
}
#[test]
fn test_license_payload_get_value_no_metadata() {
let payload = LicensePayload {
format_version: LICENSE_FORMAT_VERSION,
license_id: "test".to_string(),
customer_id: "customer".to_string(),
customer_name: None,
issued_at: Utc::now(),
constraints: LicenseConstraints::new(),
metadata: None,
};
assert!(payload.get_value("any").is_none());
assert_eq!(payload.get_string_or("any", "default"), "default");
assert_eq!(payload.get_i64_or("any", 42), 42);
assert!(!payload.has_key("any"));
assert_eq!(payload.keys().count(), 0);
}
#[test]
fn test_validation_result_get_value() {
let mut metadata = HashMap::new();
metadata.insert("tier".to_string(), serde_json::json!("premium"));
metadata.insert("limit".to_string(), serde_json::json!(500));
let payload = LicensePayload {
format_version: LICENSE_FORMAT_VERSION,
license_id: "test".to_string(),
customer_id: "customer".to_string(),
customer_name: None,
issued_at: Utc::now(),
constraints: LicenseConstraints::new(),
metadata: Some(metadata),
};
let result = ValidationResult::success(payload);
assert_eq!(result.get_string("tier"), Some("premium"));
assert_eq!(result.get_string_or("tier", "basic"), "premium");
assert_eq!(result.get_i64("limit"), Some(500));
assert_eq!(result.get_i64_or("limit", 100), 500);
assert!(result.has_key("tier"));
assert!(!result.has_key("nonexistent"));
}
#[test]
fn test_validation_result_get_value_failure() {
let result = ValidationResult::failure(vec![]);
assert!(result.get_value("any").is_none());
assert_eq!(result.get_string_or("any", "default"), "default");
assert_eq!(result.get_i64_or("any", 42), 42);
assert!(!result.has_key("any"));
}
}