Skip to main content

fs_core/cli/
version.rs

1//! The identifying `--version` line: `<tool> (<crate>) <version>`.
2//!
3//! One shape for every name the binary answers to, because the question
4//! it exists to answer is "is the program PATH found ours?", asked by
5//! `doctor` and by every test suite before it trusts a tool.
6
7use super::family::Family;
8
9/// The version clap prints after a command's name: `(<crate>) <version>`,
10/// so `fs.<fs> --version` reads `fs.<fs> (am-fs-<fs>) 1.2.3`.
11pub fn clap_version(family: &Family) -> &'static str {
12    // clap takes a 'static str; one per process, a few dozen bytes.
13    Box::leak(format!("({}) {}", family.crate_name, family.version).into_boxed_str())
14}
15
16/// The whole line for `tool`.
17pub fn line(family: &Family, tool: &str) -> String {
18    format!("{tool} ({}) {}", family.crate_name, family.version)
19}
20
21/// What a version line says, if it has our shape.
22#[derive(Debug, PartialEq, Eq)]
23pub struct Identity {
24    pub tool: String,
25    pub crate_name: String,
26    pub version: String,
27}
28
29/// Read a `--version` answer: the first line, as `<tool> (<crate>)
30/// <version>`. Anything else — another package's banner, a usage message,
31/// nothing at all — is `None`.
32pub fn parse(output: &str) -> Option<Identity> {
33    let first = output.lines().next()?.trim();
34    let (tool, rest) = first.split_once(" (")?;
35    let (crate_name, version) = rest.split_once(") ")?;
36    let well_formed = !tool.is_empty()
37        && !tool.contains(char::is_whitespace)
38        && !crate_name.is_empty()
39        && !crate_name.contains(char::is_whitespace)
40        && version.split('.').count() >= 3
41        && !version.contains(char::is_whitespace);
42    well_formed.then(|| Identity {
43        tool: tool.to_string(),
44        crate_name: crate_name.to_string(),
45        version: version.to_string(),
46    })
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    fn identity(tool: &str, crate_name: &str, version: &str) -> Option<Identity> {
54        Some(Identity {
55            tool: tool.into(),
56            crate_name: crate_name.into(),
57            version: version.into(),
58        })
59    }
60
61    #[test]
62    fn our_shape_is_read() {
63        assert_eq!(
64            parse("mkfs.ext4 (am-fs-ext4) 0.6.0\n"),
65            identity("mkfs.ext4", "am-fs-ext4", "0.6.0")
66        );
67        assert_eq!(
68            parse("img.qcow2 (am-img-qcow2) 0.4.5\ntrailing lines are ignored\n"),
69            identity("img.qcow2", "am-img-qcow2", "0.4.5")
70        );
71    }
72
73    #[test]
74    fn another_package_in_our_shape_is_read_and_names_itself() {
75        // erofs-utils prints our shape and names its own package, which is
76        // how doctor tells it apart: by the crate, not the shape.
77        assert_eq!(
78            parse("mkfs.erofs (erofs-utils) 1.9.1\n"),
79            identity("mkfs.erofs", "erofs-utils", "1.9.1")
80        );
81    }
82
83    #[test]
84    fn other_banners_and_usage_messages_are_nobodys() {
85        for foreign in [
86            "mke2fs 1.47.0 (5-Feb-2023)",
87            "Usage: mkfs.ext4 [-c|-l filename] device",
88            "mkntfs v2022.10.3 (libntfs-3g)",
89            "Usage: mkntfs [options] device [number-of-sectors]",
90            "mkfs.erofs 1.8.10",
91            "Usage: mkfs.erofs [OPTIONS] FILE SOURCE",
92            "unsquashfs version 4.6.1 (2023/03/25)",
93            "btrfs-progs v6.6.3",
94            "xfs_db version 6.1.0",
95            "",
96            // Each half of the shape, broken on its own.
97            "fs.x () 1.2.3",
98            " (am-fs-x) 1.2.3",
99            "two words (am-fs-x) 1.2.3",
100            "fs.x (am fs x) 1.2.3",
101            "fs.x (am-fs-x) 1.2",
102            "fs.x (am-fs-x) 1.2.3 beta",
103        ] {
104            assert_eq!(parse(foreign), None, "{foreign:?}");
105        }
106    }
107
108    #[test]
109    fn the_line_clap_prints_is_the_line_parse_reads() {
110        let family = super::super::Family {
111            repo: "rust-fs-x",
112            crate_name: "am-fs-x",
113            version: "1.2.3",
114            about: "",
115            install_hints: &[],
116            tools: &[],
117        };
118        assert_eq!(clap_version(&family), "(am-fs-x) 1.2.3");
119        assert_eq!(line(&family, "fs.x"), "fs.x (am-fs-x) 1.2.3");
120        assert_eq!(
121            parse(&line(&family, "fs.x")),
122            identity("fs.x", "am-fs-x", "1.2.3")
123        );
124    }
125}