From f0e503709036c2448c150c1e127190733f30a90d Mon Sep 17 00:00:00 2001 From: kazk Date: Sat, 12 Dec 2020 02:56:36 -0800 Subject: [PATCH] Add files and license --- .github/workflows/ci.yml | 44 +++++++ .gitignore | 2 + Cargo.toml | 42 +++++++ LICENSE.md | 22 ++++ README.md | 33 +++++ benches/xid_id_from_str.rs | 14 +++ benches/xid_new.rs | 10 ++ benches/xid_new_to_string.rs | 12 ++ examples/gen.rs | 5 + src/generator.rs | 63 ++++++++++ src/id.rs | 237 +++++++++++++++++++++++++++++++++++ src/lib.rs | 74 +++++++++++ src/machine_id.rs | 59 +++++++++ src/pid.rs | 25 ++++ 14 files changed, 642 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 benches/xid_id_from_str.rs create mode 100644 benches/xid_new.rs create mode 100644 benches/xid_new_to_string.rs create mode 100644 examples/gen.rs create mode 100644 src/generator.rs create mode 100644 src/id.rs create mode 100644 src/lib.rs create mode 100644 src/machine_id.rs create mode 100644 src/pid.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..73e5de3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: cargo check + uses: actions-rs/cargo@v1 + with: + command: check + - name: cargo test + uses: actions-rs/cargo@v1 + with: + command: test + - name: cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + - name: cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..37a20a6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "xid" +version = "0.1.0" +license = "MIT" +description = "Globally unique sortable id generator. A Rust port of https://github.com/rs/xid." +keywords = ["id"] +homepage = "https://github.com/kazk/xid-rs" +repository = "https://github.com/kazk/xid-rs" +readme = "README.md" +authors = ["kazk "] +edition = "2018" +exclude = [".github/"] + +[dependencies] +crc32fast = "^1" +hostname = "^0.3" +md5 = "^0.7" +once_cell = "^1" +rand = "^0.7" +thiserror = "^1" + +[target.'cfg(target_os = "macos")'.dependencies] +sysctl = "^0.4" + +[target.'cfg(target_os = "windows")'.dependencies] +winreg = "^0.8" + +[dev-dependencies] +criterion = "0.3" + + +[[bench]] +name = "xid_new" +harness = false + +[[bench]] +name = "xid_new_to_string" +harness = false + +[[bench]] +name = "xid_id_from_str" +harness = false diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6a797eb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2015 Olivier Poitrey +Copyright (c) 2020 kazk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ec8d3b --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# xid + +Globally unique sortable id generator. A Rust port of https://github.com/rs/xid. + +The binary representation is compatible with the Mongo DB 12-byte [ObjectId][object-id]. +The value consists of: + +- a 4-byte timestamp value in seconds since the Unix epoch +- a 3-byte value based on the machine identifier +- a 2-byte value based on the process id +- a 3-byte incrementing counter, initialized to a random value + +The string representation is 20 bytes, using a base32 hex variant with characters `[0-9a-v]` +to retain the sortable property of the id. + +See the original [`xid`] project for more details. + +## Usage + +```rust +use xid; + +fn main() { + println!("{}", xid::new().to_string()); //=> bva9lbqn1bt68k8mj62g +} +``` + +## Examples + +- [`cargo run --example gen`](./examples/gen.rs): Generate xid + +[`xid`]: https://github.com/rs/xid +[object-id]: https://docs.mongodb.org/manual/reference/object-id/ diff --git a/benches/xid_id_from_str.rs b/benches/xid_id_from_str.rs new file mode 100644 index 0000000..a47be26 --- /dev/null +++ b/benches/xid_id_from_str.rs @@ -0,0 +1,14 @@ +use std::str::FromStr; + +use criterion::{criterion_group, criterion_main, Criterion}; + +use xid::Id; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("xid::Id::from_str()", |b| { + b.iter(|| Id::from_str("9m4e2mr0ui3e8a215n4g")) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/benches/xid_new.rs b/benches/xid_new.rs new file mode 100644 index 0000000..be3ec74 --- /dev/null +++ b/benches/xid_new.rs @@ -0,0 +1,10 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use xid; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("xid::new()", |b| b.iter(|| xid::new())); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/benches/xid_new_to_string.rs b/benches/xid_new_to_string.rs new file mode 100644 index 0000000..2d8f6c7 --- /dev/null +++ b/benches/xid_new_to_string.rs @@ -0,0 +1,12 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use xid; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("xid::new().to_string()", |b| { + b.iter(|| xid::new().to_string()) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/examples/gen.rs b/examples/gen.rs new file mode 100644 index 0000000..f830bba --- /dev/null +++ b/examples/gen.rs @@ -0,0 +1,5 @@ +use xid; + +fn main() { + println!("{}", xid::new().to_string()); +} diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..39fb5f4 --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,63 @@ +use std::sync::atomic::{AtomicU32, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use once_cell::sync::OnceCell; +use rand::RngCore; + +use crate::id::{Id, RAW_LEN}; +use crate::machine_id; +use crate::pid; + +#[derive(Debug)] +pub struct Generator { + counter: AtomicU32, + machine_id: [u8; 3], + pid: [u8; 2], +} + +pub fn get() -> &'static Generator { + static INSTANCE: OnceCell = OnceCell::new(); + + INSTANCE.get_or_init(|| Generator { + counter: AtomicU32::new(init_random()), + machine_id: machine_id::get(), + pid: pid::get().to_be_bytes(), + }) +} + +impl Generator { + pub fn new_id(&self) -> Id { + self.with_time(&SystemTime::now()) + } + + fn with_time(&self, time: &SystemTime) -> Id { + // Panic if the time is before the epoch. + let unix_ts = time + .duration_since(UNIX_EPOCH) + .expect("Clock may have gone backwards"); + self.generate(unix_ts.as_secs() as u32) + } + + fn generate(&self, unix_ts: u32) -> Id { + let counter = self.counter.fetch_add(1, Ordering::SeqCst); + + let mut raw = [0u8; RAW_LEN]; + // 4 bytes of Timestamp (big endian) + raw[0..=3].copy_from_slice(&unix_ts.to_be_bytes()); + // 3 bytes of Machine ID + raw[4..=6].copy_from_slice(&self.machine_id); + // 2 bytes of PID + raw[7..=8].copy_from_slice(&self.pid); + // 3 bytes of increment counter (big endian) + raw[9..].copy_from_slice(&counter.to_be_bytes()[1..]); + + Id(raw) + } +} + +// https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id.go#L136 +fn init_random() -> u32 { + let mut bs = [0u8; 3]; + rand::thread_rng().fill_bytes(&mut bs); + u32::from_be_bytes([0, bs[0], bs[1], bs[2]]) +} diff --git a/src/id.rs b/src/id.rs new file mode 100644 index 0000000..8ef5be0 --- /dev/null +++ b/src/id.rs @@ -0,0 +1,237 @@ +use std::str::{self, FromStr}; +use std::string::ToString; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use thiserror::Error; + +pub(crate) const RAW_LEN: usize = 12; +const ENCODED_LEN: usize = 20; +const ENC: &[u8] = "0123456789abcdefghijklmnopqrstuv".as_bytes(); +const DEC: [u8; 256] = gen_dec(); + +/// An ID. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub struct Id(pub [u8; RAW_LEN]); + +impl Id { + /// The binary representation of the id. + pub fn as_bytes(&self) -> &[u8; RAW_LEN] { + let Self(raw) = self; + raw + } + + /// Extract the 3-byte machine id. + pub fn machine(&self) -> [u8; 3] { + let raw = self.as_bytes(); + [raw[4], raw[5], raw[6]] + } + + /// Extract the process id. + pub fn pid(&self) -> u16 { + let raw = self.as_bytes(); + u16::from_be_bytes([raw[7], raw[8]]) + } + + /// Extract the timestamp. + pub fn time(&self) -> SystemTime { + let raw = self.as_bytes(); + let unix_ts = u32::from_be_bytes([raw[0], raw[1], raw[2], raw[3]]); + UNIX_EPOCH + Duration::from_secs(unix_ts as u64) + } + + /// Extract the incrementing counter. + pub fn counter(&self) -> u32 { + // Counter is stored as big-endian 3-byte value + let raw = self.as_bytes(); + u32::from_be_bytes([0, raw[9], raw[10], raw[11]]) + } +} + +impl ToString for Id { + // https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id.go#L208 + /// Returns the string representation of the id. + fn to_string(&self) -> String { + let Self(raw) = self; + let mut bs = [0u8; ENCODED_LEN]; + bs[19] = ENC[((raw[11] << 4) & 31) as usize]; + bs[18] = ENC[((raw[11] >> 1) & 31) as usize]; + bs[17] = ENC[(((raw[11] >> 6) | (raw[10] << 2)) & 31) as usize]; + bs[16] = ENC[(raw[10] >> 3) as usize]; + bs[15] = ENC[(raw[9] & 31) as usize]; + bs[14] = ENC[(((raw[9] >> 5) | (raw[8] << 3)) & 31) as usize]; + bs[13] = ENC[((raw[8] >> 2) & 31) as usize]; + bs[12] = ENC[(((raw[8] >> 7) | (raw[7] << 1)) & 31) as usize]; + bs[11] = ENC[(((raw[7] >> 4) | (raw[6] << 4)) & 31) as usize]; + bs[10] = ENC[((raw[6] >> 1) & 31) as usize]; + bs[9] = ENC[(((raw[6] >> 6) | (raw[5] << 2)) & 31) as usize]; + bs[8] = ENC[(raw[5] >> 3) as usize]; + bs[7] = ENC[(raw[4] & 31) as usize]; + bs[6] = ENC[(((raw[4] >> 5) | (raw[3] << 3)) & 31) as usize]; + bs[5] = ENC[((raw[3] >> 2) & 31) as usize]; + bs[4] = ENC[(((raw[3] >> 7) | (raw[2] << 1)) & 31) as usize]; + bs[3] = ENC[(((raw[2] >> 4) | (raw[1] << 4)) & 31) as usize]; + bs[2] = ENC[((raw[1] >> 1) & 31) as usize]; + bs[1] = ENC[(((raw[1] >> 6) | (raw[0] << 2)) & 31) as usize]; + bs[0] = ENC[(raw[0] >> 3) as usize]; + str::from_utf8(&bs).unwrap().to_string() + } +} + +/// An error which can be returned when parsing an id. +#[derive(Error, Debug, PartialEq)] +pub enum ParseIdError { + /// Returned when the id had length other than 20. + #[error("invalid length {0}")] + InvalidLength(usize), + /// Returned when the id had character not in `[0-9a-v]`. + #[error("invalid character '{0}'")] + InvalidCharacter(char), +} + +impl FromStr for Id { + type Err = ParseIdError; + + // https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id.go#L259 + /// Create an Id from its string representation. + fn from_str(s: &str) -> Result { + if s.len() != 20 { + return Err(ParseIdError::InvalidLength(s.len())); + } + if let Some(c) = s.chars().find(|c| !is_valid_char(c)) { + return Err(ParseIdError::InvalidCharacter(c)); + } + + let bs = s.as_bytes(); + let mut raw = [0u8; RAW_LEN]; + raw[11] = DEC[bs[17] as usize] << 6 | DEC[bs[18] as usize] << 1 | DEC[bs[19] as usize] >> 4; + raw[10] = DEC[bs[16] as usize] << 3 | DEC[bs[17] as usize] >> 2; + raw[9] = DEC[bs[14] as usize] << 5 | DEC[bs[15] as usize]; + raw[8] = DEC[bs[12] as usize] << 7 | DEC[bs[13] as usize] << 2 | DEC[bs[14] as usize] >> 3; + raw[7] = DEC[bs[11] as usize] << 4 | DEC[bs[12] as usize] >> 1; + raw[6] = DEC[bs[9] as usize] << 6 | DEC[bs[10] as usize] << 1 | DEC[bs[11] as usize] >> 4; + raw[5] = DEC[bs[8] as usize] << 3 | DEC[bs[9] as usize] >> 2; + raw[4] = DEC[bs[6] as usize] << 5 | DEC[bs[7] as usize]; + raw[3] = DEC[bs[4] as usize] << 7 | DEC[bs[5] as usize] << 2 | DEC[bs[6] as usize] >> 3; + raw[2] = DEC[bs[3] as usize] << 4 | DEC[bs[4] as usize] >> 1; + raw[1] = DEC[bs[1] as usize] << 6 | DEC[bs[2] as usize] << 1 | DEC[bs[3] as usize] >> 4; + raw[0] = DEC[bs[0] as usize] << 3 | DEC[bs[1] as usize] >> 2; + Ok(Self(raw)) + } +} + +fn is_valid_char(c: &char) -> bool { + match c { + '0'..='9' | 'a'..='v' => true, + _ => false, + } +} + +#[rustfmt::skip] +const fn gen_dec() -> [u8; 256] { + let mut dec = [0u8; 256]; + // Fill in ranges b'0'..=b'9' and b'a'..=b'v'. + // dec[48..=57].copy_from_slice(&(0..=9).collect::>()); + dec[48] = 0; dec[49] = 1; dec[50] = 2; dec[51] = 3; dec[52] = 4; + dec[53] = 5; dec[54] = 6; dec[55] = 7; dec[56] = 8; dec[57] = 9; + // dec[97..=118].copy_from_slice(&(10..=31).collect::>()); + dec[ 97] = 10; dec[ 98] = 11; dec[ 99] = 12; dec[100] = 13; + dec[101] = 14; dec[102] = 15; dec[103] = 16; dec[104] = 17; + dec[105] = 18; dec[106] = 19; dec[107] = 20; dec[108] = 21; + dec[109] = 22; dec[110] = 23; dec[111] = 24; dec[112] = 25; + dec[113] = 26; dec[114] = 27; dec[115] = 28; dec[116] = 29; + dec[117] = 30; dec[118] = 31; + dec +} + +#[cfg(test)] +mod tests { + use super::*; + + // https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id_test.go#L101 + #[test] + fn test_to_string() { + assert_eq!( + Id([0x4d, 0x88, 0xe1, 0x5b, 0x60, 0xf4, 0x86, 0xe4, 0x28, 0x41, 0x2d, 0xc9]) + .to_string(), + "9m4e2mr0ui3e8a215n4g" + ); + } + + // https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id_test.go#L116 + #[test] + fn test_from_str_valid() { + assert_eq!( + Id::from_str("9m4e2mr0ui3e8a215n4g").unwrap(), + Id([0x4d, 0x88, 0xe1, 0x5b, 0x60, 0xf4, 0x86, 0xe4, 0x28, 0x41, 0x2d, 0xc9]) + ); + } + + #[test] + fn test_from_str_invalid_length() { + assert_eq!( + Id::from_str("9m4e2mr0ui3e8a215n4"), + Err(ParseIdError::InvalidLength(19)) + ); + } + + #[test] + fn test_from_str_invalid_char() { + assert_eq!( + Id::from_str("9z4e2mr0ui3e8a215n4g"), + Err(ParseIdError::InvalidCharacter('z')) + ); + } + + // https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id_test.go#L45 + #[test] + fn test_extraction() { + struct IDParts { + raw: [u8; RAW_LEN], + timestamp: u64, + machine_id: [u8; 3], + pid: u16, + counter: u32, + } + + let tests = vec![ + IDParts { + raw: [ + 0x4d, 0x88, 0xe1, 0x5b, 0x60, 0xf4, 0x86, 0xe4, 0x28, 0x41, 0x2d, 0xc9, + ], + timestamp: 1300816219, + machine_id: [0x60, 0xf4, 0x86], + pid: 0xe428, + counter: 4271561, + }, + IDParts { + raw: [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + timestamp: 0, + machine_id: [0x00, 0x00, 0x00], + pid: 0x0000, + counter: 0, + }, + IDParts { + raw: [ + 0x00, 0x00, 0x00, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x00, 0x00, 0x01, + ], + timestamp: 0, + machine_id: [0xaa, 0xbb, 0xcc], + pid: 0xddee, + counter: 1, + }, + ]; + + for t in tests { + let id = Id(t.raw); + assert_eq!( + id.time().duration_since(UNIX_EPOCH).unwrap().as_secs(), + t.timestamp + ); + assert_eq!(id.machine(), t.machine_id); + assert_eq!(id.pid(), t.pid); + assert_eq!(id.counter(), t.counter); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c3b8669 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,74 @@ +//! Globally unique sortable id generator. A Rust port of . +//! +//! The binary representation is compatible with the Mongo DB 12-byte +//! [ObjectId][object-id]. The value consists of: +//! +//! - a 4-byte timestamp value in seconds since the Unix epoch +//! - a 3-byte value based on the machine identifier +//! - a 2-byte value based on the process id +//! - a 3-byte incrementing counter, initialized to a random value +//! +//! The string representation is 20 bytes, using a base32 hex variant with +//! characters `[0-9a-v]` to retain the sortable property of the id. +//! +//! See the original [`xid`] project for more details. +//! +//! ## Usage +//! +//! ```rust,ignore +//! use xid; +//! +//! fn main() { +//! println!("{}", xid::new().to_string()); //=> bva9lbqn1bt68k8mj62g +//! } +//! ``` +//! +//! [`xid`]: https://github.com/rs/xid +//! [object-id]: https://docs.mongodb.org/manual/reference/object-id/ +mod generator; +mod id; +mod machine_id; +mod pid; + +pub use id::{Id, ParseIdError}; + +/// Generate a new globally unique id. +pub fn new() -> Id { + generator::get().new_id() +} + +#[cfg(test)] +mod tests { + use super::*; + + // https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id_test.go#L64 + #[test] + fn test_new() { + let mut ids = Vec::new(); + for _ in 0..10 { + ids.push(new()); + } + + for i in 1..10 { + // Test for uniqueness among all other 9 generated ids + for j in 0..10 { + if i != j { + assert_ne!(ids[i], ids[j]); + } + } + + let id = &ids[i]; + let prev_id = &ids[i - 1]; + // Check that timestamp was incremented and is within 5 seconds of the previous one + // Panics if it went backwards. + let secs = id.time().duration_since(prev_id.time()).unwrap().as_secs(); + assert!(secs <= 5); + // Check that machine ids are the same + assert_eq!(id.machine(), prev_id.machine()); + // Check that pids are the same + assert_eq!(id.pid(), prev_id.pid()); + // Test for proper increment + assert_eq!(id.counter() - prev_id.counter(), 1); + } + } +} diff --git a/src/machine_id.rs b/src/machine_id.rs new file mode 100644 index 0000000..69853bf --- /dev/null +++ b/src/machine_id.rs @@ -0,0 +1,59 @@ +use std::fs; +use std::io; + +use hostname; +use md5; +use rand::RngCore; +#[cfg(any(target_os = "macos"))] +use sysctl::{Sysctl, SysctlError}; + +// https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id.go#L117 +pub fn get() -> [u8; 3] { + let id = match machine_id().unwrap_or_default() { + x if !x.is_empty() => x, + _ => hostname::get() + .map(|s| s.into_string().unwrap_or_default()) + .unwrap_or_default(), + }; + + let mut bytes = [0u8; 3]; + if id.is_empty() { + // Fallback to random bytes + rand::thread_rng().fill_bytes(&mut bytes); + } else { + bytes.copy_from_slice(&md5::compute(id)[0..3]); + } + bytes +} + +// OS dependent machine ids. Only linux was confirmed. + +// https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/hostid_linux.go +// Not checking "/sys/class/dmi/id/product_uuid" because normal users can't read it. +#[cfg(target_os = "linux")] +fn machine_id() -> io::Result { + fs::read_to_string("/var/lib/dbus/machine-id") + .or_else(|_| fs::read_to_string("/etc/machine-id")) +} + +// https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/hostid_darwin.go +#[cfg(target_os = "macos")] +fn machine_id() -> Result { + sysctl::Ctl::new("kern.uuid")? + .value() + .map(|v| v.to_string()) +} + +// https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/hostid_windows.go +#[cfg(target_os = "windows")] +fn machine_id() -> io::Result { + winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE) + .open_subkey("SOFTWARE\\Microsoft\\Cryptography")? + .get_value("MachineGuid")? +} + +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +fn machine_id() -> io::Result { + // Fallback to hostname or a random value + Ok("".to_string()) +} diff --git a/src/pid.rs b/src/pid.rs new file mode 100644 index 0000000..bd86018 --- /dev/null +++ b/src/pid.rs @@ -0,0 +1,25 @@ +use std::fs; +use std::process; + +use crc32fast::Hasher; + +// 2 bytes of PID +// https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id.go#L159 +pub fn get() -> u16 { + // https://github.com/rs/xid/blob/efa678f304ab65d6d57eedcb086798381ae22206/id.go#L105 + // > If /proc/self/cpuset exists and is not /, we can assume that we are in a + // > form of container and use the content of cpuset xor-ed with the PID in + // > order get a reasonable machine global unique PID. + let pid = match fs::read("/proc/self/cpuset") { + Ok(buff) if buff.len() > 1 => process::id() ^ crc32(&buff), + _ => process::id(), + }; + + pid as u16 +} + +fn crc32(buff: &[u8]) -> u32 { + let mut hasher = Hasher::new(); + hasher.update(buff); + hasher.finalize() +}