yamid 0.1.2

Yet Another Machine ID
Documentation
use uuid::Uuid;

pub mod error;

pub type Result<T> = std::result::Result<T, error::Error>;

#[derive(PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MachineId(Uuid);

impl std::fmt::Display for MachineId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        std::fmt::UpperHex::fmt(&self.0, f)
    }
}

impl AsRef<Uuid> for MachineId {
    #[inline]
    fn as_ref(&self) -> &Uuid {
        &self.0
    }
}

impl AsRef<[u8]> for MachineId {
    #[inline]
    fn as_ref(&self) -> &[u8] {
        self.0.as_ref()
    }
}

impl From<MachineId> for Uuid {
    #[inline]
    fn from(machine_id: MachineId) -> Self {
        machine_id.0
    }
}

impl MachineId {
    #[cfg(windows)]
    pub fn new() -> Result<Self> {
        use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey};

        let guid_str = RegKey::predef(HKEY_LOCAL_MACHINE)
            .open_subkey("SOFTWARE\\Microsoft\\Cryptography")
            .and_then(|key| key.get_value::<String, _>("MachineGuid"))?;

        let machine_uuid = Uuid::parse_str(&guid_str)?;

        Ok(Self(machine_uuid))
    }

    #[cfg(target_os = "linux")]
    pub fn new() -> Result<Self> {
        use std::fs::read_to_string;

        let guid_str = read_to_string("/etc/machine-id")
            .and_then(|data| {
                if data.is_empty() {
                    Err(std::io::Error::new(std::io::ErrorKind::InvalidData, ""))
                } else {
                    Ok(data)
                }
            })
            .or_else(|_| read_to_string("/var/lib/dbus/machine-id"))?;
        let machine_uuid = Uuid::parse_str(guid_str.trim_end())?;

        Ok(Self(machine_uuid))
    }

    #[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
    pub fn new() -> Result<Self> {
        let id = unix::host_uuid().or_else(|_| std::fs::read_to_string("/etc/hostid"))?;
        let machine_uuid = Uuid::parse_str(id.trim_end())?;
        Ok(Self(machine_uuid))
    }

    #[cfg(target_os = "macos")]
    pub fn new() -> Result<Self> {
        use apple_sys::IOKit as io;
        use core_foundation::{
            base::TCFType,
            string::{CFString, CFStringRef},
        };

        struct ObjectReleaser(u32);
        impl Drop for ObjectReleaser {
            fn drop(&mut self) {
                unsafe { io::IOObjectRelease(self.0) };
            }
        }

        let uuid_str = unsafe {
            let root = io::IORegistryEntryFromPath(
                io::kIOMasterPortDefault,
                "IOService:/\0".as_ptr() as _,
            );

            if root == io::MACH_PORT_NULL {
                return Err(std::io::Error::last_os_error().into());
            }

            let root = ObjectReleaser(root);
            let key = CFString::from_static_string("IOPlatformUUID");
            let uuid_cref: CFStringRef = io::IORegistryEntryCreateCFProperty(
                root.0,
                key.as_CFTypeRef() as _,
                io::kCFAllocatorDefault,
                0,
            ) as _;

            if uuid_cref.is_null() {
                return Err(std::io::Error::last_os_error().into());
            }
            CFString::wrap_under_create_rule(uuid_cref).to_string()
        };

        Ok(Self(uuid::Uuid::parse_str(&uuid_str)?))
    }
}

#[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
mod unix {
    pub fn host_uuid() -> std::io::Result<String> {
        const KERN_HOSTUUID: i32 = 0x24i32;
        let vec = sysctl([libc::CTL_KERN, KERN_HOSTUUID])?;

        Ok(vec
            .into_iter()
            .take_while(|ch| *ch != 0)
            .map(|ch| ch as char)
            .collect::<String>())
    }

    pub fn sysctl<const N: usize>(mib: [i32; N]) -> std::io::Result<Vec<u8>> {
        use std::ptr;
        let (mut m, mut n) = (mib, 0);
        let r = unsafe {
            libc::sysctl(
                m.as_mut_ptr() as _,
                m.len() as _,
                ptr::null_mut(),
                &mut n,
                ptr::null_mut(),
                0,
            )
        };

        if r != 0 {
            return Err(std::io::Error::from_raw_os_error(r));
        }
        let mut b = Vec::with_capacity(n);
        let s = unsafe {
            let res = libc::sysctl(
                m.as_mut_ptr() as _,
                m.len() as _,
                b.as_mut_ptr() as _,
                &mut n,
                ptr::null_mut(),
                0,
            );
            b.set_len(n);
            res
        };
        if s != 0 {
            Err(std::io::Error::from_raw_os_error(s))
        } else {
            Ok(b)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_machine_id() {
        let first = MachineId::new().unwrap();
        let second = MachineId::new().unwrap();

        println!("Machine id: {first}");

        assert_eq!(first, second);
    }

    #[test]
    fn test_into_uuid() {
        let machine_id = MachineId::new().unwrap();
        let expected_uuid: Uuid = *machine_id.as_ref();
        let uuid: Uuid = machine_id.into();

        assert_eq!(expected_uuid, uuid);
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_serde() {
        let id = MachineId::new().unwrap();
        let s = serde_json::to_string(&id).unwrap();

        let de: MachineId = serde_json::from_str(&s).unwrap();
        assert_eq!(id, de);
    }
}