Skip to main content

sentri_utils/
version.rs

1//! Semantic versioning and release management for Sentri.
2//!
3//! Provides:
4//! - Semantic versioning with validation
5//! - Release artifact generation
6//! - Reproducible build metadata
7//! - Security checksums (SHA256)
8
9use std::fmt;
10
11/// Semantic version following SemVer 2.0.0.
12#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
13pub struct SemanticVersion {
14    /// Major version (breaking changes).
15    pub major: u32,
16    /// Minor version (feature additions).
17    pub minor: u32,
18    /// Patch version (bug fixes).
19    pub patch: u32,
20}
21
22impl SemanticVersion {
23    /// Create a new semantic version.
24    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
25        Self {
26            major,
27            minor,
28            patch,
29        }
30    }
31
32    /// Parse semantic version from string (e.g., "0.1.0").
33    pub fn parse(s: &str) -> Result<Self, String> {
34        let parts: Vec<&str> = s.split('.').collect();
35
36        if parts.len() != 3 {
37            return Err("Invalid version format, expected MAJOR.MINOR.PATCH".to_string());
38        }
39
40        let major = parts[0]
41            .parse::<u32>()
42            .map_err(|_| "Major version must be a number")?;
43        let minor = parts[1]
44            .parse::<u32>()
45            .map_err(|_| "Minor version must be a number")?;
46        let patch = parts[2]
47            .parse::<u32>()
48            .map_err(|_| "Patch version must be a number")?;
49
50        Ok(Self {
51            major,
52            minor,
53            patch,
54        })
55    }
56
57    /// Check if this version is compatible with a minimum required version.
58    pub fn is_compatible_with(&self, minimum: SemanticVersion) -> bool {
59        if self.major != minimum.major {
60            return self.major > minimum.major;
61        }
62        if self.minor != minimum.minor {
63            return self.minor > minimum.minor;
64        }
65        self.patch >= minimum.patch
66    }
67
68    /// Increment major version (reset minor and patch).
69    pub fn bump_major(&mut self) {
70        self.major += 1;
71        self.minor = 0;
72        self.patch = 0;
73    }
74
75    /// Increment minor version (reset patch).
76    pub fn bump_minor(&mut self) {
77        self.minor += 1;
78        self.patch = 0;
79    }
80
81    /// Increment patch version.
82    pub fn bump_patch(&mut self) {
83        self.patch += 1;
84    }
85}
86
87impl fmt::Display for SemanticVersion {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
90    }
91}
92
93/// Release artifact metadata.
94#[derive(Debug, Clone)]
95pub struct ReleaseArtifact {
96    /// Semantic version of the release.
97    pub version: SemanticVersion,
98    /// Target platform (e.g., "linux-x86_64", "darwin-aarch64", "windows-x86_64").
99    pub target: String,
100    /// SHA256 checksum of the binary.
101    pub checksum: String,
102    /// Whether this is a reproducible build.
103    pub reproducible: bool,
104}
105
106impl ReleaseArtifact {
107    /// Create a new release artifact.
108    pub fn new(
109        version: SemanticVersion,
110        target: String,
111        checksum: String,
112        reproducible: bool,
113    ) -> Self {
114        Self {
115            version,
116            target,
117            checksum,
118            reproducible,
119        }
120    }
121
122    /// Compute expected artifact filename.
123    pub fn filename(&self) -> String {
124        format!("sentri-{}-{}", self.version, self.target)
125    }
126
127    /// Verify that artifact checksum matches expected value.
128    pub fn verify_checksum(&self, actual_checksum: &str) -> bool {
129        self.checksum.eq_ignore_ascii_case(actual_checksum)
130    }
131}
132
133impl fmt::Display for ReleaseArtifact {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(
136            f,
137            "sentri {} ({}) [{}]",
138            self.version, self.target, self.checksum
139        )
140    }
141}
142
143/// Reproducible build configuration.
144#[derive(Debug, Clone)]
145pub struct ReproducibleBuildConfig {
146    /// Enable LTO (Link Time Optimization).
147    pub lto: bool,
148    /// Optimization level (0-3).
149    pub opt_level: u32,
150    /// Strip debug symbols.
151    pub strip: bool,
152    /// Pin Rust version.
153    pub rust_version: String,
154}
155
156impl ReproducibleBuildConfig {
157    /// Create default reproducible build configuration.
158    pub fn default_release() -> Self {
159        Self {
160            lto: true,
161            opt_level: 3,
162            strip: false, // Keep debug symbols for crash analysis
163            rust_version: "1.70.0".to_string(),
164        }
165    }
166
167    /// Verify that build environment matches configuration.
168    pub fn verify_environment(&self, current_rust_version: &str) -> Result<(), String> {
169        if current_rust_version != self.rust_version {
170            return Err(format!(
171                "Rust version mismatch: expected {}, got {}",
172                self.rust_version, current_rust_version
173            ));
174        }
175        Ok(())
176    }
177}
178
179/// Supported platforms for binary releases.
180#[derive(Debug, Clone, Copy, Eq, PartialEq)]
181pub enum Platform {
182    /// Linux x86_64.
183    LinuxX86_64,
184    /// Linux aarch64 (ARM64).
185    LinuxAarch64,
186    /// macOS x86_64 (Intel).
187    MacOSX86_64,
188    /// macOS aarch64 (Apple Silicon).
189    MacOSAarch64,
190    /// Windows x86_64.
191    WindowsX86_64,
192}
193
194impl Platform {
195    /// Get the target triple for this platform.
196    pub fn target_triple(&self) -> &'static str {
197        match self {
198            Self::LinuxX86_64 => "x86_64-unknown-linux-gnu",
199            Self::LinuxAarch64 => "aarch64-unknown-linux-gnu",
200            Self::MacOSX86_64 => "x86_64-apple-darwin",
201            Self::MacOSAarch64 => "aarch64-apple-darwin",
202            Self::WindowsX86_64 => "x86_64-pc-windows-msvc",
203        }
204    }
205
206    /// Get the artifact filename suffix for this platform.
207    pub fn artifact_suffix(&self) -> &'static str {
208        match self {
209            Self::LinuxX86_64 => "linux-x86_64",
210            Self::LinuxAarch64 => "linux-aarch64",
211            Self::MacOSX86_64 => "darwin-x86_64",
212            Self::MacOSAarch64 => "darwin-aarch64",
213            Self::WindowsX86_64 => "windows-x86_64",
214        }
215    }
216
217    /// Get all supported platforms.
218    pub fn all() -> &'static [Self] {
219        &[
220            Self::LinuxX86_64,
221            Self::LinuxAarch64,
222            Self::MacOSX86_64,
223            Self::MacOSAarch64,
224            Self::WindowsX86_64,
225        ]
226    }
227}
228
229impl fmt::Display for Platform {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(f, "{}", self.artifact_suffix())
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_semver_parse() {
241        let v = SemanticVersion::parse("1.2.3").unwrap();
242        assert_eq!(v.major, 1);
243        assert_eq!(v.minor, 2);
244        assert_eq!(v.patch, 3);
245    }
246
247    #[test]
248    fn test_semver_parse_invalid() {
249        assert!(SemanticVersion::parse("1.2").is_err());
250        assert!(SemanticVersion::parse("1.2.a").is_err());
251    }
252
253    #[test]
254    fn test_semver_display() {
255        let v = SemanticVersion::new(0, 1, 0);
256        assert_eq!(v.to_string(), "0.1.0");
257    }
258
259    #[test]
260    fn test_semver_bump() {
261        let mut v = SemanticVersion::new(0, 1, 0);
262        v.bump_patch();
263        assert_eq!(v, SemanticVersion::new(0, 1, 1));
264
265        v.bump_minor();
266        assert_eq!(v, SemanticVersion::new(0, 2, 0));
267
268        v.bump_major();
269        assert_eq!(v, SemanticVersion::new(1, 0, 0));
270    }
271
272    #[test]
273    fn test_semver_compatibility() {
274        let v1 = SemanticVersion::new(1, 2, 3);
275        let v2 = SemanticVersion::new(1, 2, 0);
276        let v3 = SemanticVersion::new(0, 5, 0);
277
278        assert!(v1.is_compatible_with(v2)); // 1.2.3 >= 1.2.0
279        assert!(!v2.is_compatible_with(v1)); // 1.2.0 < 1.2.3
280        assert!(!v3.is_compatible_with(v1)); // 0.5.0 < 1.0.0
281    }
282
283    #[test]
284    fn test_release_artifact_filename() {
285        let artifact = ReleaseArtifact::new(
286            SemanticVersion::new(0, 1, 0),
287            "linux-x86_64".to_string(),
288            "abc123".to_string(),
289            true,
290        );
291        assert_eq!(artifact.filename(), "sentri-0.1.0-linux-x86_64");
292    }
293
294    #[test]
295    fn test_release_artifact_verify_checksum() {
296        let artifact = ReleaseArtifact::new(
297            SemanticVersion::new(0, 1, 0),
298            "linux-x86_64".to_string(),
299            "ABC123".to_string(),
300            true,
301        );
302        assert!(artifact.verify_checksum("abc123")); // Case-insensitive
303        assert!(!artifact.verify_checksum("xyz789"));
304    }
305
306    #[test]
307    fn test_platform_target_triples() {
308        assert_eq!(
309            Platform::LinuxX86_64.target_triple(),
310            "x86_64-unknown-linux-gnu"
311        );
312        assert_eq!(
313            Platform::MacOSAarch64.target_triple(),
314            "aarch64-apple-darwin"
315        );
316        assert_eq!(
317            Platform::WindowsX86_64.target_triple(),
318            "x86_64-pc-windows-msvc"
319        );
320    }
321
322    #[test]
323    fn test_platform_all() {
324        assert_eq!(Platform::all().len(), 5);
325    }
326}