1pub const LINTIAN_DATA_PATH: &str = "/usr/share/lintian/data";
5
6pub const RELEASE_DATES_PATH_OLD: &str = "/usr/share/lintian/data/debian-policy/release-dates.json";
8
9pub const RELEASE_DATES_PATH_NEW: &str = "/usr/share/lintian/data/debian-policy/releases.json";
11
12#[derive(Debug, Clone)]
13pub struct PolicyRelease {
15 pub version: StandardsVersion,
17 pub timestamp: chrono::DateTime<chrono::Utc>,
19 pub closes: Vec<i32>,
21 pub epoch: Option<i32>,
23 pub author: Option<String>,
25 pub changes: Vec<String>,
27}
28
29#[derive(Debug, Clone, serde::Deserialize)]
30#[allow(dead_code)]
31struct Preamble {
32 pub cargo: String,
33 pub title: String,
34}
35
36#[derive(Debug, Clone, serde::Deserialize)]
38struct PolicyReleaseNewFormat {
39 pub version: StandardsVersion,
40 pub timestamp: chrono::DateTime<chrono::Utc>,
41 pub closes: Vec<f64>,
42 pub epoch: Option<i32>,
43 pub author: Option<String>,
44 pub changes: Vec<String>,
45}
46
47#[derive(Debug, Clone, serde::Deserialize)]
49struct PolicyReleaseOldFormat {
50 pub version: StandardsVersion,
51 pub timestamp: chrono::DateTime<chrono::Utc>,
52 pub closes: Vec<i32>,
53 pub epoch: Option<i32>,
54 pub author: Option<String>,
55 pub changes: Vec<String>,
56}
57
58impl From<PolicyReleaseNewFormat> for PolicyRelease {
59 fn from(r: PolicyReleaseNewFormat) -> Self {
60 PolicyRelease {
61 version: r.version,
62 timestamp: r.timestamp,
63 closes: r.closes.into_iter().map(|c| c as i32).collect(),
64 epoch: r.epoch,
65 author: r.author,
66 changes: r.changes,
67 }
68 }
69}
70
71impl From<PolicyReleaseOldFormat> for PolicyRelease {
72 fn from(r: PolicyReleaseOldFormat) -> Self {
73 PolicyRelease {
74 version: r.version,
75 timestamp: r.timestamp,
76 closes: r.closes,
77 epoch: r.epoch,
78 author: r.author,
79 changes: r.changes,
80 }
81 }
82}
83
84#[derive(Debug, Clone, serde::Deserialize)]
85#[allow(dead_code)]
86struct PolicyReleasesNewFormat {
87 pub preamble: Preamble,
88 pub releases: Vec<PolicyReleaseNewFormat>,
89}
90
91#[derive(Debug, Clone, serde::Deserialize)]
92#[allow(dead_code)]
93struct PolicyReleasesOldFormat {
94 pub preamble: Preamble,
95 pub releases: Vec<PolicyReleaseOldFormat>,
96}
97
98#[derive(Debug, Clone)]
99pub struct StandardsVersion(Vec<i32>);
101
102impl StandardsVersion {
103 pub fn new(major: i32, minor: i32, patch: i32) -> Self {
105 Self(vec![major, minor, patch])
106 }
107
108 fn normalize(&self, n: usize) -> Self {
109 let mut version = self.0.clone();
110 version.resize(n, 0);
111 Self(version)
112 }
113}
114
115impl std::cmp::PartialEq for StandardsVersion {
116 fn eq(&self, other: &Self) -> bool {
117 let n = std::cmp::max(self.0.len(), other.0.len());
119 let self_normalized = self.normalize(n);
120 let other_normalized = other.normalize(n);
121 self_normalized.0 == other_normalized.0
122 }
123}
124
125impl std::cmp::Ord for StandardsVersion {
126 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
127 let n = std::cmp::max(self.0.len(), other.0.len());
129 let self_normalized = self.normalize(n);
130 let other_normalized = other.normalize(n);
131 self_normalized.0.cmp(&other_normalized.0)
132 }
133}
134
135impl std::cmp::PartialOrd for StandardsVersion {
136 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
137 Some(self.cmp(other))
138 }
139}
140
141impl std::cmp::Eq for StandardsVersion {}
142
143impl std::str::FromStr for StandardsVersion {
144 type Err = core::num::ParseIntError;
145
146 fn from_str(s: &str) -> Result<Self, Self::Err> {
147 let mut parts = s.split('.').map(|part| part.parse::<i32>());
148 let mut version = Vec::new();
149 for part in &mut parts {
150 version.push(part?);
151 }
152 Ok(StandardsVersion(version))
153 }
154}
155
156impl<'a> serde::Deserialize<'a> for StandardsVersion {
157 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158 where
159 D: serde::Deserializer<'a>,
160 {
161 let s = String::deserialize(deserializer)?;
162 s.parse().map_err(serde::de::Error::custom)
163 }
164}
165
166impl std::fmt::Display for StandardsVersion {
167 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
168 write!(
169 f,
170 "{}",
171 self.0
172 .iter()
173 .map(|part| part.to_string())
174 .collect::<Vec<_>>()
175 .join(".")
176 )
177 }
178}
179
180pub fn iter_standards_versions() -> impl Iterator<Item = PolicyRelease> {
182 iter_standards_versions_opt()
183 .expect("Failed to read release dates from either releases.json or release-dates.json")
184}
185
186pub fn iter_standards_versions_opt() -> Option<impl Iterator<Item = PolicyRelease>> {
189 if let Ok(data) = std::fs::read(RELEASE_DATES_PATH_NEW) {
191 if let Ok(parsed) = serde_json::from_slice::<PolicyReleasesNewFormat>(&data) {
193 return Some(
194 parsed
195 .releases
196 .into_iter()
197 .map(|r| r.into())
198 .collect::<Vec<_>>()
199 .into_iter(),
200 );
201 }
202 }
203
204 if let Ok(data) = std::fs::read(RELEASE_DATES_PATH_OLD) {
206 if let Ok(parsed) = serde_json::from_slice::<PolicyReleasesOldFormat>(&data) {
207 return Some(
208 parsed
209 .releases
210 .into_iter()
211 .map(|r| r.into())
212 .collect::<Vec<_>>()
213 .into_iter(),
214 );
215 }
216 }
217
218 None
219}
220
221pub fn latest_standards_version() -> StandardsVersion {
223 iter_standards_versions()
224 .next()
225 .expect("No standards versions found")
226 .version
227}
228
229pub fn latest_standards_version_opt() -> Option<StandardsVersion> {
232 iter_standards_versions_opt()
233 .and_then(|mut iter| iter.next())
234 .map(|release| release.version)
235}
236
237#[cfg(test)]
238mod tests {
239 use chrono::Datelike;
240
241 #[test]
242 fn test_standards_version() {
243 let version: super::StandardsVersion = "4.2.0".parse().unwrap();
244 assert_eq!(version.0, vec![4, 2, 0]);
245 assert_eq!(version.to_string(), "4.2.0");
246 assert_eq!(version, "4.2".parse().unwrap());
247 assert_eq!(version, "4.2.0".parse().unwrap());
248 }
249
250 #[test]
251 fn test_parse_releases() {
252 let input = r###"{
253 "preamble" : {
254 "cargo" : "releases",
255 "title" : "Debian Policy Releases"
256 },
257 "releases" : [
258 {
259 "author" : "Sean Whitton <spwhitton@spwhitton.name>",
260 "changes" : [
261 "",
262 "debian-policy (4.7.0.0) unstable; urgency=medium",
263 "",
264 " [ Sean Whitton ]",
265 " * Policy: Prefer native overriding mechanisms to diversions & alternatives",
266 " Wording: Luca Boccassi <bluca@debian.org>",
267 " Seconded: Sean Whitton <spwhitton@spwhitton.name>",
268 " Seconded: Russ Allbery <rra@debian.org>",
269 " Seconded: Holger Levsen <holger@layer-acht.org>",
270 " Closes: #1035733",
271 " * Policy: Improve alternative build dependency discussion",
272 " Wording: Russ Allbery <rra@debian.org>",
273 " Seconded: Wouter Verhelst <wouter@debian.org>",
274 " Seconded: Sean Whitton <spwhitton@spwhitton.name>",
275 " Closes: #968226",
276 " * Policy: No network access for required targets for contrib & non-free",
277 " Wording: Aurelien Jarno <aurel32@debian.org>",
278 " Seconded: Sam Hartman <hartmans@debian.org>",
279 " Seconded: Tobias Frost <tobi@debian.org>",
280 " Seconded: Holger Levsen <holger@layer-acht.org>",
281 " Closes: #1068192",
282 "",
283 " [ Russ Allbery ]",
284 " * Policy: Add mention of the new non-free-firmware archive area",
285 " Wording: Gunnar Wolf <gwolf@gwolf.org>",
286 " Seconded: Holger Levsen <holger@layer-acht.org>",
287 " Seconded: Russ Allbery <rra@debian.org>",
288 " Closes: #1029211",
289 " * Policy: Source packages in main may build binary packages in contrib",
290 " Wording: Simon McVittie <smcv@debian.org>",
291 " Seconded: Holger Levsen <holger@layer-acht.org>",
292 " Seconded: Russ Allbery <rra@debian.org>",
293 " Closes: #994008",
294 " * Policy: Allow hard links in source packages",
295 " Wording: Russ Allbery <rra@debian.org>",
296 " Seconded: Helmut Grohne <helmut@subdivi.de>",
297 " Seconded: Guillem Jover <guillem@debian.org>",
298 " Closes: #970234",
299 " * Policy: Binary and Description fields may be absent in .changes",
300 " Wording: Russ Allbery <rra@debian.org>",
301 " Seconded: Sam Hartman <hartmans@debian.org>",
302 " Seconded: Guillem Jover <guillem@debian.org>",
303 " Closes: #963524",
304 " * Policy: systemd units are required to start and stop system services",
305 " Wording: Luca Boccassi <bluca@debian.org>",
306 " Wording: Russ Allbery <rra@debian.org>",
307 " Seconded: Luca Boccassi <bluca@debian.org>",
308 " Seconded: Sam Hartman <hartmans@debian.org>",
309 " Closes: #1039102"
310 ],
311 "closes" : [
312 963524,
313 968226,
314 970234,
315 994008,
316 1029211,
317 1035733,
318 1039102,
319 1068192
320 ],
321 "epoch" : 1712466535,
322 "timestamp" : "2024-04-07T05:08:55Z",
323 "version" : "4.7.0.0"
324 }
325 ]
326}"###;
327 let data: super::PolicyReleasesOldFormat = serde_json::from_str(input).unwrap();
328 assert_eq!(data.releases.len(), 1);
329 }
330
331 #[test]
332 fn test_iter_standards_versions_opt() {
333 let Some(iter) = super::iter_standards_versions_opt() else {
336 return;
338 };
339
340 let versions: Vec<_> = iter.collect();
341
342 assert!(!versions.is_empty());
344
345 let latest = &versions[0];
347
348 assert!(!latest.version.to_string().is_empty());
350 assert!(latest.version.to_string().contains('.'));
351
352 assert!(latest.timestamp.year() >= 2020);
354 assert!(!latest.changes.is_empty());
355 }
356
357 #[test]
358 fn test_latest_standards_version_opt() {
359 let Some(latest) = super::latest_standards_version_opt() else {
361 return;
363 };
364
365 let version_str = latest.to_string();
367 assert!(!version_str.is_empty());
368 assert!(version_str.contains('.'));
369
370 assert!(latest >= "4.0.0".parse::<super::StandardsVersion>().unwrap());
372 }
373}