diff --git a/Cargo.lock b/Cargo.lock index 5d7bfe9..40f05df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -123,6 +132,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" + [[package]] name = "cc" version = "1.2.19" @@ -391,7 +406,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.57.0", + "windows-core", ] [[package]] @@ -505,6 +520,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -521,6 +545,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -934,6 +959,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "shlex" version = "1.3.0" @@ -1166,7 +1197,9 @@ dependencies = [ "uu_renice", "uu_rev", "uu_setsid", + "uu_uuidgen", "uucore", + "uuid", "xattr", ] @@ -1331,6 +1364,19 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_uuidgen" +version = "0.0.1" +dependencies = [ + "clap", + "nix", + "rand 0.9.1", + "thiserror", + "uucore", + "uuid", + "windows", +] + [[package]] name = "uucore" version = "0.0.30" @@ -1369,6 +1415,29 @@ version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb6d972f580f8223cb7052d8580aea2b7061e368cf476de32ea9457b19459ed" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "atomic", + "getrandom", + "md-5", + "rand 0.9.1", + "sha1_smol", + "uuid-rng-internal", +] + +[[package]] +name = "uuid-rng-internal" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9376f53b15ed85851c10175b5e45f0af556b4853ff3fe335080b337e3828981e" +dependencies = [ + "rand 0.9.1", +] + [[package]] name = "version_check" version = "0.9.5" @@ -1499,7 +1568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", - "windows-core 0.61.0", + "windows-core", "windows-future", "windows-link", "windows-numerics", @@ -1511,19 +1580,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.0", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-core", ] [[package]] @@ -1532,10 +1589,10 @@ version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-implement", + "windows-interface", "windows-link", - "windows-result 0.3.2", + "windows-result", "windows-strings", ] @@ -1545,21 +1602,10 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ - "windows-core 0.61.0", + "windows-core", "windows-link", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.60.0" @@ -1571,17 +1617,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.59.1" @@ -1605,19 +1640,10 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.0", + "windows-core", "windows-link", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 69ed2bc..3e38c23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ feat_common_core = [ "renice", "rev", "setsid", + "uuidgen", ] [workspace.dependencies] @@ -69,6 +70,8 @@ tempfile = "3.9.0" textwrap = { version = "0.16.0", features = ["terminal_size"] } thiserror = "2.0" uucore = "0.0.30" +uuid = { version = "1.16.0", features = ["rng-rand"] } +windows = { version = "0.61.1" } xattr = "1.3.1" [dependencies] @@ -99,6 +102,7 @@ mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", pa renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } +uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path ="src/uu/uuidgen" } [dev-dependencies] # dmesg test require fixed-boot-time feature turned on. @@ -109,6 +113,7 @@ rand = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } uucore = { workspace = true, features = ["entries", "process", "signals"] } +uuid = { workspace = true } [target.'cfg(unix)'.dev-dependencies] nix = { workspace = true, features = ["term"] } diff --git a/README.md b/README.md index 8b16f83..c6892e8 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ First, reimplement the most important tools from util-linux: - `ldattach`: Attaches line discipline to a serial line. - `readprofile`: Reads kernel profiling info. - `i386, linux32, linux64, x86_64`: Set personality flags for execution environment. +- `uuidgen`: Generate different types of UUID. Note: * /bin/more is already implemented in https://github.com/uutils/coreutils diff --git a/src/uu/uuidgen/Cargo.toml b/src/uu/uuidgen/Cargo.toml new file mode 100644 index 0000000..5921a45 --- /dev/null +++ b/src/uu/uuidgen/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "uu_uuidgen" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/uuidgen.rs" + +[[bin]] +name = "uuidgen" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +rand = { workspace = true } +thiserror = { workspace = true } +uucore = { workspace = true } +uuid = { workspace = true, features = ["v1", "v3", "v4", "v5"] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { workspace = true, features = ["Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_Networking_WinSock"] } + +[target.'cfg(all(target_family = "unix", not(target_os = "redox")))'.dependencies] +nix = { workspace = true, features = ["net"] } diff --git a/src/uu/uuidgen/src/main.rs b/src/uu/uuidgen/src/main.rs new file mode 100644 index 0000000..7801442 --- /dev/null +++ b/src/uu/uuidgen/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_uuidgen); diff --git a/src/uu/uuidgen/src/uuidgen.rs b/src/uu/uuidgen/src/uuidgen.rs new file mode 100644 index 0000000..69f7e47 --- /dev/null +++ b/src/uu/uuidgen/src/uuidgen.rs @@ -0,0 +1,209 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(target_os = "windows")] +use windows::Win32::{ + Foundation::{ERROR_BUFFER_OVERFLOW, ERROR_SUCCESS}, + NetworkManagement::IpHelper::{ + GetAdaptersAddresses, GAA_FLAG_SKIP_ANYCAST, GAA_FLAG_SKIP_DNS_SERVER, + GAA_FLAG_SKIP_FRIENDLY_NAME, GAA_FLAG_SKIP_MULTICAST, GAA_FLAG_SKIP_UNICAST, + IP_ADAPTER_ADDRESSES_LH, + }, + Networking::WinSock::AF_UNSPEC, +}; + +use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; + +#[cfg(all(target_family = "unix", not(target_os = "redox")))] +use nix::ifaddrs::getifaddrs; +use uucore::{ + error::{UResult, USimpleError}, + format_usage, help_about, help_usage, +}; +use uuid::Uuid; + +const ABOUT: &str = help_about!("uuidgen.md"); +const USAGE: &str = help_usage!("uuidgen.md"); + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = uu_app().try_get_matches_from_mut(args)?; + + let md5 = args.get_flag(options::MD5); + let sha1 = args.get_flag(options::SHA1); + + let namespace = args.get_one(options::NAMESPACE); + let name: Option<&String> = args.get_one(options::NAME); + + // https://github.com/clap-rs/clap/issues/1537 + if !(md5 || sha1) && (namespace.is_some() || name.is_some()) { + return Err(USimpleError::new( + 1, + "--namespace and --name arguments require either --md5 or --sha1", + )); + } + + if args.get_flag(options::TIME) { + let node_id = get_node_id().unwrap_or_else(|| { + let mut default: [u8; 6] = rand::random(); + default[0] |= 0x01; + default + }); + println!("{:?}", Uuid::now_v1(&node_id)); + } else if md5 || sha1 { + let f = if md5 { Uuid::new_v3 } else { Uuid::new_v5 }; + + println!( + "{:?}", + f( + namespace.expect("namespace arg to be set"), + name.expect("name to be set").as_bytes() + ) + ); + } else { + // Random is the default + println!("{}", Uuid::new_v4()); + } + + Ok(()) +} + +pub fn uu_app() -> Command { + let all_uuid_types = [options::RANDOM, options::TIME, options::MD5, options::SHA1]; + + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new(options::RANDOM) + .short('r') + .long(options::RANDOM) + .action(ArgAction::SetTrue) + .help("generate random UUID (v4)"), + ) + .arg( + Arg::new(options::TIME) + .short('t') + .long(options::TIME) + .action(ArgAction::SetTrue) + .help("generate time UUID (v1)"), + ) + .arg( + Arg::new(options::NAMESPACE) + .short('n') + .long(options::NAMESPACE) + .action(ArgAction::Set) + .value_parser(namespace_from_str) + .help("namespace for md5/sha1 - one of: @dns @url @oid @x500"), + ) + .arg( + Arg::new(options::NAME) + .short('N') + .long(options::NAME) + .action(ArgAction::Set) + .help("name for md5/sha1"), + ) + .arg( + Arg::new(options::MD5) + .short('m') + .long(options::MD5) + .action(ArgAction::SetTrue) + .requires_all([options::NAMESPACE, options::NAME]) + .help("generate md5 UUID (v3)"), + ) + .arg( + Arg::new(options::SHA1) + .short('s') + .long(options::SHA1) + .action(ArgAction::SetTrue) + .requires_all([options::NAMESPACE, options::NAME]) + .help("generate sha1 UUID (v5)"), + ) + .group(ArgGroup::new("mode").args(all_uuid_types).multiple(false)) +} + +fn namespace_from_str(s: &str) -> Result { + match s { + "@dns" => Ok(Uuid::NAMESPACE_DNS), + "@url" => Ok(Uuid::NAMESPACE_URL), + "@oid" => Ok(Uuid::NAMESPACE_OID), + "@x500" => Ok(Uuid::NAMESPACE_X500), + _ => Err(USimpleError { + code: 1, + message: format!("Invalid namespace {}.", s), + }), + } +} + +mod options { + pub const RANDOM: &str = "random"; + pub const TIME: &str = "time"; + pub const MD5: &str = "md5"; + pub const SHA1: &str = "sha1"; + pub const NAMESPACE: &str = "namespace"; + pub const NAME: &str = "name"; +} + +#[cfg(target_os = "windows")] +fn get_node_id() -> Option<[u8; 6]> { + unsafe { + // Skip everything we can - we are only interested in PhysicalAddress + let flags = GAA_FLAG_SKIP_UNICAST + | GAA_FLAG_SKIP_ANYCAST + | GAA_FLAG_SKIP_MULTICAST + | GAA_FLAG_SKIP_DNS_SERVER + | GAA_FLAG_SKIP_FRIENDLY_NAME; + + let mut size = 0; + let ret = GetAdaptersAddresses(AF_UNSPEC.0 as u32, flags, None, None, &mut size); + if ret != ERROR_BUFFER_OVERFLOW.0 { + return None; + } + + let mut buf = vec![0u8; size as usize]; + let ret = GetAdaptersAddresses( + AF_UNSPEC.0 as u32, + flags, + None, + Some(buf.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH), + &mut size, + ); + if ret != ERROR_SUCCESS.0 { + return None; + } + + // SAFETY: GetAdaptersAddresses returns ERROR_NO_DATA error if it's zero len + let mut adapter_ptr = buf.as_ptr() as *const IP_ADAPTER_ADDRESSES_LH; + while !adapter_ptr.is_null() { + let adapter = adapter_ptr.read(); + + if adapter.PhysicalAddressLength == 6 { + return Some(adapter.PhysicalAddress[0..6].try_into().unwrap()); + } + + adapter_ptr = adapter.Next; + } + } + None +} + +#[cfg(all(target_family = "unix", not(target_os = "redox")))] +fn get_node_id() -> Option<[u8; 6]> { + getifaddrs().ok().and_then(|iflist| { + iflist + .filter_map(|intf| intf.address?.as_link_addr()?.addr()) + .find(|mac| mac.iter().any(|x| *x != 0)) + }) +} + +#[cfg(not(any( + target_os = "windows", + all(target_family = "unix", not(target_os = "redox")) +)))] +fn get_node_id() -> Option<[u8; 6]> { + None +} diff --git a/src/uu/uuidgen/uuidgen.md b/src/uu/uuidgen/uuidgen.md new file mode 100644 index 0000000..6e4cd23 --- /dev/null +++ b/src/uu/uuidgen/uuidgen.md @@ -0,0 +1,11 @@ +# uuidgen +``` +uuidgen [options] +``` + + +Create UUIDs: + - v1: time + MAC address (-t/--time) + - v3: namespace + name, MD5 based (-m/--md5) + - v4: random (-r/--random) + - v5: namespace + name, SHA1 based (-s/--sha1) diff --git a/tests/by-util/test_uuidgen.rs b/tests/by-util/test_uuidgen.rs new file mode 100644 index 0000000..e6eaa48 --- /dev/null +++ b/tests/by-util/test_uuidgen.rs @@ -0,0 +1,98 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use uuid::Uuid; + +use crate::common::util::{TestScenario, UCommand}; + +fn assert_ver_eq(cmd: &mut UCommand, ver: uuid::Version) { + let uuid = Uuid::parse_str( + cmd.succeeds() + .stdout_str() + .strip_suffix('\n') + .expect("newline"), + ) + .expect("valid UUID"); + assert_eq!(uuid.get_variant(), uuid::Variant::RFC4122); + assert_eq!(uuid.get_version(), Some(ver)); +} + +#[test] +fn test_random() { + assert_ver_eq(&mut new_ucmd!(), uuid::Version::Random); + assert_ver_eq(new_ucmd!().arg("-r"), uuid::Version::Random); + assert_ver_eq(new_ucmd!().arg("--random"), uuid::Version::Random); +} + +#[test] +fn test_time() { + assert_ver_eq(new_ucmd!().arg("-t"), uuid::Version::Mac); + assert_ver_eq(new_ucmd!().arg("--time"), uuid::Version::Mac); +} + +#[test] +fn test_arg_conflict() { + new_ucmd!().args(&["-r", "-t"]).fails().code_is(1); + new_ucmd!().args(&["--time", "--random"]).fails().code_is(1); +} + +#[test] +fn test_md5_sha1() { + new_ucmd!() + .args(&["--namespace", "@dns", "--name", "example.com", "-m"]) + .succeeds() + .stdout_only("9073926b-929f-31c2-abc9-fad77ae3e8eb\n"); + new_ucmd!() + .args(&["-s", "--namespace", "@dns", "--name", "foobar"]) + .succeeds() + .stdout_only("a050b517-6677-5119-9a77-2d26bbf30507\n"); + new_ucmd!() + .args(&["-s", "--namespace", "@url", "--name", "foobar"]) + .succeeds() + .stdout_only("8304efdd-bd6e-5b7c-a27f-83f3f05c64e0\n"); + new_ucmd!() + .args(&["--sha1", "--namespace", "@oid", "--name", "foobar"]) + .succeeds() + .stdout_only("364c03e1-bcdc-58bb-94ed-43e9a92f5f08\n"); + new_ucmd!() + .args(&["--sha1", "--namespace", "@x500", "--name", "foobar"]) + .succeeds() + .stdout_only("34da942e-f4a3-5169-9c65-267d2b22cf11\n"); +} + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("-Z").fails().code_is(1); + new_ucmd!().args(&["-r", "-Z"]).fails().code_is(1); +} + +#[test] +fn test_name_namespace_on_non_hash() { + new_ucmd!() + .args(&["--namespace", "@dns", "-r"]) + .fails() + .code_is(1); + new_ucmd!() + .args(&["--name", "example.com", "-r"]) + .fails() + .code_is(1); + new_ucmd!() + .args(&["--namespace", "@dns", "--name", "example.com", "-r"]) + .fails() + .code_is(1); +} + +#[test] +fn test_missing_name_namespace() { + new_ucmd!().arg("--sha1").fails().code_is(1); + new_ucmd!() + .args(&["--sha1", "--namespace", "@dns"]) + .fails() + .code_is(1); + new_ucmd!() + .args(&["--sha1", "--name", "example.com"]) + .fails() + .code_is(1); +} diff --git a/tests/tests.rs b/tests/tests.rs index 0fb3ed1..d89d95d 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -60,3 +60,7 @@ mod test_fsfreeze; #[cfg(feature = "mcookie")] #[path = "by-util/test_mcookie.rs"] mod test_mcookie; + +#[cfg(feature = "uuidgen")] +#[path = "by-util/test_uuidgen.rs"] +mod test_uuidgen;