Skip to main content

standard_version/
version_plain.rs

1//! Plain `VERSION` file engine.
2//!
3//! Implements [`VersionFile`] for projects that store the version string as
4//! the sole content of a `VERSION` file.
5
6use crate::version_file::{VersionFile, VersionFileError};
7
8/// Version file engine for plain `VERSION` files.
9///
10/// Expects the file to contain nothing but a version string (optionally
11/// followed by a trailing newline).
12#[derive(Debug, Clone, Copy)]
13pub struct PlainVersionFile;
14
15/// Maximum length (in bytes) for a `VERSION` file to be considered valid.
16const MAX_VERSION_LEN: usize = 64;
17
18impl VersionFile for PlainVersionFile {
19    fn name(&self) -> &str {
20        "VERSION"
21    }
22
23    fn filenames(&self) -> &[&str] {
24        &["VERSION"]
25    }
26
27    fn detect(&self, content: &str) -> bool {
28        let trimmed = content.trim();
29        if trimmed.is_empty() || trimmed.len() > MAX_VERSION_LEN {
30            return false;
31        }
32        // Reject content with multiple lines (not a plain version file).
33        if trimmed.contains('\n') {
34            return false;
35        }
36        // Require at least one dot (version-like: X.Y or X.Y.Z).
37        if !trimmed.contains('.') {
38            return false;
39        }
40        // Reject content with characters unlikely in a version string.
41        trimmed
42            .chars()
43            .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '+')
44    }
45
46    fn read_version(&self, content: &str) -> Option<String> {
47        let trimmed = content.trim();
48        if trimmed.is_empty() {
49            return None;
50        }
51        Some(trimmed.to_string())
52    }
53
54    fn write_version(&self, _content: &str, new_version: &str) -> Result<String, VersionFileError> {
55        Ok(format!("{new_version}\n"))
56    }
57}
58
59// ---------------------------------------------------------------------------
60// Tests
61// ---------------------------------------------------------------------------
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    // --- detect ---
68
69    #[test]
70    fn detect_positive_semver() {
71        assert!(PlainVersionFile.detect("1.2.3\n"));
72    }
73
74    #[test]
75    fn detect_positive_prerelease() {
76        assert!(PlainVersionFile.detect("1.0.0-rc.1\n"));
77    }
78
79    #[test]
80    fn detect_positive_build_metadata() {
81        assert!(PlainVersionFile.detect("1.0.0+build.42\n"));
82    }
83
84    #[test]
85    fn detect_negative_empty() {
86        assert!(!PlainVersionFile.detect(""));
87        assert!(!PlainVersionFile.detect("  \n"));
88    }
89
90    #[test]
91    fn detect_negative_multiline() {
92        assert!(!PlainVersionFile.detect("1.0.0\nsome other stuff\n"));
93    }
94
95    #[test]
96    fn detect_negative_binary_garbage() {
97        assert!(!PlainVersionFile.detect("\x00\x01\x02\x03"));
98    }
99
100    #[test]
101    fn detect_negative_too_long() {
102        let long = "a".repeat(MAX_VERSION_LEN + 1);
103        assert!(!PlainVersionFile.detect(&long));
104    }
105
106    #[test]
107    fn detect_negative_special_characters() {
108        assert!(!PlainVersionFile.detect("1.0.0; rm -rf /\n"));
109    }
110
111    #[test]
112    fn detect_negative_bare_word() {
113        assert!(!PlainVersionFile.detect("latest\n"));
114        assert!(!PlainVersionFile.detect("stable\n"));
115    }
116
117    // --- read_version ---
118
119    #[test]
120    fn read_version_basic() {
121        assert_eq!(
122            PlainVersionFile.read_version("1.2.3\n"),
123            Some("1.2.3".to_string()),
124        );
125    }
126
127    #[test]
128    fn read_version_with_whitespace() {
129        assert_eq!(
130            PlainVersionFile.read_version("  1.2.3  \n"),
131            Some("1.2.3".to_string()),
132        );
133    }
134
135    #[test]
136    fn read_version_empty() {
137        assert_eq!(PlainVersionFile.read_version(""), None);
138        assert_eq!(PlainVersionFile.read_version("  \n"), None);
139    }
140
141    // --- write_version ---
142
143    #[test]
144    fn write_version_overwrites_entirely() {
145        let result = PlainVersionFile.write_version("1.2.3\n", "2.0.0").unwrap();
146        assert_eq!(result, "2.0.0\n");
147    }
148
149    #[test]
150    fn write_version_always_has_trailing_newline() {
151        let result = PlainVersionFile.write_version("1.2.3", "2.0.0").unwrap();
152        assert_eq!(result, "2.0.0\n");
153    }
154
155    // --- integration ---
156
157    #[test]
158    fn integration_roundtrip() {
159        let dir = tempfile::tempdir().unwrap();
160        let path = dir.path().join("VERSION");
161        std::fs::write(&path, "1.2.3\n").unwrap();
162
163        let content = std::fs::read_to_string(&path).unwrap();
164        assert!(PlainVersionFile.detect(&content));
165        assert_eq!(
166            PlainVersionFile.read_version(&content),
167            Some("1.2.3".to_string()),
168        );
169
170        let updated = PlainVersionFile.write_version(&content, "3.0.0").unwrap();
171        std::fs::write(&path, &updated).unwrap();
172
173        let final_content = std::fs::read_to_string(&path).unwrap();
174        assert_eq!(
175            PlainVersionFile.read_version(&final_content),
176            Some("3.0.0".to_string()),
177        );
178    }
179}