debian_analyzer/
debhelper.rs1use debversion::Version;
3use std::path::Path;
4
5fn parse_debhelper_compat(s: &str) -> Option<u8> {
7 s.split_once('#').map_or(s, |s| s.0).trim().parse().ok()
8}
9
10pub fn read_debhelper_compat_file(path: &Path) -> Result<Option<u8>, std::io::Error> {
15 match std::fs::read_to_string(path) {
16 Ok(content) => Ok(parse_debhelper_compat(&content)),
17 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
18 Err(e) => Err(e),
19 }
20}
21
22pub fn get_debhelper_compat_level_from_control(control: &debian_control::Control) -> Option<u8> {
30 let source = control.source()?;
31
32 if let Some(dh_compat) = source.as_deb822().get("X-DH-Compat") {
33 return parse_debhelper_compat(dh_compat.as_str());
34 }
35
36 let build_depends = source.build_depends()?;
37
38 let rels = build_depends
39 .entries()
40 .flat_map(|entry| entry.relations().collect::<Vec<_>>())
41 .find(|r| r.name() == "debhelper-compat");
42
43 rels.and_then(|r| r.version().and_then(|v| v.1.to_string().parse().ok()))
44}
45
46pub fn get_debhelper_compat_level(path: &Path) -> Result<Option<u8>, std::io::Error> {
54 match read_debhelper_compat_file(&path.join("debian/compat")) {
55 Ok(Some(level)) => {
56 return Ok(Some(level));
57 }
58 Err(e) => {
59 return Err(e);
60 }
61 Ok(None) => {}
62 }
63
64 let p = path.join("debian/control");
65
66 match std::fs::File::open(p) {
67 Ok(f) => {
68 let control = debian_control::Control::read_relaxed(f).unwrap().0;
69 Ok(get_debhelper_compat_level_from_control(&control))
70 }
71 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
72 Err(e) => Err(e),
73 }
74}
75
76pub fn maximum_debhelper_compat_version(compat_release: &str) -> u8 {
84 crate::release_info::debhelper_versions
85 .get(compat_release)
86 .map(|v| {
87 v.upstream_version
88 .split('.')
89 .next()
90 .unwrap()
91 .parse()
92 .unwrap()
93 })
94 .unwrap_or_else(lowest_non_deprecated_compat_level)
95}
96
97fn get_lintian_compat_levels() -> &'static SupportedCompatLevels {
101 lazy_static::lazy_static! {
102 static ref LINTIAN_COMPAT_LEVELS: SupportedCompatLevels = {
103 let output = std::process::Command::new("dh_assistant")
106 .arg("supported-compat-levels")
107 .output()
108 .expect("failed to run dh_assistant")
109 .stdout;
110 serde_json::from_slice(&output).expect("failed to parse dh_assistant output")
111 };
112 };
113 &LINTIAN_COMPAT_LEVELS
114}
115
116#[derive(Debug, serde::Deserialize)]
117#[allow(dead_code)]
118struct SupportedCompatLevels {
119 #[serde(rename = "HIGHEST_STABLE_COMPAT_LEVEL")]
120 highest_stable_compat_level: u8,
121 #[serde(rename = "LOWEST_NON_DEPRECATED_COMPAT_LEVEL")]
122 lowest_non_deprecated_compat_level: u8,
123 #[serde(rename = "LOWEST_VIRTUAL_DEBHELPER_COMPAT_LEVEL")]
124 lowest_virtual_debhelper_compat_level: u8,
125 #[serde(rename = "MAX_COMPAT_LEVEL")]
126 max_compat_level: u8,
127 #[serde(rename = "MIN_COMPAT_LEVEL")]
128 min_compat_level: u8,
129 #[serde(rename = "MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL")]
130 min_compat_level_not_scheduled_for_removal: u8,
131}
132
133pub fn lowest_non_deprecated_compat_level() -> u8 {
135 get_lintian_compat_levels().lowest_non_deprecated_compat_level
136}
137
138pub fn highest_stable_compat_level() -> u8 {
140 get_lintian_compat_levels().highest_stable_compat_level
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum EnsureDebhelperError {
146 DebhelperInWrongField(String),
148 ComplexDebhelperCompatRule,
150 DebhelperCompatWithoutVersion,
152}
153
154impl std::fmt::Display for EnsureDebhelperError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 EnsureDebhelperError::DebhelperInWrongField(field) => {
158 write!(f, "debhelper in {}", field)
159 }
160 EnsureDebhelperError::ComplexDebhelperCompatRule => {
161 write!(f, "Complex rule for debhelper-compat, aborting")
162 }
163 EnsureDebhelperError::DebhelperCompatWithoutVersion => {
164 write!(f, "debhelper-compat without version, aborting")
165 }
166 }
167 }
168}
169
170impl std::error::Error for EnsureDebhelperError {}
171
172pub fn ensure_minimum_debhelper_version(
198 source: &mut debian_control::lossless::Source,
199 minimum_version: &Version,
200) -> Result<bool, EnsureDebhelperError> {
201 for (field_name, rels_opt) in [
203 ("Build-Depends-Arch", source.build_depends_arch()),
204 ("Build-Depends-Indep", source.build_depends_indep()),
205 ] {
206 let Some(rels) = rels_opt else {
207 continue;
208 };
209
210 for entry in rels.entries() {
211 for rel in entry.relations() {
212 if rel.name() == "debhelper-compat" || rel.name() == "debhelper" {
213 return Err(EnsureDebhelperError::DebhelperInWrongField(
214 field_name.to_string(),
215 ));
216 }
217 }
218 }
219 }
220
221 let mut rels = source.build_depends().unwrap_or_default();
222
223 for entry in rels.entries() {
225 let has_debhelper_compat = entry
226 .relations()
227 .any(|rel| rel.name() == "debhelper-compat");
228
229 if !has_debhelper_compat {
230 continue;
231 }
232
233 if entry.relations().count() > 1 {
234 return Err(EnsureDebhelperError::ComplexDebhelperCompatRule);
235 }
236
237 let rel = entry.relations().next().unwrap();
238 let Some((constraint, version)) = rel.version() else {
239 return Err(EnsureDebhelperError::DebhelperCompatWithoutVersion);
240 };
241
242 if constraint != debian_control::relations::VersionConstraint::Equal {
243 return Err(EnsureDebhelperError::ComplexDebhelperCompatRule);
244 }
245
246 if &version >= minimum_version {
247 return Ok(false);
248 }
249 }
250
251 let changed = crate::relations::ensure_minimum_version(&mut rels, "debhelper", minimum_version);
253
254 if changed {
255 source.set_build_depends(&rels);
256 }
257
258 Ok(changed)
259}
260
261pub fn get_sequences(source: &debian_control::lossless::Source) -> impl Iterator<Item = String> {
282 let build_depends = source.build_depends().unwrap_or_default();
283
284 build_depends
285 .entries()
286 .flat_map(|entry| entry.relations().collect::<Vec<_>>())
287 .filter_map(|rel| {
288 let name = rel.name();
289 if name.starts_with("dh-sequence-") {
290 Some(name[12..].to_string())
291 } else {
292 None
293 }
294 })
295 .collect::<Vec<_>>()
296 .into_iter()
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_parse_debhelper_compat() {
305 assert_eq!(super::parse_debhelper_compat("9"), Some(9));
306 assert_eq!(super::parse_debhelper_compat("9 # comment"), Some(9));
307 assert_eq!(
308 super::parse_debhelper_compat("9 # comment # comment"),
309 Some(9)
310 );
311 assert_eq!(super::parse_debhelper_compat(""), None);
312 assert_eq!(super::parse_debhelper_compat(" # comment"), None);
313 }
314
315 #[test]
316 fn test_get_debhelper_compat_level_from_control() {
317 let text = "Source: foo
318Build-Depends: debhelper-compat (= 9)
319
320Package: foo
321Architecture: any
322";
323
324 let control = debian_control::Control::read_relaxed(&mut text.as_bytes())
325 .unwrap()
326 .0;
327
328 assert_eq!(
329 super::get_debhelper_compat_level_from_control(&control),
330 Some(9)
331 );
332 }
333
334 #[test]
335 fn test_get_debhelper_compat_level_from_control_x_dh_compat() {
336 let text = "Source: foo
337X-DH-Compat: 9
338Build-Depends: debhelper
339";
340
341 let control = debian_control::Control::read_relaxed(&mut text.as_bytes())
342 .unwrap()
343 .0;
344
345 assert_eq!(
346 super::get_debhelper_compat_level_from_control(&control),
347 Some(9)
348 );
349 }
350
351 mod ensure_minimum_debhelper_version_tests {
352 use super::*;
353
354 #[test]
355 fn test_already() {
356 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
357 let control = debian_control::Control::read_relaxed(text.as_bytes())
358 .unwrap()
359 .0;
360 let mut source = control.source().unwrap();
361
362 assert!(
363 !ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap()
364 );
365 assert_eq!(
366 source.build_depends().unwrap().to_string(),
367 "debhelper (>= 10)"
368 );
369
370 assert!(!ensure_minimum_debhelper_version(&mut source, &"9".parse().unwrap()).unwrap());
371 assert_eq!(
372 source.build_depends().unwrap().to_string(),
373 "debhelper (>= 10)"
374 );
375 }
376
377 #[test]
378 fn test_already_compat() {
379 let text = "Source: foo\nBuild-Depends: debhelper-compat (= 10)\n";
380 let control = debian_control::Control::read_relaxed(text.as_bytes())
381 .unwrap()
382 .0;
383 let mut source = control.source().unwrap();
384
385 assert!(
386 !ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap()
387 );
388 assert_eq!(
389 source.build_depends().unwrap().to_string(),
390 "debhelper-compat (= 10)"
391 );
392
393 assert!(!ensure_minimum_debhelper_version(&mut source, &"9".parse().unwrap()).unwrap());
394 assert_eq!(
395 source.build_depends().unwrap().to_string(),
396 "debhelper-compat (= 10)"
397 );
398 }
399
400 #[test]
401 fn test_bump() {
402 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
403 let control = debian_control::Control::read_relaxed(text.as_bytes())
404 .unwrap()
405 .0;
406 let mut source = control.source().unwrap();
407
408 assert!(ensure_minimum_debhelper_version(&mut source, &"11".parse().unwrap()).unwrap());
409 assert_eq!(
410 source.build_depends().unwrap().to_string(),
411 "debhelper (>= 11)"
412 );
413 }
414
415 #[test]
416 fn test_bump_compat() {
417 let text = "Source: foo\nBuild-Depends: debhelper-compat (= 10)\n";
418 let control = debian_control::Control::read_relaxed(text.as_bytes())
419 .unwrap()
420 .0;
421 let mut source = control.source().unwrap();
422
423 assert!(ensure_minimum_debhelper_version(&mut source, &"11".parse().unwrap()).unwrap());
424 assert_eq!(
425 source.build_depends().unwrap().to_string(),
426 "debhelper (>= 11), debhelper-compat (= 10)"
427 );
428
429 assert!(
430 ensure_minimum_debhelper_version(&mut source, &"11.1".parse().unwrap()).unwrap()
431 );
432 assert_eq!(
433 source.build_depends().unwrap().to_string(),
434 "debhelper (>= 11.1), debhelper-compat (= 10)"
435 );
436 }
437
438 #[test]
439 fn test_not_set() {
440 let text = "Source: foo\n";
441 let control = debian_control::Control::read_relaxed(text.as_bytes())
442 .unwrap()
443 .0;
444 let mut source = control.source().unwrap();
445
446 assert!(ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap());
447 assert_eq!(
448 source.build_depends().unwrap().to_string(),
449 "debhelper (>= 10)"
450 );
451 }
452
453 #[test]
454 fn test_in_indep() {
455 let text = "Source: foo\nBuild-Depends-Indep: debhelper (>= 9)\n";
456 let control = debian_control::Control::read_relaxed(text.as_bytes())
457 .unwrap()
458 .0;
459 let mut source = control.source().unwrap();
460
461 let result = ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap());
462 assert!(result.is_err());
463 assert_eq!(
464 result.unwrap_err(),
465 EnsureDebhelperError::DebhelperInWrongField("Build-Depends-Indep".to_string())
466 );
467 }
468 }
469
470 mod get_sequences_tests {
471 use super::*;
472
473 #[test]
474 fn test_no_sequences() {
475 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
476 let control = debian_control::Control::read_relaxed(text.as_bytes())
477 .unwrap()
478 .0;
479 let source = control.source().unwrap();
480
481 let sequences: Vec<String> = get_sequences(&source).collect();
482 assert_eq!(sequences, Vec::<String>::new());
483 }
484
485 #[test]
486 fn test_single_sequence() {
487 let text = "Source: foo\nBuild-Depends: dh-sequence-python3, debhelper (>= 10)\n";
488 let control = debian_control::Control::read_relaxed(text.as_bytes())
489 .unwrap()
490 .0;
491 let source = control.source().unwrap();
492
493 let sequences: Vec<String> = get_sequences(&source).collect();
494 assert_eq!(sequences, vec!["python3"]);
495 }
496
497 #[test]
498 fn test_multiple_sequences() {
499 let text = "Source: foo\nBuild-Depends: dh-sequence-python3, dh-sequence-nodejs, debhelper (>= 10)\n";
500 let control = debian_control::Control::read_relaxed(text.as_bytes())
501 .unwrap()
502 .0;
503 let source = control.source().unwrap();
504
505 let sequences: Vec<String> = get_sequences(&source).collect();
506 assert_eq!(sequences, vec!["python3", "nodejs"]);
507 }
508
509 #[test]
510 fn test_no_build_depends() {
511 let text = "Source: foo\n";
512 let control = debian_control::Control::read_relaxed(text.as_bytes())
513 .unwrap()
514 .0;
515 let source = control.source().unwrap();
516
517 let sequences: Vec<String> = get_sequences(&source).collect();
518 assert_eq!(sequences, Vec::<String>::new());
519 }
520 }
521}