Skip to main content

linuxutils_misc/
uuidgen.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::process::ExitCode;
7use uuid::Uuid;
8
9#[derive(Parser)]
10#[command(name = "uuidgen", version, about = "Create a new UUID value")]
11pub struct Args {
12    /// Generate a random-based UUID (v4)
13    #[arg(short = 'r', long = "random")]
14    random: bool,
15
16    /// Generate a time-based UUID (v1)
17    #[arg(short = 't', long = "time")]
18    time: bool,
19
20    /// Generate a time-based UUID (v6), lexicographically sortable
21    #[arg(short = '6', long = "time-v6")]
22    time_v6: bool,
23
24    /// Generate a time-based UUID (v7), lexicographically sortable
25    #[arg(short = '7', long = "time-v7")]
26    time_v7: bool,
27
28    /// Use MD5 as the hash algorithm (v3)
29    #[arg(
30        short = 'm',
31        long = "md5",
32        requires = "namespace",
33        requires = "name"
34    )]
35    md5: bool,
36
37    /// Use SHA1 as the hash algorithm (v5)
38    #[arg(
39        short = 's',
40        long = "sha1",
41        requires = "namespace",
42        requires = "name"
43    )]
44    sha1: bool,
45
46    /// Namespace UUID or alias (@dns, @url, @oid, @x500)
47    #[arg(short = 'n', long = "namespace")]
48    namespace: Option<String>,
49
50    /// Name to hash
51    #[arg(short = 'N', long = "name")]
52    name: Option<String>,
53
54    /// Number of UUIDs to generate
55    #[arg(short = 'C', long = "count", default_value = "1")]
56    count: usize,
57
58    /// Interpret name as a hexadecimal string
59    #[arg(short = 'x', long = "hex")]
60    hex: bool,
61}
62
63pub fn run(args: Args) -> ExitCode {
64    for _ in 0..args.count {
65        let u = match generate(&args) {
66            Ok(u) => u,
67            Err(e) => {
68                eprintln!("uuidgen: {e}");
69                return ExitCode::FAILURE;
70            }
71        };
72        println!("{}", u.as_hyphenated());
73    }
74    ExitCode::SUCCESS
75}
76
77fn generate(args: &Args) -> Result<Uuid, String> {
78    if args.md5 || args.sha1 {
79        let ns = parse_namespace(args.namespace.as_deref().unwrap())?;
80        let name_str = args.name.as_deref().unwrap();
81        let name_bytes = if args.hex {
82            hex_decode(name_str)?
83        } else {
84            name_str.as_bytes().to_vec()
85        };
86        if args.md5 {
87            Ok(Uuid::new_v3(&ns, &name_bytes))
88        } else {
89            Ok(Uuid::new_v5(&ns, &name_bytes))
90        }
91    } else if args.time {
92        Ok(generate_v1())
93    } else if args.time_v6 {
94        let ts = uuid::timestamp::Timestamp::now(uuid::NoContext);
95        let node = random_node_id();
96        Ok(Uuid::new_v6(ts, &node))
97    } else if args.time_v7 {
98        let ts = uuid::timestamp::Timestamp::now(uuid::NoContext);
99        Ok(Uuid::new_v7(ts))
100    } else {
101        // Default: random (v4).
102        Ok(Uuid::new_v4())
103    }
104}
105
106fn parse_namespace(ns: &str) -> Result<Uuid, String> {
107    match ns {
108        "@dns" => Ok(Uuid::NAMESPACE_DNS),
109        "@url" => Ok(Uuid::NAMESPACE_URL),
110        "@oid" => Ok(Uuid::NAMESPACE_OID),
111        "@x500" => Ok(Uuid::NAMESPACE_X500),
112        other => Uuid::parse_str(other)
113            .map_err(|e| format!("invalid namespace: {e}")),
114    }
115}
116
117fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
118    let s = s.replace([' ', '-', ':'], "");
119    if !s.len().is_multiple_of(2) {
120        return Err("hex string must have even length".to_string());
121    }
122    (0..s.len())
123        .step_by(2)
124        .map(|i| {
125            u8::from_str_radix(&s[i..i + 2], 16)
126                .map_err(|e| format!("invalid hex: {e}"))
127        })
128        .collect()
129}
130
131fn generate_v1() -> Uuid {
132    let ts = uuid::timestamp::Timestamp::now(uuid::NoContext);
133    let node = random_node_id();
134    Uuid::new_v1(ts, &node)
135}
136
137fn random_node_id() -> [u8; 6] {
138    let u = Uuid::new_v4();
139    let mut node = [0u8; 6];
140    node.copy_from_slice(&u.as_bytes()[..6]);
141    node[0] |= 0x01; // multicast bit
142    node
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn default_generates_v4() {
151        let args = Args {
152            random: false,
153            time: false,
154            time_v6: false,
155            time_v7: false,
156            md5: false,
157            sha1: false,
158            namespace: None,
159            name: None,
160            count: 1,
161            hex: false,
162        };
163        let u = generate(&args).unwrap();
164        assert_eq!(u.get_version(), Some(uuid::Version::Random));
165    }
166
167    #[test]
168    fn sha1_is_deterministic() {
169        let args = Args {
170            random: false,
171            time: false,
172            time_v6: false,
173            time_v7: false,
174            md5: false,
175            sha1: true,
176            namespace: Some("@dns".to_string()),
177            name: Some("example.com".to_string()),
178            count: 1,
179            hex: false,
180        };
181        let u1 = generate(&args).unwrap();
182        let u2 = generate(&args).unwrap();
183        assert_eq!(u1, u2);
184        assert_eq!(u1.get_version(), Some(uuid::Version::Sha1));
185    }
186
187    #[test]
188    fn md5_is_deterministic() {
189        let args = Args {
190            random: false,
191            time: false,
192            time_v6: false,
193            time_v7: false,
194            md5: true,
195            sha1: false,
196            namespace: Some("@dns".to_string()),
197            name: Some("example.com".to_string()),
198            count: 1,
199            hex: false,
200        };
201        let u1 = generate(&args).unwrap();
202        let u2 = generate(&args).unwrap();
203        assert_eq!(u1, u2);
204        assert_eq!(u1.get_version(), Some(uuid::Version::Md5));
205    }
206
207    #[test]
208    fn namespace_aliases() {
209        assert_eq!(parse_namespace("@dns").unwrap(), Uuid::NAMESPACE_DNS);
210        assert_eq!(parse_namespace("@url").unwrap(), Uuid::NAMESPACE_URL);
211        assert_eq!(parse_namespace("@oid").unwrap(), Uuid::NAMESPACE_OID);
212        assert_eq!(parse_namespace("@x500").unwrap(), Uuid::NAMESPACE_X500);
213    }
214
215    #[test]
216    fn hex_decode_works() {
217        assert_eq!(hex_decode("48656c6c6f").unwrap(), b"Hello");
218        assert!(hex_decode("zz").is_err());
219        assert!(hex_decode("abc").is_err());
220    }
221}