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 #[arg(short = 'J', long = "json")]
24 json: bool,
25
26 #[arg(short = 'n', long = "noheadings")]
28 noheadings: bool,
29
30 #[arg(short = 'o', long = "output", value_delimiter = ',')]
32 output: Option<Vec<String>>,
33
34 #[arg(short = 'r', long = "raw")]
36 raw: bool,
37
38 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 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 let total_secs = secs as i64;
152 let micros = nanos / 1000;
153
154 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 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 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 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}