Skip to main content

linuxutils_misc/
uuidparse.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7    io::{self, BufRead},
8    process::ExitCode,
9};
10use uuid::Uuid;
11
12const ALL_COLUMNS: &[&str] = &["UUID", "VARIANT", "TYPE", "TIME"];
13
14#[derive(Parser)]
15#[command(
16    name = "uuidparse",
17    version,
18    about = "A utility to parse unique identifiers",
19    after_help = "Available output columns:\n     UUID  unique identifier\n  VARIANT  variant name\n     TYPE  type name\n     TIME  timestamp"
20)]
21pub struct Args {
22    /// Use JSON output format
23    #[arg(short = 'J', long = "json")]
24    json: bool,
25
26    /// Do not print a header line
27    #[arg(short = 'n', long = "noheadings")]
28    noheadings: bool,
29
30    /// Specify which output columns to print (UUID,VARIANT,TYPE,TIME)
31    #[arg(short = 'o', long = "output", value_delimiter = ',')]
32    output: Option<Vec<String>>,
33
34    /// Use the raw output format
35    #[arg(short = 'r', long = "raw")]
36    raw: bool,
37
38    /// UUIDs to parse
39    pub uuids: Vec<String>,
40}
41
42struct UuidInfo {
43    uuid: String,
44    variant: String,
45    type_name: String,
46    time: String,
47}
48
49pub fn run(args: Args) -> ExitCode {
50    let columns = match &args.output {
51        Some(cols) => {
52            let mut result = Vec::new();
53            for col in cols {
54                let upper = col.to_uppercase();
55                if ALL_COLUMNS.contains(&upper.as_str()) {
56                    result.push(upper);
57                } else {
58                    eprintln!("uuidparse: unknown column: {col}");
59                    return ExitCode::FAILURE;
60                }
61            }
62            result
63        }
64        None => ALL_COLUMNS.iter().map(|s| s.to_string()).collect(),
65    };
66
67    let mut inputs: Vec<String> = args.uuids.clone();
68
69    // If no arguments, read from stdin.
70    if inputs.is_empty() {
71        let stdin = io::stdin();
72        for line in stdin.lock().lines() {
73            let Ok(line) = line else { break };
74            for word in line.split_whitespace() {
75                inputs.push(word.to_string());
76            }
77        }
78    }
79
80    if inputs.is_empty() {
81        return ExitCode::SUCCESS;
82    }
83
84    let infos: Vec<UuidInfo> =
85        inputs.iter().map(|s| parse_uuid_info(s)).collect();
86
87    if args.json {
88        print_json(&infos, &columns);
89    } else if args.raw {
90        print_table(&infos, &columns, !args.noheadings, true);
91    } else {
92        print_table(&infos, &columns, !args.noheadings, false);
93    }
94
95    ExitCode::SUCCESS
96}
97
98fn parse_uuid_info(s: &str) -> UuidInfo {
99    let (variant, type_name, time) = match Uuid::parse_str(s) {
100        Ok(u) => {
101            let variant = variant_name(&u);
102            let type_name = type_name(&u);
103            let time = format_time(&u);
104            (variant, type_name, time)
105        }
106        Err(_) => ("invalid".to_string(), "invalid".to_string(), String::new()),
107    };
108
109    UuidInfo {
110        uuid: s.to_string(),
111        variant,
112        type_name,
113        time,
114    }
115}
116
117fn variant_name(u: &Uuid) -> String {
118    match u.get_variant() {
119        uuid::Variant::NCS => "NCS".to_string(),
120        uuid::Variant::RFC4122 => "DCE".to_string(),
121        uuid::Variant::Microsoft => "Microsoft".to_string(),
122        uuid::Variant::Future => "other".to_string(),
123        _ => "other".to_string(),
124    }
125}
126
127fn type_name(u: &Uuid) -> String {
128    if u.is_nil() {
129        return "nil".to_string();
130    }
131    match u.get_version() {
132        Some(uuid::Version::Mac) => "time-based".to_string(),
133        Some(uuid::Version::Dce) => "DCE".to_string(),
134        Some(uuid::Version::Md5) => "name-based".to_string(),
135        Some(uuid::Version::Random) => "random".to_string(),
136        Some(uuid::Version::Sha1) => "sha1-based".to_string(),
137        Some(uuid::Version::SortMac) => "time-v6".to_string(),
138        Some(uuid::Version::SortRand) => "time-v7".to_string(),
139        _ => "unknown".to_string(),
140    }
141}
142
143fn format_time(u: &Uuid) -> String {
144    let ts = match u.get_timestamp() {
145        Some(ts) => ts,
146        None => return String::new(),
147    };
148
149    let (secs, nanos) = ts.to_unix();
150    // Format as ISO-ish timestamp matching util-linux output.
151    let total_secs = secs as i64;
152    let micros = nanos / 1000;
153
154    // Use libc to get local time.
155    let mut tm: libc::tm = unsafe { std::mem::zeroed() };
156    unsafe { libc::localtime_r(&total_secs as *const i64, &mut tm) };
157
158    let mut tz_buf = [0u8; 8];
159    let tz_len = unsafe {
160        libc::strftime(
161            tz_buf.as_mut_ptr() as *mut libc::c_char,
162            tz_buf.len(),
163            c"%z".as_ptr(),
164            &tm,
165        )
166    };
167    let tz = std::str::from_utf8(&tz_buf[..tz_len]).unwrap_or("");
168    // Insert colon: +0200 -> +02:00
169    let tz_formatted = if tz.len() == 5 {
170        format!("{}:{}", &tz[..3], &tz[3..])
171    } else {
172        tz.to_string()
173    };
174
175    format!(
176        "{:04}-{:02}-{:02} {:02}:{:02}:{:02},{:06}{}",
177        tm.tm_year + 1900,
178        tm.tm_mon + 1,
179        tm.tm_mday,
180        tm.tm_hour,
181        tm.tm_min,
182        tm.tm_sec,
183        micros,
184        tz_formatted,
185    )
186}
187
188fn column_value<'a>(info: &'a UuidInfo, col: &str) -> &'a str {
189    match col {
190        "UUID" => &info.uuid,
191        "VARIANT" => &info.variant,
192        "TYPE" => &info.type_name,
193        "TIME" => &info.time,
194        _ => "",
195    }
196}
197
198fn min_column_width(col: &str) -> usize {
199    // Match util-linux's column widths (libsmartcols).
200    // These include trailing padding to match the original output.
201    match col {
202        "UUID" => 37,
203        "VARIANT" => 7,
204        "TYPE" => 10,
205        "TIME" => 4,
206        _ => col.len(),
207    }
208}
209
210fn print_table(
211    infos: &[UuidInfo],
212    columns: &[String],
213    header: bool,
214    raw: bool,
215) {
216    // Calculate column widths.
217    let mut widths: Vec<usize> =
218        columns.iter().map(|c| min_column_width(c)).collect();
219    if !raw {
220        for info in infos {
221            for (i, col) in columns.iter().enumerate() {
222                widths[i] = widths[i].max(column_value(info, col).len());
223            }
224        }
225    }
226
227    let sep = " ";
228
229    if header {
230        let parts: Vec<String> = columns
231            .iter()
232            .enumerate()
233            .map(|(i, col)| {
234                if raw || i == columns.len() - 1 {
235                    col.to_string()
236                } else {
237                    format!("{:<width$}", col, width = widths[i])
238                }
239            })
240            .collect();
241        println!("{}", parts.join(sep));
242    }
243
244    for info in infos {
245        let parts: Vec<String> = columns
246            .iter()
247            .enumerate()
248            .map(|(i, col)| {
249                let val = column_value(info, col);
250                if raw || i == columns.len() - 1 {
251                    val.to_string()
252                } else {
253                    format!("{:<width$}", val, width = widths[i])
254                }
255            })
256            .collect();
257        println!("{}", parts.join(sep));
258    }
259}
260
261fn print_json(infos: &[UuidInfo], columns: &[String]) {
262    println!("{{");
263    println!("   \"uuids\": [");
264    for (i, info) in infos.iter().enumerate() {
265        println!("      {{");
266        let mut first = true;
267        for col in columns {
268            if !first {
269                println!(",");
270            }
271            first = false;
272            let key = col.to_lowercase();
273            let val = column_value(info, col);
274            if val.is_empty() {
275                print!("         \"{key}\": null");
276            } else {
277                print!("         \"{key}\": \"{val}\"");
278            }
279        }
280        println!();
281        if i + 1 < infos.len() {
282            println!("      }},");
283        } else {
284            println!("      }}");
285        }
286    }
287    println!("   ]");
288    println!("}}");
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn parse_v4() {
297        let info = parse_uuid_info("550e8400-e29b-41d4-a716-446655440000");
298        assert_eq!(info.variant, "DCE");
299        assert_eq!(info.type_name, "random");
300    }
301
302    #[test]
303    fn parse_nil() {
304        let info = parse_uuid_info("00000000-0000-0000-0000-000000000000");
305        assert_eq!(info.type_name, "nil");
306    }
307
308    #[test]
309    fn parse_random() {
310        let u = Uuid::new_v4();
311        let info = parse_uuid_info(&u.to_string());
312        assert_eq!(info.variant, "DCE");
313        assert_eq!(info.type_name, "random");
314    }
315
316    #[test]
317    fn parse_invalid() {
318        let info = parse_uuid_info("not-a-uuid");
319        assert_eq!(info.variant, "invalid");
320        assert_eq!(info.type_name, "invalid");
321    }
322}