1extern crate chrono;
7extern crate csv;
8#[macro_use]
9extern crate failure;
10
11use chrono::naive::NaiveDate;
12use csv::ReaderBuilder;
13use failure::Error;
14
15const UBUNTU_CSV_PATH: &str = "/usr/share/distro-info/ubuntu.csv";
16const DEBIAN_CSV_PATH: &str = "/usr/share/distro-info/debian.csv";
17
18pub enum Distro {
19 Debian,
20 Ubuntu,
21}
22
23impl Distro {
24 pub fn to_string(&self) -> &'static str {
25 match self {
26 Distro::Ubuntu => "Ubuntu",
27 Distro::Debian => "Debian",
28 }
29 }
30}
31
32fn parse_date(field: String) -> Result<NaiveDate, Error> {
33 Ok(NaiveDate::parse_from_str(field.as_str(), "%Y-%m-%d")?)
34}
35
36#[derive(Default, Clone, Debug)]
37pub struct DistroRelease {
38 version: Option<String>,
39 codename: String,
40 series: String,
41 created: Option<NaiveDate>,
42 release: Option<NaiveDate>,
43 eol: Option<NaiveDate>,
44 eol_lts: Option<NaiveDate>,
45 eol_elts: Option<NaiveDate>,
46 eol_esm: Option<NaiveDate>,
47 eol_server: Option<NaiveDate>,
48}
49
50impl DistroRelease {
51 pub fn new(
52 version: String,
53 codename: String,
54 series: String,
55 created: Option<NaiveDate>,
56 release: Option<NaiveDate>,
57 eol: Option<NaiveDate>,
58 eol_lts: Option<NaiveDate>,
59 eol_elts: Option<NaiveDate>,
60 eol_esm: Option<NaiveDate>,
61 eol_server: Option<NaiveDate>,
62 ) -> Self {
63 Self {
64 version: if version.is_empty() {
65 None
66 } else {
67 Some(version)
68 },
69 codename,
70 series,
71 created,
72 release,
73 eol,
74 eol_lts,
75 eol_elts,
76 eol_esm,
77 eol_server,
78 }
79 }
80
81 pub fn version(&self) -> &Option<String> {
83 &self.version
84 }
85 pub fn codename(&self) -> &String {
86 &self.codename
87 }
88 pub fn series(&self) -> &String {
89 &self.series
90 }
91 pub fn created(&self) -> &Option<NaiveDate> {
92 &self.created
93 }
94 pub fn release(&self) -> &Option<NaiveDate> {
95 &self.release
96 }
97 pub fn eol(&self) -> &Option<NaiveDate> {
98 &self.eol
99 }
100 pub fn eol_server(&self) -> &Option<NaiveDate> {
101 &self.eol_server
102 }
103 pub fn eol_esm(&self) -> &Option<NaiveDate> {
104 &self.eol_esm
105 }
106 pub fn eol_elts(&self) -> &Option<NaiveDate> {
107 &self.eol_elts
108 }
109 pub fn eol_lts(&self) -> &Option<NaiveDate> {
110 &self.eol_lts
111 }
112
113 pub fn is_lts(&self) -> bool {
116 self.version
117 .as_ref()
118 .map(|version| version.contains("LTS"))
119 .unwrap_or(false)
120 }
121
122 pub fn created_at(&self, date: NaiveDate) -> bool {
123 match self.created {
124 Some(created) => date >= created,
125 None => false,
126 }
127 }
128
129 pub fn released_at(&self, date: NaiveDate) -> bool {
130 match self.release {
131 Some(release) => date >= release,
132 None => false,
133 }
134 }
135
136 pub fn supported_at(&self, date: NaiveDate) -> bool {
137 self.created_at(date)
138 && match self.eol {
139 Some(eol) => match self.eol_server {
140 Some(eol_server) => date <= ::std::cmp::max(eol, eol_server),
141 None => date <= eol,
142 },
143 None => true,
144 }
145 }
146}
147
148pub trait DistroInfo: Sized {
149 fn distro(&self) -> &Distro;
150 fn releases(&self) -> &Vec<DistroRelease>;
151 fn from_vec(releases: Vec<DistroRelease>) -> Self;
152 fn csv_path() -> &'static str;
154 fn from_csv_reader<T: std::io::Read>(mut rdr: csv::Reader<T>) -> Result<Self, Error> {
159 let columns = rdr.headers()?.clone();
160 let parse_required_str = |field: Option<String>| -> Result<String, Error> {
161 field.ok_or(format_err!("failed to read required option"))
162 };
163 let getfield = |r: &csv::StringRecord, n: &str| -> Option<String> {
164 columns
165 .iter()
166 .position(|header| header == n)
167 .and_then(|i| r.get(i))
168 .map(|s| s.to_string())
169 };
170 let mut releases = vec![];
171 for record in rdr.records() {
172 let record = record?;
173 releases.push(DistroRelease::new(
174 parse_required_str(getfield(&record, "version"))?,
175 parse_required_str(getfield(&record, "codename"))?,
176 parse_required_str(getfield(&record, "series"))?,
177 getfield(&record, "created").map(parse_date).transpose()?,
178 getfield(&record, "release").map(parse_date).transpose()?,
179 getfield(&record, "eol").map(parse_date).transpose()?,
180 getfield(&record, "eol-lts").map(parse_date).transpose()?,
181 getfield(&record, "eol-elts").map(parse_date).transpose()?,
182 getfield(&record, "eol-esm").map(parse_date).transpose()?,
183 getfield(&record, "eol-server")
184 .map(parse_date)
185 .transpose()?,
186 ))
187 }
188 Ok(Self::from_vec(releases))
189 }
190
191 fn new() -> Result<Self, Error> {
193 Self::from_csv_reader(
194 ReaderBuilder::new()
195 .flexible(true)
196 .has_headers(true)
197 .from_path(Self::csv_path())?,
198 )
199 }
200
201 fn all_at(&self, date: NaiveDate) -> Vec<&DistroRelease> {
203 self.releases()
204 .iter()
205 .filter(|distro_release| match distro_release.created {
206 Some(created) => date >= created,
207 None => false,
208 })
209 .collect()
210 }
211
212 fn released(&self, date: NaiveDate) -> Vec<&DistroRelease> {
214 self.releases()
215 .iter()
216 .filter(|distro_release| distro_release.released_at(date))
217 .collect()
218 }
219
220 fn supported(&self, date: NaiveDate) -> Vec<&DistroRelease> {
223 self.releases()
224 .iter()
225 .filter(|distro_release| distro_release.supported_at(date))
226 .collect()
227 }
228
229 fn unsupported(&self, date: NaiveDate) -> Vec<&DistroRelease> {
232 self.released(date)
233 .into_iter()
234 .filter(|distro_release| !distro_release.supported_at(date))
235 .collect()
236 }
237
238 fn ubuntu_devel(&self, date: NaiveDate) -> Vec<&DistroRelease> {
241 self.all_at(date)
242 .into_iter()
243 .filter(|distro_release| match distro_release.release {
244 Some(release) => date < release,
245 None => false,
246 })
247 .collect()
248 }
249
250 fn debian_devel(&self, date: NaiveDate) -> Vec<&DistroRelease> {
253 self.all_at(date)
254 .into_iter()
255 .filter(|distro_release| match distro_release.release {
256 Some(release) => date < release,
257 None => true,
258 })
259 .filter(|distro_release| distro_release.version.is_none())
260 .collect::<Vec<_>>()
261 .first()
262 .copied()
263 .map(|dr| vec![dr])
264 .unwrap_or_else(std::vec::Vec::new)
265 }
266
267 fn latest(&self, date: NaiveDate) -> Option<&DistroRelease> {
269 self.supported(date)
270 .into_iter()
271 .filter(|distro_release| distro_release.released_at(date))
272 .collect::<Vec<_>>()
273 .last()
274 .copied()
275 }
276
277 fn iter(&self) -> ::std::slice::Iter<DistroRelease> {
278 self.releases().iter()
279 }
280}
281
282pub struct UbuntuDistroInfo {
283 releases: Vec<DistroRelease>,
284}
285
286impl DistroInfo for UbuntuDistroInfo {
287 fn distro(&self) -> &Distro {
288 &Distro::Ubuntu
289 }
290 fn releases(&self) -> &Vec<DistroRelease> {
291 &self.releases
292 }
293 fn csv_path() -> &'static str {
294 UBUNTU_CSV_PATH
295 }
296 fn from_vec(releases: Vec<DistroRelease>) -> Self {
298 Self { releases }
299 }
300}
301
302impl IntoIterator for UbuntuDistroInfo {
303 type Item = DistroRelease;
304 type IntoIter = ::std::vec::IntoIter<DistroRelease>;
305
306 fn into_iter(self) -> Self::IntoIter {
307 self.releases.into_iter()
308 }
309}
310
311pub struct DebianDistroInfo {
312 releases: Vec<DistroRelease>,
313}
314
315impl DistroInfo for DebianDistroInfo {
316 fn distro(&self) -> &Distro {
317 &Distro::Debian
318 }
319 fn releases(&self) -> &Vec<DistroRelease> {
320 &self.releases
321 }
322 fn csv_path() -> &'static str {
323 DEBIAN_CSV_PATH
324 }
325 fn from_vec(releases: Vec<DistroRelease>) -> Self {
327 Self { releases }
328 }
329}
330
331impl IntoIterator for DebianDistroInfo {
332 type Item = DistroRelease;
333 type IntoIter = ::std::vec::IntoIter<DistroRelease>;
334
335 fn into_iter(self) -> Self::IntoIter {
336 self.releases.into_iter()
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use chrono::naive::NaiveDate;
343 use {
344 super::DebianDistroInfo, super::DistroInfo, super::DistroRelease, super::UbuntuDistroInfo,
345 };
346
347 #[test]
348 fn create_struct() {
349 DistroRelease {
350 version: Some("version".to_string()),
351 codename: "codename".to_string(),
352 series: "series".to_string(),
353 created: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
354 release: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
355 eol: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
356 eol_server: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
357 ..Default::default()
358 };
359 }
360
361 #[test]
362 fn distro_release_new() {
363 let get_date = |mut n| {
364 let mut date = NaiveDate::from_ymd_opt(2018, 6, 14).unwrap();
365 while n > 0 {
366 date = date.succ_opt().unwrap();
367 n -= 1;
368 }
369 date
370 };
371 let distro_release = DistroRelease::new(
372 "version".to_string(),
373 "codename".to_string(),
374 "series".to_string(),
375 Some(get_date(0)),
376 Some(get_date(1)),
377 Some(get_date(2)),
378 Some(get_date(3)),
379 Some(get_date(4)),
380 Some(get_date(5)),
381 Some(get_date(6)),
382 );
383 assert_eq!(Some("version".to_string()), distro_release.version);
384 assert_eq!("codename", distro_release.codename);
385 assert_eq!("series", distro_release.series);
386 assert_eq!(Some(get_date(0)), distro_release.created);
387 assert_eq!(Some(get_date(1)), distro_release.release);
388 assert_eq!(Some(get_date(2)), distro_release.eol);
389 assert_eq!(Some(get_date(3)), distro_release.eol_lts);
390 assert_eq!(Some(get_date(4)), distro_release.eol_elts);
391 assert_eq!(Some(get_date(5)), distro_release.eol_esm);
392 assert_eq!(Some(get_date(6)), distro_release.eol_server);
393
394 assert_eq!(&Some("version".to_string()), distro_release.version());
395 assert_eq!(&"codename", distro_release.codename());
396 assert_eq!(&"series", distro_release.series());
397 assert_eq!(&Some(get_date(0)), distro_release.created());
398 assert_eq!(&Some(get_date(1)), distro_release.release());
399 assert_eq!(&Some(get_date(2)), distro_release.eol());
400 assert_eq!(&Some(get_date(3)), distro_release.eol_lts());
401 assert_eq!(&Some(get_date(4)), distro_release.eol_elts());
402 assert_eq!(&Some(get_date(5)), distro_release.eol_esm());
403 assert_eq!(&Some(get_date(6)), distro_release.eol_server());
404 }
405
406 #[test]
407 fn distro_release_is_lts() {
408 let distro_release = DistroRelease::new(
409 "98.04 LTS".to_string(),
410 "codename".to_string(),
411 "series".to_string(),
412 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
413 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
414 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
415 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
416 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
417 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
418 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
419 );
420 assert!(distro_release.is_lts());
421
422 let distro_release = DistroRelease::new(
423 "98.04".to_string(),
424 "codename".to_string(),
425 "series".to_string(),
426 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
427 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
428 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
429 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
430 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
431 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
432 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
433 );
434 assert!(!distro_release.is_lts());
435 }
436
437 #[test]
438 fn distro_release_released_at() {
439 let distro_release = DistroRelease::new(
440 "98.04 LTS".to_string(),
441 "codename".to_string(),
442 "series".to_string(),
443 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
444 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
445 Some(NaiveDate::from_ymd_opt(2018, 6, 16).unwrap()),
446 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
447 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
448 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
449 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
450 );
451 assert!(!distro_release.released_at(NaiveDate::from_ymd_opt(2018, 6, 13).unwrap()));
453 assert!(distro_release.released_at(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()));
455 assert!(distro_release.released_at(NaiveDate::from_ymd_opt(2018, 6, 17).unwrap()));
457 }
458
459 #[test]
460 fn distro_release_supported_at() {
461 let distro_release = DistroRelease::new(
462 "98.04 LTS".to_string(),
463 "codename".to_string(),
464 "series".to_string(),
465 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
466 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
467 Some(NaiveDate::from_ymd_opt(2018, 6, 16).unwrap()),
468 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
469 Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
470 None,
471 None,
472 );
473 assert!(!distro_release.supported_at(NaiveDate::from_ymd_opt(2018, 6, 13).unwrap()));
475 assert!(distro_release.supported_at(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()));
477 assert!(!distro_release.supported_at(NaiveDate::from_ymd_opt(2018, 6, 17).unwrap()));
479 }
480
481 #[test]
482 fn debian_distro_info_new() {
483 DebianDistroInfo::new().unwrap();
484 }
485
486 #[test]
487 fn ubuntu_distro_info_new() {
488 UbuntuDistroInfo::new().unwrap();
489 }
490
491 #[test]
492 fn debian_distro_info_item() {
493 let distro_release = DebianDistroInfo::new().unwrap().into_iter().next().unwrap();
494 assert_eq!(Some("1.1".to_string()), distro_release.version);
495 assert_eq!("Buzz", distro_release.codename);
496 assert_eq!("buzz", distro_release.series);
497 assert_eq!(
498 Some(NaiveDate::from_ymd_opt(1993, 8, 16).unwrap()),
499 distro_release.created
500 );
501 assert_eq!(
502 Some(NaiveDate::from_ymd_opt(1996, 6, 17).unwrap()),
503 distro_release.release
504 );
505 assert_eq!(
506 Some(NaiveDate::from_ymd_opt(1997, 6, 5).unwrap()),
507 distro_release.eol
508 );
509 assert_eq!(None, distro_release.eol_server);
510 }
511
512 #[test]
513 fn ubuntu_distro_info_item() {
514 let distro_release = UbuntuDistroInfo::new().unwrap().into_iter().next().unwrap();
515 assert_eq!(Some("4.10".to_string()), distro_release.version);
516 assert_eq!("Warty Warthog", distro_release.codename);
517 assert_eq!("warty", distro_release.series);
518 assert_eq!(
519 Some(NaiveDate::from_ymd_opt(2004, 3, 5).unwrap()),
520 distro_release.created
521 );
522 assert_eq!(
523 Some(NaiveDate::from_ymd_opt(2004, 10, 20).unwrap()),
524 distro_release.release
525 );
526 assert_eq!(
527 Some(NaiveDate::from_ymd_opt(2006, 4, 30).unwrap()),
528 distro_release.eol
529 );
530 assert_eq!(None, distro_release.eol_server);
531 }
532
533 #[test]
534 fn ubuntu_distro_info_eol_server() {
535 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
536 for distro_release in ubuntu_distro_info {
537 match distro_release.series.as_ref() {
538 "breezy" => assert_eq!(None, distro_release.eol_server),
539 "dapper" => {
540 assert_eq!(
541 Some(NaiveDate::from_ymd_opt(2011, 6, 1).unwrap()),
542 distro_release.eol_server
543 );
544 break;
545 }
546 _ => {}
547 }
548 }
549 }
550 #[test]
551 fn ubuntu_distro_info_released() {
552 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
553 let date = NaiveDate::from_ymd_opt(2006, 6, 1).unwrap();
555 let released_series: Vec<String> = ubuntu_distro_info
556 .released(date)
557 .iter()
558 .map(|distro_release| distro_release.series.clone())
559 .collect();
560 assert_eq!(
561 vec![
562 "warty".to_string(),
563 "hoary".to_string(),
564 "breezy".to_string(),
565 "dapper".to_string(),
566 ],
567 released_series
568 );
569 }
570
571 #[test]
572 fn ubuntu_distro_info_supported() {
573 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
574 let date = NaiveDate::from_ymd_opt(2018, 4, 26).unwrap();
576 let supported_series: Vec<String> = ubuntu_distro_info
577 .supported(date)
578 .iter()
579 .map(|distro_release| distro_release.series.clone())
580 .collect();
581 assert_eq!(
582 vec![
583 "trusty".to_string(),
584 "xenial".to_string(),
585 "artful".to_string(),
586 "bionic".to_string(),
587 "cosmic".to_string(),
588 ],
589 supported_series
590 );
591 }
592
593 #[test]
594 fn ubuntu_distro_info_unsupported() {
595 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
596 let date = NaiveDate::from_ymd_opt(2006, 11, 1).unwrap();
598 let unsupported_series: Vec<String> = ubuntu_distro_info
599 .unsupported(date)
600 .iter()
601 .map(|distro_release| distro_release.series.clone())
602 .collect();
603 assert_eq!(
604 vec!["warty".to_string(), "hoary".to_string()],
605 unsupported_series
606 );
607 }
608
609 #[test]
610 fn ubuntu_distro_info_supported_on_eol_day() {
611 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
612 let date = NaiveDate::from_ymd_opt(2018, 7, 19).unwrap();
614 let supported_series: Vec<String> = ubuntu_distro_info
615 .supported(date)
616 .iter()
617 .map(|distro_release| distro_release.series.clone())
618 .collect();
619 assert_eq!(
620 vec![
621 "trusty".to_string(),
622 "xenial".to_string(),
623 "artful".to_string(),
624 "bionic".to_string(),
625 "cosmic".to_string(),
626 ],
627 supported_series
628 );
629 }
630
631 #[test]
632 fn ubuntu_distro_info_supported_with_server_eol() {
633 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
634 let date = NaiveDate::from_ymd_opt(2011, 5, 14).unwrap();
635 let supported_series: Vec<String> = ubuntu_distro_info
636 .supported(date)
637 .iter()
638 .map(|distro_release| distro_release.series.clone())
639 .collect();
640 assert!(supported_series.contains(&"dapper".to_string()));
641 }
642
643 #[test]
644 fn ubuntu_distro_info_devel() {
645 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
646 let date = NaiveDate::from_ymd_opt(2018, 4, 26).unwrap();
647 let devel_series: Vec<String> = ubuntu_distro_info
648 .ubuntu_devel(date)
649 .iter()
650 .map(|distro_release| distro_release.series.clone())
651 .collect();
652 assert_eq!(vec!["cosmic".to_string()], devel_series);
653 }
654
655 #[test]
656 fn ubuntu_distro_info_all_at() {
657 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
658 let date = NaiveDate::from_ymd_opt(2005, 4, 8).unwrap();
659 let all_series: Vec<String> = ubuntu_distro_info
660 .all_at(date)
661 .iter()
662 .map(|distro_release| distro_release.series.clone())
663 .collect();
664 assert_eq!(
665 vec![
666 "warty".to_string(),
667 "hoary".to_string(),
668 "breezy".to_string(),
669 ],
670 all_series
671 );
672 }
673
674 #[test]
675 fn ubuntu_distro_info_latest() {
676 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
677 let date = NaiveDate::from_ymd_opt(2005, 4, 8).unwrap();
678 let latest_series = ubuntu_distro_info.latest(date).unwrap().series.clone();
679 assert_eq!("hoary".to_string(), latest_series);
680 }
681
682 #[test]
683 fn ubuntu_distro_info_iter() {
684 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
685 let iter_suites: Vec<String> = ubuntu_distro_info
686 .iter()
687 .map(|distro_release| distro_release.series.clone())
688 .collect();
689 let mut for_loop_suites = vec![];
690 for distro_release in ubuntu_distro_info {
691 for_loop_suites.push(distro_release.series.clone());
692 }
693 assert_eq!(for_loop_suites, iter_suites);
694 }
695
696 #[test]
697 fn ubuntu_distro_info_iters_are_separate() {
698 let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
699 let mut iter1 = ubuntu_distro_info.iter();
700 let mut iter2 = ubuntu_distro_info.iter();
701 assert_eq!(iter1.next().unwrap().series, iter2.next().unwrap().series);
702 }
703}