debian_analyzer/
lintian.rs

1//! Lintian data structures and utilities
2
3/// The path to the Lintian data directory
4pub const LINTIAN_DATA_PATH: &str = "/usr/share/lintian/data";
5
6/// The path to the Lintian release dates file (old name)
7pub const RELEASE_DATES_PATH_OLD: &str = "/usr/share/lintian/data/debian-policy/release-dates.json";
8
9/// The path to the Lintian release dates file (new name)
10pub const RELEASE_DATES_PATH_NEW: &str = "/usr/share/lintian/data/debian-policy/releases.json";
11
12#[derive(Debug, Clone)]
13/// A release of the Debian Policy
14pub struct PolicyRelease {
15    /// The version of the release
16    pub version: StandardsVersion,
17    /// When the release was published
18    pub timestamp: chrono::DateTime<chrono::Utc>,
19    /// List of bug numbers closed by this release
20    pub closes: Vec<i32>,
21    /// The epoch of the release
22    pub epoch: Option<i32>,
23    /// The author of the release
24    pub author: Option<String>,
25    /// The changes made in this release
26    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// Internal struct for deserializing releases.json (new format with floats)
37#[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// Internal struct for deserializing release-dates.json (old format with ints)
48#[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)]
99/// A version of the Debian Policy
100pub struct StandardsVersion(Vec<i32>);
101
102impl StandardsVersion {
103    /// Create a new StandardsVersion from major, minor, and patch numbers
104    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        // Normalize to the same length
118        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        // Normalize to the same length
128        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
180/// Returns an iterator over all known standards versions
181pub 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
186/// Returns an iterator over all known standards versions
187/// Returns None if neither releases.json nor release-dates.json can be found
188pub fn iter_standards_versions_opt() -> Option<impl Iterator<Item = PolicyRelease>> {
189    // Try the new filename first, then fall back to the old one
190    if let Ok(data) = std::fs::read(RELEASE_DATES_PATH_NEW) {
191        // Try to parse as new format (releases.json)
192        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    // Fall back to old format (release-dates.json)
205    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
221/// Returns the latest standards version
222pub fn latest_standards_version() -> StandardsVersion {
223    iter_standards_versions()
224        .next()
225        .expect("No standards versions found")
226        .version
227}
228
229/// Returns the latest standards version
230/// Returns None if release data files are not available
231pub 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        // This test verifies that we can read the policy release data
334        // In test environments, the files may not exist
335        let Some(iter) = super::iter_standards_versions_opt() else {
336            // Skip test if no files are available
337            return;
338        };
339
340        let versions: Vec<_> = iter.collect();
341
342        // Should have at least one version
343        assert!(!versions.is_empty());
344
345        // The latest version should be first
346        let latest = &versions[0];
347
348        // Verify the version has a proper format
349        assert!(!latest.version.to_string().is_empty());
350        assert!(latest.version.to_string().contains('.'));
351
352        // Verify other fields are populated
353        assert!(latest.timestamp.year() >= 2020);
354        assert!(!latest.changes.is_empty());
355    }
356
357    #[test]
358    fn test_latest_standards_version_opt() {
359        // Test that we can get the latest standards version
360        let Some(latest) = super::latest_standards_version_opt() else {
361            // Skip test if no files are available
362            return;
363        };
364
365        // Should have a valid version string
366        let version_str = latest.to_string();
367        assert!(!version_str.is_empty());
368        assert!(version_str.contains('.'));
369
370        // Should be at least 4.0.0 (Debian policy versions)
371        assert!(latest >= "4.0.0".parse::<super::StandardsVersion>().unwrap());
372    }
373}