1use nu_engine::command_prelude::*;
2use nu_protocol::FromValue;
3use uuid::{Timestamp, Uuid};
4
5#[derive(Clone)]
6pub struct RandomUuid;
7
8impl Command for RandomUuid {
9 fn name(&self) -> &str {
10 "random uuid"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("random uuid")
15 .category(Category::Random)
16 .input_output_types(vec![(Type::Nothing, Type::String)])
17 .param(
18 Flag::new("version")
19 .short('v')
20 .arg(SyntaxShape::Int)
21 .desc(
22 "The UUID version to generate (1, 3, 4, 5, 7). \
23 Defaults to 4 if not specified.",
24 )
25 .completion(Completion::new_list(&["1", "3", "4", "5", "7"])),
26 )
27 .param(
28 Flag::new("namespace")
29 .short('n')
30 .arg(SyntaxShape::String)
31 .desc(
32 "The namespace for v3 and v5 UUIDs (dns, url, oid, x500). \
33 Required for v3 and v5.",
34 )
35 .completion(Completion::new_list(&["dns", "url", "oid", "x500"])),
36 )
37 .named(
38 "name",
39 SyntaxShape::String,
40 "The name string for v3 and v5 UUIDs. Required for v3 and v5.",
41 Some('s'),
42 )
43 .named(
44 "mac",
45 SyntaxShape::String,
46 "The MAC address (node ID) used to generate v1 UUIDs. Required for v1.",
47 Some('m'),
48 )
49 .allow_variants_without_examples(true)
50 }
51
52 fn description(&self) -> &str {
53 "Generate a random uuid string of the specified version."
54 }
55
56 fn search_terms(&self) -> Vec<&str> {
57 vec!["generate", "uuid4", "uuid1", "uuid3", "uuid5", "uuid7"]
58 }
59
60 fn run(
61 &self,
62 engine_state: &EngineState,
63 stack: &mut Stack,
64 call: &Call,
65 _input: PipelineData,
66 ) -> Result<PipelineData, ShellError> {
67 let fix_call_span = |err: ShellError| match err {
68 ShellError::IncorrectValue { msg, val_span, .. } => ShellError::IncorrectValue {
69 msg,
70 val_span,
71 call_span: call.head,
72 },
73 _ => err,
74 };
75 uuid(engine_state, stack, call).map_err(fix_call_span)
76 }
77
78 fn examples(&self) -> Vec<Example<'_>> {
79 vec![
80 Example {
81 description: "Generate a random uuid v4 string (default).",
82 example: "random uuid",
83 result: None,
84 },
85 Example {
86 description: "Generate a uuid v1 string (timestamp-based).",
87 example: "random uuid -v 1 -m 00:11:22:33:44:55",
88 result: None,
89 },
90 Example {
91 description: "Generate a uuid v3 string (namespace with MD5).",
92 example: "random uuid -v 3 -n dns -s example.com",
93 result: None,
94 },
95 Example {
96 description: "Generate a uuid v4 string (random).",
97 example: "random uuid -v 4",
98 result: None,
99 },
100 Example {
101 description: "Generate a uuid v5 string (namespace with SHA1).",
102 example: "random uuid -v 5 -n dns -s example.com",
103 result: None,
104 },
105 Example {
106 description: "Generate a uuid v7 string (timestamp + random).",
107 example: "random uuid -v 7",
108 result: None,
109 },
110 ]
111 }
112}
113
114fn uuid(
115 engine_state: &EngineState,
116 stack: &mut Stack,
117 call: &Call,
118) -> Result<PipelineData, ShellError> {
119 let span = call.head;
120
121 let version: Option<i64> = call.get_flag(engine_state, stack, "version")?;
122 let version = version.unwrap_or(4);
123
124 validate_flags(engine_state, stack, call, span, version)?;
125
126 let uuid_str = match version {
127 1 => {
128 let ts = Timestamp::now(uuid::timestamp::context::NoContext);
129 let MacAddr(node_id) = call
130 .get_flag::<MacAddr>(engine_state, stack, "mac")?
131 .ok_or_else(|| ShellError::MissingParameter {
132 param_name: "mac".to_string(),
133 span,
134 })?;
135 let uuid = Uuid::new_v1(ts, &node_id);
136 uuid.hyphenated().to_string()
137 }
138 3 => {
139 let (namespace, name) = get_namespace_and_name(engine_state, stack, call, span)?;
140 let uuid = Uuid::new_v3(&namespace, name.as_bytes());
141 uuid.hyphenated().to_string()
142 }
143 4 => {
144 let uuid = Uuid::new_v4();
145 uuid.hyphenated().to_string()
146 }
147 5 => {
148 let (namespace, name) = get_namespace_and_name(engine_state, stack, call, span)?;
149 let uuid = Uuid::new_v5(&namespace, name.as_bytes());
150 uuid.hyphenated().to_string()
151 }
152 7 => {
153 let ts = Timestamp::now(uuid::timestamp::context::NoContext);
154 let uuid = Uuid::new_v7(ts);
155 uuid.hyphenated().to_string()
156 }
157 _ => {
158 return Err(ShellError::IncorrectValue {
159 msg: format!(
160 "Unsupported UUID version: {version}. Supported versions are 1, 3, 4, 5, and 7."
161 ),
162 val_span: span,
163 call_span: span,
164 });
165 }
166 };
167
168 Ok(PipelineData::value(Value::string(uuid_str, span), None))
169}
170
171fn validate_flags(
172 engine_state: &EngineState,
173 stack: &mut Stack,
174 call: &Call,
175 span: Span,
176 version: i64,
177) -> Result<(), ShellError> {
178 match version {
179 1 => {
180 if call
181 .get_flag::<Option<String>>(engine_state, stack, "namespace")?
182 .is_some()
183 {
184 return Err(ShellError::IncompatibleParametersSingle {
185 msg: "version 1 uuid does not take namespace as a parameter".to_string(),
186 span,
187 });
188 }
189 if call
190 .get_flag::<Option<String>>(engine_state, stack, "name")?
191 .is_some()
192 {
193 return Err(ShellError::IncompatibleParametersSingle {
194 msg: "version 1 uuid does not take name as a parameter".to_string(),
195 span,
196 });
197 }
198 }
199 3 | 5 => {
200 if call
201 .get_flag::<Option<String>>(engine_state, stack, "mac")?
202 .is_some()
203 {
204 return Err(ShellError::IncompatibleParametersSingle {
205 msg: "version 3 and 5 uuids do not take mac as a parameter".to_string(),
206 span,
207 });
208 }
209 }
210 v => {
211 if v != 4 && v != 7 {
212 return Err(ShellError::IncorrectValue {
213 msg: format!(
214 "Unsupported UUID version: {v}. Supported versions are 1, 3, 4, 5, and 7."
215 ),
216 val_span: span,
217 call_span: span,
218 });
219 }
220 if call
221 .get_flag::<Option<String>>(engine_state, stack, "mac")?
222 .is_some()
223 {
224 return Err(ShellError::IncompatibleParametersSingle {
225 msg: format!("version {v} uuid does not take mac as a parameter"),
226 span,
227 });
228 }
229 if call
230 .get_flag::<Option<String>>(engine_state, stack, "namespace")?
231 .is_some()
232 {
233 return Err(ShellError::IncompatibleParametersSingle {
234 msg: format!("version {v} uuid does not take namespace as a parameter"),
235 span,
236 });
237 }
238 if call
239 .get_flag::<Option<String>>(engine_state, stack, "name")?
240 .is_some()
241 {
242 return Err(ShellError::IncompatibleParametersSingle {
243 msg: format!("version {v} uuid does not take name as a parameter"),
244 span,
245 });
246 }
247 }
248 }
249 Ok(())
250}
251
252struct MacAddr([u8; 6]);
253
254impl FromValue for MacAddr {
255 fn from_value(v: Value) -> Result<Self, ShellError> {
256 let span = v.span();
257 let s = v.into_string()?;
258
259 let mut buf = [0_u8; 6];
260 let mut mac_parts = s.split(':').map(|x| u8::from_str_radix(x, 0x10));
261
262 let has_6_hexadecimal_parts = buf.iter_mut().all(|ele| match mac_parts.next() {
263 Some(Ok(n)) => {
264 *ele = n;
265 true
266 }
267 _ => false,
268 });
269
270 if has_6_hexadecimal_parts && mac_parts.next().is_none() {
271 Ok(Self(buf))
272 } else {
273 Err(ShellError::IncorrectValue {
274 msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
275 val_span: span,
276 call_span: span,
277 })
278 }
279 }
280}
281
282struct NameSpace(Uuid);
283
284impl FromValue for NameSpace {
285 fn from_value(v: Value) -> Result<Self, ShellError> {
286 let span = v.span();
287 let s = v.into_string()?;
288
289 let namespace = match s.to_lowercase().as_str() {
290 "dns" => Uuid::NAMESPACE_DNS,
291 "url" => Uuid::NAMESPACE_URL,
292 "oid" => Uuid::NAMESPACE_OID,
293 "x500" => Uuid::NAMESPACE_X500,
294 _ => match Uuid::parse_str(&s) {
295 Ok(uuid) => uuid,
296 Err(_) => {
297 return Err(ShellError::IncorrectValue {
298 msg:
299 "Namespace must be one of: dns, url, oid, x500, or a valid UUID string"
300 .to_string(),
301 val_span: span,
302 call_span: span,
303 });
304 }
305 },
306 };
307
308 Ok(Self(namespace))
309 }
310}
311
312fn get_namespace_and_name(
313 engine_state: &EngineState,
314 stack: &mut Stack,
315 call: &Call,
316 span: Span,
317) -> Result<(Uuid, String), ShellError> {
318 let NameSpace(namespace) = call
319 .get_flag::<NameSpace>(engine_state, stack, "namespace")?
320 .ok_or_else(|| ShellError::MissingParameter {
321 param_name: "namespace".to_string(),
322 span,
323 })?;
324
325 let name = call
326 .get_flag::<String>(engine_state, stack, "name")?
327 .ok_or_else(|| ShellError::MissingParameter {
328 param_name: "name".to_string(),
329 span,
330 })?;
331
332 Ok((namespace, name))
333}
334
335#[cfg(test)]
336mod test {
337 use super::*;
338
339 #[test]
340 fn test_examples() -> nu_test_support::Result {
341 nu_test_support::test().examples(RandomUuid)
342 }
343}