use std::path::{Path, PathBuf};
use std::process::Command;
use crate::error::Error;
use crate::source::{Probe, Source, SourceKind};
use crate::sources::util::{normalize, read_capped};
const HOSTID_PATH: &str = "/etc/hostid";
#[derive(Debug, Clone)]
pub struct FreeBsdHostIdFile {
path: PathBuf,
}
impl FreeBsdHostIdFile {
#[must_use]
pub fn new() -> Self {
Self {
path: PathBuf::from(HOSTID_PATH),
}
}
#[must_use]
pub fn at(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl Default for FreeBsdHostIdFile {
fn default() -> Self {
Self::new()
}
}
impl Source for FreeBsdHostIdFile {
fn kind(&self) -> SourceKind {
SourceKind::FreeBsdHostId
}
fn probe(&self) -> Result<Option<Probe>, Error> {
match read_capped(&self.path) {
Ok(content) => {
let Some(value) = normalize(&content) else {
return Ok(None);
};
if !value.bytes().all(is_printable_ascii) {
return Err(Error::Malformed {
source_kind: SourceKind::FreeBsdHostId,
reason: "hostid file contains non-printable bytes".to_owned(),
});
}
Ok(Some(Probe::new(SourceKind::FreeBsdHostId, value)))
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(Error::Io {
source_kind: SourceKind::FreeBsdHostId,
path: self.path.clone(),
source,
}),
}
}
}
fn is_printable_ascii(b: u8) -> bool {
matches!(b, 0x20..=0x7e)
}
#[derive(Debug, Default, Clone)]
pub struct KenvSmbios {
_priv: (),
}
impl KenvSmbios {
#[must_use]
pub fn new() -> Self {
Self { _priv: () }
}
}
impl Source for KenvSmbios {
fn kind(&self) -> SourceKind {
SourceKind::KenvSmbios
}
fn probe(&self) -> Result<Option<Probe>, Error> {
let output = Command::new("/bin/kenv")
.args(["-q", "smbios.system.uuid"])
.output()
.map_err(|e| Error::Platform {
source_kind: SourceKind::KenvSmbios,
reason: format!("kenv: {e}"),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
log::debug!(
"host-identity: kenv exited with {}: {}",
output.status,
stderr.trim()
);
return Ok(None);
}
let Ok(value) = std::str::from_utf8(&output.stdout) else {
return Ok(None);
};
Ok(normalize(value).map(|v| Probe::new(SourceKind::KenvSmbios, v)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn printable_ascii_boundary() {
assert!(!is_printable_ascii(0x1f));
assert!(is_printable_ascii(0x20));
assert!(is_printable_ascii(0x7e));
assert!(!is_printable_ascii(0x7f));
}
#[test]
fn hostid_file_reads_uuid() {
let mut f = tempfile::NamedTempFile::new().unwrap();
writeln!(f, "12345678-1234-1234-1234-123456789abc").unwrap();
let probe = FreeBsdHostIdFile::at(f.path()).probe().unwrap().unwrap();
assert_eq!(probe.kind(), SourceKind::FreeBsdHostId);
assert_eq!(probe.value(), "12345678-1234-1234-1234-123456789abc");
}
#[test]
fn hostid_file_missing_is_none() {
let dir = tempfile::TempDir::new().unwrap();
assert!(
FreeBsdHostIdFile::at(dir.path().join("hostid"))
.probe()
.unwrap()
.is_none()
);
}
#[test]
fn hostid_file_empty_is_none() {
let f = tempfile::NamedTempFile::new().unwrap();
assert!(FreeBsdHostIdFile::at(f.path()).probe().unwrap().is_none());
}
#[test]
fn hostid_file_non_printable_bytes_errors() {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(&[0x00, 0x01, 0x02, 0x03]).unwrap();
match FreeBsdHostIdFile::at(f.path()).probe() {
Err(Error::Malformed {
source_kind,
reason,
}) => {
assert_eq!(source_kind, SourceKind::FreeBsdHostId);
assert!(reason.contains("non-printable"));
}
other => panic!("expected Error::Malformed, got {other:?}"),
}
}
#[test]
fn hostid_file_reports_io_error_for_directory() {
let dir = tempfile::TempDir::new().unwrap();
match FreeBsdHostIdFile::at(dir.path()).probe() {
Err(Error::Io { path, .. }) => assert_eq!(path, dir.path()),
other => panic!("expected Error::Io, got {other:?}"),
}
}
}