Skip to main content

licenz_core/
license.rs

1//! License data structures and types
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Hardware binding information for a license
8#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
9pub struct HardwareBinding {
10    /// Allowed MAC addresses (any match is valid)
11    #[serde(default, skip_serializing_if = "Vec::is_empty")]
12    pub mac_addresses: Vec<String>,
13
14    /// Allowed disk/drive IDs (any match is valid)
15    #[serde(default, skip_serializing_if = "Vec::is_empty")]
16    pub disk_ids: Vec<String>,
17
18    /// Allowed hostnames (any match is valid)
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub hostnames: Vec<String>,
21
22    /// Custom hardware identifiers (key-value pairs)
23    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
24    pub custom: HashMap<String, Vec<String>>,
25}
26
27impl HardwareBinding {
28    /// Create a new empty hardware binding
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Add a MAC address to the allowed list
34    pub fn with_mac_address(mut self, mac: impl Into<String>) -> Self {
35        self.mac_addresses.push(mac.into().to_uppercase());
36        self
37    }
38
39    /// Add multiple MAC addresses
40    pub fn with_mac_addresses(mut self, macs: impl IntoIterator<Item = impl Into<String>>) -> Self {
41        self.mac_addresses
42            .extend(macs.into_iter().map(|m| m.into().to_uppercase()));
43        self
44    }
45
46    /// Add a disk ID to the allowed list
47    pub fn with_disk_id(mut self, disk_id: impl Into<String>) -> Self {
48        self.disk_ids.push(disk_id.into());
49        self
50    }
51
52    /// Add a hostname to the allowed list
53    pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
54        self.hostnames.push(hostname.into().to_lowercase());
55        self
56    }
57
58    /// Add a custom hardware identifier
59    pub fn with_custom(mut self, key: impl Into<String>, values: Vec<String>) -> Self {
60        self.custom.insert(key.into(), values);
61        self
62    }
63
64    /// Check if any hardware binding is set
65    pub fn is_empty(&self) -> bool {
66        self.mac_addresses.is_empty()
67            && self.disk_ids.is_empty()
68            && self.hostnames.is_empty()
69            && self.custom.is_empty()
70    }
71}
72
73/// The core license data structure
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct LicenseData {
76    /// Unique license identifier
77    pub id: String,
78
79    /// Serial number for tracking
80    pub serial: String,
81
82    /// Customer/organization identifier
83    pub customer_id: String,
84
85    /// Product identifier
86    pub product_id: String,
87
88    /// License version
89    #[serde(default = "default_version")]
90    pub version: u32,
91
92    /// When the license becomes valid
93    pub valid_from: DateTime<Utc>,
94
95    /// When the license expires
96    pub valid_until: DateTime<Utc>,
97
98    /// List of enabled features
99    #[serde(default)]
100    pub features: Vec<String>,
101
102    /// Hardware binding restrictions
103    #[serde(default)]
104    pub hardware_binding: HardwareBinding,
105
106    /// Maximum number of seats/users (0 = unlimited)
107    #[serde(default)]
108    pub max_seats: u32,
109
110    /// Additional metadata
111    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
112    pub metadata: HashMap<String, String>,
113
114    /// License issue timestamp
115    pub issued_at: DateTime<Utc>,
116}
117
118fn default_version() -> u32 {
119    1
120}
121
122impl LicenseData {
123    /// Create a new license data builder
124    pub fn builder() -> LicenseDataBuilder {
125        LicenseDataBuilder::new()
126    }
127
128    /// Check if a feature is enabled
129    pub fn has_feature(&self, feature: &str) -> bool {
130        self.features
131            .iter()
132            .any(|f| f.eq_ignore_ascii_case(feature))
133    }
134
135    /// Get remaining days until expiration
136    pub fn days_remaining(&self) -> i64 {
137        let now = Utc::now();
138        (self.valid_until - now).num_days()
139    }
140
141    /// Check if the license is currently in its valid time window
142    pub fn is_time_valid(&self) -> bool {
143        let now = Utc::now();
144        now >= self.valid_from && now <= self.valid_until
145    }
146}
147
148/// Builder for creating license data
149#[derive(Default)]
150pub struct LicenseDataBuilder {
151    id: Option<String>,
152    serial: Option<String>,
153    customer_id: Option<String>,
154    product_id: Option<String>,
155    version: u32,
156    valid_from: Option<DateTime<Utc>>,
157    valid_until: Option<DateTime<Utc>>,
158    features: Vec<String>,
159    hardware_binding: HardwareBinding,
160    max_seats: u32,
161    metadata: HashMap<String, String>,
162}
163
164impl LicenseDataBuilder {
165    /// Create a new license data builder with default values.
166    pub fn new() -> Self {
167        Self {
168            version: 1,
169            ..Default::default()
170        }
171    }
172
173    /// Set the unique license identifier (required).
174    pub fn id(mut self, id: impl Into<String>) -> Self {
175        self.id = Some(id.into());
176        self
177    }
178
179    /// Set the serial number for tracking (required).
180    pub fn serial(mut self, serial: impl Into<String>) -> Self {
181        self.serial = Some(serial.into());
182        self
183    }
184
185    /// Set the customer or organization identifier (required).
186    pub fn customer_id(mut self, customer_id: impl Into<String>) -> Self {
187        self.customer_id = Some(customer_id.into());
188        self
189    }
190
191    /// Set the product identifier (required).
192    pub fn product_id(mut self, product_id: impl Into<String>) -> Self {
193        self.product_id = Some(product_id.into());
194        self
195    }
196
197    /// Set the license schema version (default: 1).
198    pub fn version(mut self, version: u32) -> Self {
199        self.version = version;
200        self
201    }
202
203    /// Set the start of the validity window.
204    pub fn valid_from(mut self, valid_from: DateTime<Utc>) -> Self {
205        self.valid_from = Some(valid_from);
206        self
207    }
208
209    /// Set the end of the validity window.
210    pub fn valid_until(mut self, valid_until: DateTime<Utc>) -> Self {
211        self.valid_until = Some(valid_until);
212        self
213    }
214
215    /// Set validity to `days` from now (sets both `valid_from` and `valid_until`).
216    pub fn valid_days(mut self, days: i64) -> Self {
217        let now = Utc::now();
218        self.valid_from = Some(now);
219        self.valid_until = Some(now + chrono::Duration::days(days));
220        self
221    }
222
223    /// Add a single feature flag to the license.
224    pub fn feature(mut self, feature: impl Into<String>) -> Self {
225        self.features.push(feature.into());
226        self
227    }
228
229    /// Add multiple feature flags at once.
230    pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
231        self.features.extend(features.into_iter().map(|f| f.into()));
232        self
233    }
234
235    /// Attach hardware binding restrictions (MAC address, hostname, disk ID, custom).
236    pub fn hardware_binding(mut self, binding: HardwareBinding) -> Self {
237        self.hardware_binding = binding;
238        self
239    }
240
241    /// Set the maximum number of concurrent seats (0 = unlimited).
242    pub fn max_seats(mut self, max_seats: u32) -> Self {
243        self.max_seats = max_seats;
244        self
245    }
246
247    /// Add a key-value metadata entry.
248    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
249        self.metadata.insert(key.into(), value.into());
250        self
251    }
252
253    /// Build the license data, returning an error if required fields are missing.
254    pub fn build(self) -> crate::Result<LicenseData> {
255        use crate::error::LicenseError;
256
257        let now = Utc::now();
258
259        Ok(LicenseData {
260            id: self
261                .id
262                .ok_or_else(|| LicenseError::MissingField("id".into()))?,
263            serial: self
264                .serial
265                .ok_or_else(|| LicenseError::MissingField("serial".into()))?,
266            customer_id: self
267                .customer_id
268                .ok_or_else(|| LicenseError::MissingField("customer_id".into()))?,
269            product_id: self
270                .product_id
271                .ok_or_else(|| LicenseError::MissingField("product_id".into()))?,
272            version: self.version,
273            valid_from: self.valid_from.unwrap_or(now),
274            valid_until: self
275                .valid_until
276                .unwrap_or(now + chrono::Duration::days(365)),
277            features: self.features,
278            hardware_binding: self.hardware_binding,
279            max_seats: self.max_seats,
280            metadata: self.metadata,
281            issued_at: now,
282        })
283    }
284}
285
286/// A signed license containing the data and cryptographic signature
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct SignedLicense {
289    /// The license data
290    pub data: LicenseData,
291
292    /// Digital signature (base64 encoded)
293    pub signature: String,
294
295    /// Signature algorithm used
296    pub algorithm: String,
297}
298
299/// License file format marker
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum LicenseFormat {
302    /// Binary format (v2+)
303    Binary,
304    /// JSON format (v1 legacy)
305    Json,
306}
307
308/// Binary license file header
309pub const BINARY_MAGIC: &[u8; 4] = b"FLIC";
310pub const BINARY_VERSION: u8 = 1;