use std::sync::{OnceLock, RwLock};
use crate::capability::{Capability, CapabilitySet};
use crate::error::{Error, Result};
use crate::tier::Tier;
static LICENSE_PUBLIC_KEY: OnceLock<[u8; 32]> = OnceLock::new();
static SIGNED_PAYLOAD_META: RwLock<Option<SignedPayloadMeta>> = RwLock::new(None);
#[derive(Debug, Clone)]
struct SignedPayloadMeta {
expires_at: u64,
licensee: String,
company: String,
features: Vec<String>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct LicenseInfo {
pub tier: Tier,
pub expires_at: Option<String>,
pub capabilities: CapabilitySet,
pub output_is_marked: bool,
pub features: Vec<String>,
pub licensee: Option<String>,
pub company: Option<String>,
}
static GLOBAL_TIER: OnceLock<Tier> = OnceLock::new();
pub fn set_license_key(key: &str) -> Result<()> {
let trimmed = key.trim();
if trimmed.starts_with('{') {
return set_license_payload(trimmed);
}
let tier = parse_key_to_tier(key)?;
match GLOBAL_TIER.set(tier) {
Ok(()) => Ok(()),
Err(_existing) => {
let existing = *GLOBAL_TIER.get().expect("initialised");
if existing == tier {
Ok(())
} else {
Err(Error::InvalidLicense {
reason: format!(
"license already set to {existing:?}; restart the process to switch to {tier:?}",
),
})
}
}
}
}
pub fn license_info() -> LicenseInfo {
let tier = effective_tier();
let meta = SIGNED_PAYLOAD_META.read().ok().and_then(|g| g.clone());
let (expires_at, features, licensee, company) = match meta {
Some(m) => (
Some(unix_to_iso8601(m.expires_at)),
m.features,
Some(m.licensee),
Some(m.company),
),
None => (None, Vec::new(), None, None),
};
LicenseInfo {
tier,
expires_at,
capabilities: tier.capabilities(),
output_is_marked: tier.is_marked(),
features,
licensee,
company,
}
}
pub fn set_license_public_key(public_key: &[u8]) -> Result<()> {
let key_bytes: [u8; 32] = public_key.try_into().map_err(|_| Error::InvalidLicense {
reason: "public key must be exactly 32 bytes".into(),
})?;
match LICENSE_PUBLIC_KEY.set(key_bytes) {
Ok(()) => Ok(()),
Err(_) => {
let existing = *LICENSE_PUBLIC_KEY.get().expect("initialised");
if existing == key_bytes {
Ok(())
} else {
Err(Error::InvalidLicense {
reason: "public key already set; restart the process to swap keys".into(),
})
}
}
}
}
pub fn set_license_payload(license_json: &str) -> Result<()> {
let Some(public_key) = LICENSE_PUBLIC_KEY.get() else {
return Err(Error::InvalidLicense {
reason: "no public key configured — call set_license_public_key first".into(),
});
};
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let license_file = xfa_license::token::verify_license(public_key, license_json)
.map_err(map_xfa_license_error)?;
if license_file.payload.is_expired(now) {
return Err(Error::LicenseExpired {
expires_at: license_file.payload.expires_at,
});
}
let tier = map_xfa_tier(license_file.payload.tier)?;
match GLOBAL_TIER.set(tier) {
Ok(()) => {
if let Ok(mut guard) = SIGNED_PAYLOAD_META.write() {
*guard = Some(SignedPayloadMeta {
expires_at: license_file.payload.expires_at,
licensee: license_file.payload.licensee.clone(),
company: license_file.payload.company.clone(),
features: license_file.payload.features.clone().unwrap_or_default(),
});
}
Ok(())
}
Err(_existing) => {
let existing = *GLOBAL_TIER.get().expect("initialised");
if existing == tier {
Ok(())
} else {
Err(Error::InvalidLicense {
reason: format!(
"license already set to {existing:?}; restart the process to switch to {tier:?}",
),
})
}
}
}
}
fn map_xfa_license_error(e: xfa_license::error::LicenseError) -> Error {
use xfa_license::error::LicenseError as L;
match e {
L::InvalidSignature => Error::LicenseInvalidSignature,
L::Expired(ts) => Error::LicenseExpired { expires_at: ts },
L::RateLimitExceeded(limit) => Error::LicenseRateLimited {
resource: "api_calls".into(),
used: limit as u64,
limit: limit as u64,
},
L::QuotaExceeded {
resource,
used,
limit,
} => Error::LicenseRateLimited {
resource,
used,
limit,
},
L::FeatureNotAvailable(name) => Error::InvalidLicense {
reason: format!("feature not available: {name}"),
},
L::InvalidPublicKey => Error::InvalidLicense {
reason: "invalid public key (must be 32 bytes)".into(),
},
L::MalformedToken(detail) => Error::InvalidLicense {
reason: format!("malformed signed-license token: {detail}"),
},
L::Json(err) => Error::InvalidLicense {
reason: format!("license JSON parse failure: {err}"),
},
L::Io(err) => Error::InvalidLicense {
reason: format!("license I/O error: {err}"),
},
}
}
fn map_xfa_tier(t: xfa_license::Tier) -> Result<Tier> {
use xfa_license::Tier as X;
match t {
X::Trial => Ok(Tier::Trial),
X::Basic => Ok(Tier::Developer),
X::Professional => Ok(Tier::Team),
X::Enterprise => Ok(Tier::Enterprise),
X::Archival => Ok(Tier::Business),
}
}
fn unix_to_iso8601(ts: u64) -> String {
let secs = ts as i64;
let days = secs.div_euclid(86_400);
let sod = secs.rem_euclid(86_400);
let h = sod / 3600;
let m = (sod / 60) % 60;
let s = sod % 60;
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let month = if mp < 10 { mp + 3 } else { mp - 9 }; let year = y + i64::from(month <= 2);
format!("{year:04}-{month:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}
pub(crate) fn effective_tier() -> Tier {
if let Some(&t) = GLOBAL_TIER.get() {
return t;
}
if let Ok(key) = std::env::var("PDFLUENT_LICENSE_KEY") {
if let Ok(t) = parse_key_to_tier(&key) {
return t;
}
}
Tier::Trial
}
fn parse_key_to_tier(key: &str) -> Result<Tier> {
let trimmed = key.trim();
let lowered = trimmed.to_ascii_lowercase();
let after_prefix = lowered
.strip_prefix("tier:")
.ok_or_else(|| Error::InvalidLicense {
reason: format!("expected `tier:<name>` format, got {trimmed:?}"),
})?;
match after_prefix.trim() {
"trial" => Ok(Tier::Trial),
"developer" => Ok(Tier::Developer),
"team" => Ok(Tier::Team),
"business" => Ok(Tier::Business),
"enterprise" => Ok(Tier::Enterprise),
other => Err(Error::InvalidLicense {
reason: format!(
"unknown tier {other:?}; expected trial/developer/team/business/enterprise"
),
}),
}
}
pub(crate) fn require_capability_with_override(
cap: Capability,
override_key: Option<&str>,
) -> Result<()> {
let tier = match override_key {
Some(key) => parse_key_to_tier(key)?,
None => effective_tier(),
};
if tier.capabilities().contains(cap) {
return Ok(());
}
let required = [
Tier::Developer,
Tier::Team,
Tier::Business,
Tier::Enterprise,
]
.iter()
.copied()
.find(|t| t.capabilities().contains(cap))
.unwrap_or(Tier::Enterprise);
Err(Error::FeatureNotInTier {
capability: cap,
current_tier: tier,
required_tier: required,
})
}
pub(crate) fn require_capability(cap: Capability) -> Result<()> {
require_capability_with_override(cap, None)
}