standard_version/
version_plain.rs1use crate::version_file::{VersionFile, VersionFileError};
7
8#[derive(Debug, Clone, Copy)]
13pub struct PlainVersionFile;
14
15const 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 if trimmed.contains('\n') {
34 return false;
35 }
36 if !trimmed.contains('.') {
38 return false;
39 }
40 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#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[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 #[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 #[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 #[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}