use std::{
fmt::{self, Display, Formatter, Write},
num::ParseIntError,
path::PathBuf,
str::FromStr,
};
use compose_spec_macros::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
use super::{volumes::AbsolutePathError, AbsolutePath};
#[derive(SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, Hash)]
pub struct Device {
pub host_path: PathBuf,
pub container_path: AbsolutePath,
pub permissions: Permissions,
}
impl FromStr for Device {
type Err = ParseDeviceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut split = s.splitn(3, ':');
let host_path = split.next().ok_or(ParseDeviceError::Empty)?.into();
let container_path = split
.next()
.ok_or(ParseDeviceError::ContainerPathMissing)?
.parse()?;
let permissions = split.next().unwrap_or_default().parse()?;
Ok(Self {
host_path,
container_path,
permissions,
})
}
}
impl TryFrom<&str> for Device {
type Error = ParseDeviceError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseDeviceError {
#[error("device cannot be an empty string")]
Empty,
#[error("device must have a container path")]
ContainerPathMissing,
#[error("device container path must be absolute")]
ContainerPathAbsolute(#[from] AbsolutePathError),
#[error("error parsing device permissions")]
Permissions(#[from] ParsePermissionsError),
}
impl Display for Device {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let Self {
host_path,
container_path,
permissions,
} = self;
write!(
f,
"{}:{}",
host_path.display(),
container_path.as_path().display(),
)?;
if permissions.any() {
write!(f, ":{permissions}")?;
}
Ok(())
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Permissions {
pub read: bool,
pub write: bool,
pub mknod: bool,
}
impl Permissions {
#[must_use]
pub const fn all() -> Self {
Self {
read: true,
write: true,
mknod: true,
}
}
#[must_use]
pub const fn any(self) -> bool {
let Self { read, write, mknod } = self;
read || write || mknod
}
}
impl FromStr for Permissions {
type Err = ParsePermissionsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut read = false;
let mut write = false;
let mut mknod = false;
for permission in s.chars() {
match permission {
'r' => read = true,
'w' => write = true,
'm' => mknod = true,
unknown => return Err(ParsePermissionsError(unknown)),
}
}
Ok(Self { read, write, mknod })
}
}
impl TryFrom<&str> for Permissions {
type Error = ParsePermissionsError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error("invalid device permission `{0}`, must be `r` (read), `w` (write), or `m` (mknod)")]
pub struct ParsePermissionsError(char);
impl Display for Permissions {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let Self { read, write, mknod } = *self;
if read {
f.write_char('r')?;
}
if write {
f.write_char('w')?;
}
if mknod {
f.write_char('m')?;
}
Ok(())
}
}
#[derive(SerializeDisplay, DeserializeFromStr, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CgroupRule {
pub kind: Kind,
pub major: MajorMinorNumber,
pub minor: MajorMinorNumber,
pub permissions: Permissions,
}
impl FromStr for CgroupRule {
type Err = ParseCgroupRuleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut split = s.splitn(3, ' ');
let kind = split.next().ok_or(ParseCgroupRuleError::Empty)?.parse()?;
let (major, minor) = split
.next()
.and_then(|s| s.split_once(':'))
.ok_or(ParseCgroupRuleError::MajorMinorNumbersMissing)?;
let major = major.parse()?;
let minor = minor.parse()?;
let permissions = split.next().unwrap_or_default().parse()?;
Ok(Self {
kind,
major,
minor,
permissions,
})
}
}
impl TryFrom<&str> for CgroupRule {
type Error = ParseCgroupRuleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum ParseCgroupRuleError {
#[error("device cgroup rule cannot be empty")]
Empty,
#[error("invalid device kind")]
Kind(#[from] ParseKindError),
#[error("device cgroup rule missing major minor numbers")]
MajorMinorNumbersMissing,
#[error("error parsing device major minor number")]
MajorMinorNumber(#[from] ParseIntError),
#[error("error parsing device cgroup rule permissions")]
Permissions(#[from] ParsePermissionsError),
}
impl Display for CgroupRule {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let Self {
kind,
major,
minor,
permissions,
} = self;
write!(f, "{kind} {major}:{minor}")?;
if permissions.any() {
write!(f, " {permissions}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Kind {
All,
Char,
Block,
}
impl Kind {
#[must_use]
pub const fn as_char(self) -> char {
match self {
Self::All => 'a',
Self::Char => 'c',
Self::Block => 'b',
}
}
}
impl TryFrom<char> for Kind {
type Error = ParseKindError;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'a' => Ok(Self::All),
'c' => Ok(Self::Char),
'b' => Ok(Self::Block),
unknown => Err(ParseKindError(unknown.into())),
}
}
}
impl FromStr for Kind {
type Err = ParseKindError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"a" => Ok(Self::All),
"c" => Ok(Self::Char),
"b" => Ok(Self::Block),
unknown => Err(ParseKindError(unknown.to_owned())),
}
}
}
impl TryFrom<&str> for Kind {
type Error = ParseKindError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[error("invalid device kind `{0}`, must be `a` (all), `c` (char), or `b` (block)")]
pub struct ParseKindError(String);
impl Display for Kind {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_char(self.as_char())
}
}
impl From<Kind> for char {
fn from(value: Kind) -> Self {
value.as_char()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MajorMinorNumber {
All,
Integer(u16),
}
impl PartialEq<u16> for MajorMinorNumber {
fn eq(&self, other: &u16) -> bool {
match self {
Self::All => false,
Self::Integer(num) => num.eq(other),
}
}
}
impl From<u16> for MajorMinorNumber {
fn from(value: u16) -> Self {
Self::Integer(value)
}
}
impl FromStr for MajorMinorNumber {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() || s == "*" {
Ok(Self::All)
} else {
s.parse().map(Self::Integer)
}
}
}
impl TryFrom<&str> for MajorMinorNumber {
type Error = ParseIntError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl Display for MajorMinorNumber {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::All => f.write_char('*'),
Self::Integer(num) => Display::fmt(num, f),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use proptest::{
arbitrary::any,
prop_assert_eq, prop_compose, prop_oneof, proptest,
strategy::{Just, Strategy},
};
use super::*;
mod device {
use crate::service::tests::path_no_colon;
use super::*;
#[test]
fn from_str() {
let device = Device {
host_path: "/host".into(),
container_path: "/container".parse().unwrap(),
permissions: Permissions {
read: true,
write: true,
mknod: false,
},
};
assert_eq!(device, "/host:/container:rw".parse().unwrap());
}
#[test]
fn display() {
let device = Device {
host_path: "/host".into(),
container_path: "/container".parse().unwrap(),
permissions: Permissions {
read: true,
write: true,
mknod: false,
},
};
assert_eq!(device.to_string(), "/host:/container:rw");
}
proptest! {
#[test]
fn parse_no_panic(string: String) {
let _ = string.parse::<Device>();
}
#[test]
fn to_string_no_panic(device in device()) {
device.to_string();
}
#[test]
fn round_trip(device in device()) {
prop_assert_eq!(&device, &device.to_string().parse()?);
}
}
prop_compose! {
fn device()(
host_path in path_no_colon(),
container_path: AbsolutePath,
permissions in permissions()
) -> Device {
Device { host_path, container_path, permissions }
}
}
}
mod permissions {
use super::*;
#[test]
fn from_str() {
assert_eq!(Permissions::default(), "".parse().unwrap());
assert_eq!(
Permissions {
read: true,
write: true,
mknod: true
},
"rwm".parse().unwrap(),
);
}
#[test]
fn display() {
assert!(Permissions::default().to_string().is_empty());
assert_eq!(
Permissions {
read: true,
write: true,
mknod: true
}
.to_string(),
"rwm",
);
}
proptest! {
#[test]
fn parse_no_panic(string: String) {
let _ = string.parse::<Permissions>();
}
#[test]
fn to_string_no_panic(permissions in permissions()) {
permissions.to_string();
}
#[test]
fn round_trip(permissions in permissions()) {
prop_assert_eq!(permissions, permissions.to_string().parse()?);
}
}
}
mod cgroup_rule {
use super::*;
#[test]
fn from_str() {
let rule = CgroupRule {
kind: Kind::Char,
major: MajorMinorNumber::Integer(1),
minor: MajorMinorNumber::Integer(3),
permissions: Permissions {
read: true,
write: false,
mknod: true,
},
};
assert_eq!(rule, "c 1:3 mr".parse().unwrap());
let rule = CgroupRule {
kind: Kind::All,
major: MajorMinorNumber::Integer(7),
minor: MajorMinorNumber::All,
permissions: Permissions::all(),
};
assert_eq!(rule, "a 7:* rmw".parse().unwrap());
}
#[test]
fn display() {
let rule = CgroupRule {
kind: Kind::Char,
major: MajorMinorNumber::Integer(1),
minor: MajorMinorNumber::Integer(3),
permissions: Permissions {
read: true,
write: false,
mknod: true,
},
};
assert_eq!(rule.to_string(), "c 1:3 rm");
let rule = CgroupRule {
kind: Kind::All,
major: MajorMinorNumber::Integer(7),
minor: MajorMinorNumber::All,
permissions: Permissions::all(),
};
assert_eq!(rule.to_string(), "a 7:* rwm");
}
proptest! {
#[test]
fn parse_no_panic(string: String) {
let _ = string.parse::<CgroupRule>();
}
#[test]
fn to_string_no_panic(rule in cgroup_rule()) {
rule.to_string();
}
#[test]
fn round_trip(rule in cgroup_rule()) {
prop_assert_eq!(rule, rule.to_string().parse()?);
}
}
prop_compose! {
fn cgroup_rule()(
kind in kind(),
major in major_minor_number(),
minor in major_minor_number(),
permissions in permissions(),
) -> CgroupRule {
CgroupRule {
kind,
major,
minor,
permissions,
}
}
}
fn kind() -> impl Strategy<Value = Kind> {
prop_oneof![Just(Kind::All), Just(Kind::Char), Just(Kind::Block)]
}
fn major_minor_number() -> impl Strategy<Value = MajorMinorNumber> {
prop_oneof![
1 => Just(MajorMinorNumber::All),
u16::MAX.into() => any::<u16>().prop_map_into(),
]
}
}
prop_compose! {
fn permissions()(read: bool, write: bool, mknod: bool) -> Permissions {
Permissions { read, write, mknod }
}
}
}