blob: 9385e45055377bb2380e5141c3502db52d3dacb0 [file] [log] [blame]
//! [netlink](https://www.man7.org/linux/man-pages/man7/netlink.7.html) `NETLINK_KOBJECT_UEVENT` packet parser
//!
//! The [uevents](https://www.kernel.org/doc/html/latest/core-api/kobject.html#uevents) are
//! triggered by `kobject_uevent` and `kobject_uevent_env` to signal a change in the referred kobject.
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
str::{from_utf8, FromStr},
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Unexpected action: {0}")]
UnexpectedAction(String),
#[error("Invalid DEVPATH: {0}")]
InvalidDevPath(String),
#[error("Unexpected SEQNUM: {0}")]
InvalidSeqNum(String),
#[error(transparent)]
Io(#[from] io::Error),
#[error("Path not inside mountpoint")]
NotInsideMountpoint,
#[error("Packet not UTF-8")]
NotUtf8,
#[error("action not found")]
ActionNotFound,
#[error("devpath not found")]
DevPathNotFound,
#[error("subsystem not found")]
SubsystemNotFound,
#[error("seq missing")]
SeqMissing,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
/// KObject action types
///
/// See kobject_action in include/linux/kobject.h
pub enum ActionType {
/// A new kobject is added
Add,
/// A kobject is removed
Remove,
/// the kobject changed its internal state
///
/// the `env` contains kobject-specific information.
Change,
/// the kobject is reparented as a result of `kobject_move`
///
/// the `env` contains `DEVPATH_OLD=<oldpath>`.
Move,
/// The device is back online after successful `device_offline`.
Online,
/// The device is ready to be hot-removed.
Offline,
/// The device is bound to a driver.
Bind,
/// The device is not bound to its driver anymore.
Unbind,
}
impl FromStr for ActionType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use ActionType::*;
match s {
"add" => Ok(Add),
"remove" => Ok(Remove),
"change" => Ok(Change),
"move" => Ok(Move),
"online" => Ok(Online),
"offline" => Ok(Offline),
"bind" => Ok(Bind),
"unbind" => Ok(Unbind),
_ => Err(Error::UnexpectedAction(s.to_owned())),
}
}
}
/// Linux kernel userspace event
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UEvent {
/// Action happening
pub action: ActionType,
/// Complete Kernel Object path
pub devpath: PathBuf,
/// SubSystem originating the event
pub subsystem: String,
/// Arguments
pub env: HashMap<String, String>,
/// Sequence number
pub seq: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct MaybeUEvent {
/// Action happening
pub action: Option<ActionType>,
/// Complete Kernel Object path
pub devpath: Option<PathBuf>,
/// SubSystem originating the event
pub subsystem: Option<String>,
/// Arguments
pub env: HashMap<String, String>,
/// Sequence number
pub seq: Option<u64>,
}
/// Parse key=value strings as UEvent, some fields may be missing
fn parse_uevent_iter<'a>(iter: impl Iterator<Item = &'a str>) -> Result<MaybeUEvent, Error> {
let mut action = None;
let mut devpath = None;
let mut subsystem = None;
let mut env = HashMap::new();
let mut seq = None;
for f in iter {
if let Some((key, value)) = f.split_once('=') {
match key {
"ACTION" => action = Some(value.parse::<ActionType>()?),
"DEVPATH" => {
devpath = Some(
value
.parse::<PathBuf>()
.map_err(|_| Error::InvalidDevPath(value.to_owned()))?,
)
}
"SUBSYSTEM" => subsystem = Some(value.to_string()),
"SEQNUM" => {
seq = Some(
value
.parse::<u64>()
.map_err(|_| Error::InvalidSeqNum(value.to_owned()))?,
)
}
_ => {}
}
let _ = env.insert(key.into(), value.into());
}
}
Ok(MaybeUEvent {
action,
devpath,
subsystem,
env,
seq,
})
}
impl UEvent {
/// Parse a sysfs path as an Add UEvent
pub fn from_sysfs_path(
path: impl AsRef<Path>,
mountpoint: impl AsRef<Path>,
) -> Result<UEvent, Error> {
let path = path.as_ref();
let uevent = std::fs::read_to_string(path.join("uevent"))?;
let subsystem_path = std::fs::read_link(path.join("subsystem"))?;
let lines = uevent.lines();
let MaybeUEvent { env, .. } = parse_uevent_iter(lines)?;
let action = ActionType::Add;
// make it look like a netlink devpath
let devpath = Path::new("/").join(
path.canonicalize()?
.strip_prefix(mountpoint)
.map_err(|_| Error::NotInsideMountpoint)?,
);
let subsystem = subsystem_path
.file_name()
.ok_or(Error::SubsystemNotFound)?
.to_string_lossy()
.to_string();
let seq = 0;
Ok(UEvent {
action,
devpath,
subsystem,
env,
seq,
})
}
/// Parse a netlink packet as received from the NETLINK_KOBJECT_UEVENT broadcast
pub fn from_netlink_packet(pkt: &[u8]) -> Result<UEvent, Error> {
let lines = from_utf8(pkt).map_err(|_| Error::NotUtf8)?.split('\0');
let MaybeUEvent {
action,
devpath,
subsystem,
env,
seq,
} = parse_uevent_iter(lines)?;
let action = action.ok_or(Error::ActionNotFound)?;
let devpath = devpath.ok_or(Error::DevPathNotFound)?;
let subsystem = subsystem.ok_or(Error::SubsystemNotFound)?;
let seq = seq.ok_or(Error::SeqMissing)?;
Ok(UEvent {
action,
devpath,
subsystem,
env,
seq,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! uevent {
(
action: $action:expr,
devpath: $devpath:expr,
subsystem: $subsystem:expr,
env: { $($env_name:expr => $env_value:expr),* $(,)? },
seq: $seq:expr
) => {
UEvent {
action: $action,
devpath: PathBuf::from($devpath),
subsystem: $subsystem.to_string(),
env: IntoIterator::into_iter([
$(($env_name.to_string(), $env_value.to_string())),*
]).collect(),
seq: $seq,
}
};
}
#[test]
fn add_uevent() {
const DATA: &[u8] = b"add@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=add\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3469";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Add,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "add",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3469",
},
seq: 3469
}
);
}
#[test]
fn remove_uevent() {
const DATA: &[u8] = b"remove@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=remove\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3471";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Remove,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "remove",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3471",
},
seq: 3471
}
);
}
#[test]
fn change_uevent() {
const DATA: &[u8] = b"change@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=change\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3472";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Change,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "change",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3472",
},
seq: 3472
}
);
}
#[test]
fn move_uevent() {
const DATA: &[u8] = b"move@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=move\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3473";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Move,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "move",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3473",
},
seq: 3473
}
);
}
#[test]
fn online_uevent() {
const DATA: &[u8] = b"online@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=online\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3474";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Online,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "online",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3474",
},
seq: 3474
}
);
}
#[test]
fn offline_uevent() {
const DATA: &[u8] = b"offline@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=offline\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3475";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Offline,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "offline",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3475",
},
seq: 3475
}
);
}
#[test]
fn bind_uevent() {
const DATA: &[u8] = b"bind@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=bind\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3476";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Bind,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "bind",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3476",
},
seq: 3476
}
);
}
#[test]
fn unbind_uevent() {
const DATA: &[u8] = b"unbind@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=unbind\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SYNTH_UUID=0\0\
MAJOR=4\0\
MINOR=70\0\
DEVNAME=ttyS6\0\
SEQNUM=3477";
assert_eq!(
UEvent::from_netlink_packet(DATA).unwrap(),
uevent! {
action: ActionType::Unbind,
devpath: "/devices/platform/serial8250/tty/ttyS6",
subsystem: "tty",
env: {
"ACTION" => "unbind",
"DEVPATH" => "/devices/platform/serial8250/tty/ttyS6",
"SUBSYSTEM" => "tty",
"SYNTH_UUID" => "0",
"MAJOR" => "4",
"MINOR" => "70",
"DEVNAME" => "ttyS6",
"SEQNUM" => "3477",
},
seq: 3477
}
);
}
#[test]
fn invalid_event() {
const DATA: &[u8] = b"hello@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=hello\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SEQNUM=3477";
assert!(UEvent::from_netlink_packet(DATA).is_err());
}
#[test]
fn missing_action() {
const DATA: &[u8] = b"add@/devices/platform/serial8250/tty/ttyS6\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty\0\
SEQNUM=3477";
assert!(UEvent::from_netlink_packet(DATA).is_err());
}
#[test]
fn missing_devpath() {
const DATA: &[u8] = b"add@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=unbind\0\
SUBSYSTEM=tty\0\
SEQNUM=3477";
assert!(UEvent::from_netlink_packet(DATA).is_err());
}
#[test]
fn missing_subsystem() {
const DATA: &[u8] = b"add@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=unbind\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SEQNUM=3477";
assert!(UEvent::from_netlink_packet(DATA).is_err());
}
#[test]
fn missing_seqnum() {
const DATA: &[u8] = b"add@/devices/platform/serial8250/tty/ttyS6\0\
ACTION=unbind\0\
DEVPATH=/devices/platform/serial8250/tty/ttyS6\0\
SUBSYSTEM=tty";
assert!(UEvent::from_netlink_packet(DATA).is_err());
}
}