diff --git a/unix-mode b/unix-mode deleted file mode 160000 index 0bbf1ae..0000000 --- a/unix-mode +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bbf1aed9ad583017b90aa2c03d3f7870538a914 diff --git a/unix-mode/Cargo.toml b/unix-mode/Cargo.toml new file mode 100644 index 0000000..56c7e23 --- /dev/null +++ b/unix-mode/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "unix-mode" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libc = { workspace = true } + +[dependencies.bitflags] +package = "bitflags-mini" +path = "../bitflags-mini" diff --git a/unix-mode/src/bit.rs b/unix-mode/src/bit.rs new file mode 100644 index 0000000..a365c25 --- /dev/null +++ b/unix-mode/src/bit.rs @@ -0,0 +1,98 @@ +use bitflags::BitFlags; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}; + +/// Unix permission bit flags +#[derive(Clone, Copy, PartialEq)] +pub enum Bit { + Suid = 0o4000, + Sgid = 0o2000, + Sticky = 0o1000, + URead = 0o400, + UWrite = 0o200, + UExec = 0o100, + GRead = 0o40, + GWrite = 0o20, + GExec = 0o10, + ORead = 0o4, + OWrite = 0o2, + OExec = 0o1, +} + +impl BitAnd for Bit { + type Output = u32; + + fn bitand(self, rhs: u32) -> Self::Output { + self as u32 & rhs + } +} + +impl BitAnd for u32 { + type Output = u32; + + fn bitand(self, rhs: Bit) -> Self::Output { + self & rhs as u32 + } +} + +impl BitAnd for Bit { + type Output = u32; + + fn bitand(self, rhs: Self) -> Self::Output { + self as u32 & rhs as u32 + } +} + +impl BitAndAssign for u32 { + fn bitand_assign(&mut self, rhs: Bit) { + *self = *self & rhs; + } +} + +impl BitOr for Bit { + type Output = u32; + + fn bitor(self, rhs: u32) -> Self::Output { + self as u32 | rhs + } +} + +impl BitOr for u32 { + type Output = u32; + + fn bitor(self, rhs: Bit) -> Self::Output { + self | rhs as u32 + } +} + +impl BitOr for Bit { + type Output = u32; + + fn bitor(self, rhs: Self) -> Self::Output { + self as u32 | rhs as u32 + } +} + +impl BitOrAssign for u32 { + fn bitor_assign(&mut self, rhs: Bit) { + *self = *self | rhs; + } +} + +impl Bit { + pub fn as_char(&self, mode: u32) -> char { + if mode & *self != 0 { + match self { + Self::Suid | Self::Sgid => 's', + Self::Sticky => 't', + Self::URead | Self::GRead | Self::ORead => 'r', + Self::UWrite | Self::GWrite | Self::OWrite => 'w', + Self::UExec if mode.contains(Self::Suid) => 's', + Self::GExec if mode.contains(Self::Sgid) => 's', + Self::OExec if mode.contains(Self::Sticky) => 't', + Self::UExec | Self::GExec | Self::OExec => 'x', + } + } else { + '-' + } + } +} diff --git a/unix-mode/src/lib.rs b/unix-mode/src/lib.rs new file mode 100644 index 0000000..c5b6697 --- /dev/null +++ b/unix-mode/src/lib.rs @@ -0,0 +1,141 @@ +//! Functions for parsing and managing permissions +mod bit; +mod parser; +mod who; +use std::fmt::{self, Write}; + +pub use { + bit::Bit, + parser::{ParseError, Parser}, + who::Who, +}; + +/// Gets the umask for the current user +#[must_use] +pub fn get_umask() -> u32 { + let mask = unsafe { libc::umask(0) }; + let _mask = unsafe { libc::umask(mask) }; + mask +} + +/// Functions for extracting information about Unix modes +pub trait Mode { + /// Returns a string representing permissions in symbolic format + /// including file type + fn mode_string_full(&self) -> Result; + + /// Returns a string representing permissions in symbolic format + /// minus file type + fn mode_string(&self) -> Result; +} + +impl Mode for u32 { + fn mode_string_full(&self) -> Result { + let b = if self & 0o40000 != 0 && self & 0o20000 != 0 { + 'b' + } else if self & 0o40000 != 0 { + 'd' + } else if self & 0o20000 != 0 { + 'c' + } else { + '-' + }; + let mut s = String::new(); + write!(s, "{b}")?; + [ + Bit::URead, + Bit::UWrite, + Bit::UExec, + Bit::GRead, + Bit::GWrite, + Bit::GExec, + Bit::ORead, + Bit::OWrite, + Bit::OExec, + ] + .iter() + .try_for_each(|b| write!(s, "{}", b.as_char(*self)))?; + Ok(s) + } + + fn mode_string(&self) -> Result { + let mut s = String::new(); + [ + Bit::URead, + Bit::UWrite, + Bit::UExec, + Bit::GRead, + Bit::GWrite, + Bit::GExec, + Bit::ORead, + Bit::OWrite, + Bit::OExec, + ] + .iter() + .try_for_each(|b| write!(s, "{}", b.as_char(*self)))?; + Ok(s) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn getumask() { + let mask = unsafe { libc::umask(0o22) }; + assert_eq!(get_umask(), 0o022); + unsafe { + libc::umask(mask); + } + } + + #[test] + fn display_bits_dir() { + let m: u32 = 0o40755; + let s = m.mode_string_full().unwrap(); + assert_eq!(s.as_str(), "drwxr-xr-x") + } + + #[test] + fn display_bits_char() { + let m: u32 = 0o20666; + let s = m.mode_string_full().unwrap(); + assert_eq!(s.as_str(), "crw-rw-rw-") + } + + #[test] + fn display_bits_block() { + let m: u32 = 0o60660; + let s = m.mode_string_full().unwrap(); + assert_eq!(s.as_str(), "brw-rw----") + } + + #[test] + fn display_bits_file() { + let m: u32 = 0o100644; + let s = m.mode_string_full().unwrap(); + assert_eq!(s.as_str(), "-rw-r--r--") + } + + #[test] + fn display_bits_suid() { + let m: u32 = 0o104755; + let s = m.mode_string_full().unwrap(); + assert_eq!(s.as_str(), "-rwsr-xr-x") + } + + #[test] + fn display_bits_sgid() { + let m: u32 = 0o102755; + let s = m.mode_string_full().unwrap(); + assert_eq!(s.as_str(), "-rwxr-sr-x") + } + + #[test] + fn display_bits_sticky() { + let m: u32 = 0o41777; + let s = m.mode_string_full().unwrap(); + assert_eq!(s.as_str(), "drwxrwxrwt") + } +} diff --git a/unix-mode/src/parser.rs b/unix-mode/src/parser.rs new file mode 100644 index 0000000..f801150 --- /dev/null +++ b/unix-mode/src/parser.rs @@ -0,0 +1,386 @@ +use super::{get_umask, Bit, Who}; +use bitflags::BitFlags; +use std::{error, fmt::Display, num::ParseIntError}; + +/// Errors which might occur when parsing Unix permissions from a string +#[derive(Debug, PartialEq)] +pub enum ParseError { + /// the given `Bit` cannot be set for the given `Who` + InvalidBit, + /// the character is not recognized + InvalidChar, + /// the specified octal mode is invalid + OutsideRange, + ParseIntError(ParseIntError), + /// no `Op` is set when parsing a `Bit` + NoOpSet, +} + +impl Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl error::Error for ParseError {} + +impl From for ParseError { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +#[derive(PartialEq)] +/// Operations which can be performed to add, remove, or set explicitly the given +/// `Bit` for the given `Who` +enum Op { + /// `Bit`s will be added for `Who` + Add, + /// `Bit`s will be remoed for `Who` + Remove, + /// `Bit`s will be set to exactly the specified arrangement + Equals, +} + +/// A parser for octal and symbolic permissions. `Parser::default` creates an +/// instance which applies the given operations to the default setting for the +/// current user's umask. `Parser::new` creates a parser which applies the given +/// operations to the specified beginning set of permissions. Therefore, when +/// creating new files or directories use `Parser::default`, while `Parser::new` +/// should be used for setting permissions of already existing files or directories. +pub struct Parser { + mode: u32, + op: Option, + who: u32, + bits: u32, +} + +impl Default for Parser { + fn default() -> Self { + let umask = get_umask(); + let mut mode = 0o0666; + mode &= umask; + Self { + mode, + op: None, + who: 0, + bits: 0, + } + } +} + +impl Parser { + #[must_use] + pub fn new(mode: u32) -> Self { + Self { + mode, + op: None, + who: 0, + bits: 0, + } + } + + fn parse_octal(value: &str) -> Result { + let m = u32::from_str_radix(value, 8)?; + if m <= 0o7777 { + Ok(m) + } else { + Err(ParseError::OutsideRange) + } + } + + fn add_who(&mut self, who: Who) -> Result<(), ParseError> { + if self.op.is_some() || !self.bits == 0 { + Err(ParseError::InvalidChar) + } else { + self.who |= who; + Ok(()) + } + } + + fn set_op(&mut self, op: Op) -> Result<(), ParseError> { + if self.op.is_some() || !self.bits == 0 { + Err(ParseError::InvalidChar) + } else { + self.op = Some(op); + if self.who == 0 { + self.who |= 0o111; + } + Ok(()) + } + } + + fn push_read_bits(&mut self) -> Result<(), ParseError> { + if self.op.is_none() { + Err(ParseError::NoOpSet) + } else { + if self.who.contains(Who::User) { + self.bits |= Bit::URead; + } + if self.who.contains(Who::Group) { + self.bits |= Bit::GRead; + } + if self.who.contains(Who::Other) { + self.bits |= Bit::ORead; + } + Ok(()) + } + } + + fn push_write_bits(&mut self) -> Result<(), ParseError> { + if self.op.is_none() { + Err(ParseError::NoOpSet) + } else { + if self.who.contains(Who::User) { + self.bits |= Bit::UWrite; + } + if self.who.contains(Who::Group) { + self.bits |= Bit::GWrite; + } + if self.who.contains(Who::Other) { + self.bits |= Bit::OWrite; + } + Ok(()) + } + } + + fn push_exec_bits(&mut self) -> Result<(), ParseError> { + if self.op.is_none() { + Err(ParseError::NoOpSet) + } else { + if self.who.contains(Who::User) { + self.bits |= Bit::UExec; + } + if self.who.contains(Who::Group) { + self.bits |= Bit::GExec; + } + if self.who.contains(Who::Other) { + self.bits |= Bit::OExec; + } + Ok(()) + } + } + + fn push_suid_sgid(&mut self) -> Result<(), ParseError> { + if self.who == 0 || self.who.contains(Who::Other) { + return Err(ParseError::InvalidBit); + } else if self.op.is_none() { + return Err(ParseError::NoOpSet); + } + if self.who.contains(Who::User) { + self.bits |= Bit::Suid; + } + if self.who.contains(Who::Group) { + self.bits |= Bit::Sgid; + } + Ok(()) + } + + fn push_sticky(&mut self) -> Result<(), ParseError> { + if self.who == 0 || self.who.contains(Who::User) || self.who.contains(Who::Group) { + return Err(ParseError::InvalidBit); + } else if self.op.is_none() { + return Err(ParseError::NoOpSet); + } + if self.who.contains(Who::Other) { + self.bits |= Bit::Sticky; + } + Ok(()) + } + + fn add_bits(&mut self) { + self.mode |= self.bits; + } + + fn remove_bits(&mut self) { + self.mode &= !self.bits; + } + + fn set_bits(&mut self) -> Result<(), ParseError> { + match self.op { + Some(Op::Add) => self.add_bits(), + Some(Op::Remove) => self.remove_bits(), + Some(Op::Equals) => { + if self.who.contains(Who::User) { + self.mode &= !(0o4700); + } + if self.who.contains(Who::Group) { + self.mode &= !(0o2070); + } + if self.who.contains(Who::Other) { + self.mode &= !(0o1007); + } + self.add_bits(); + } + None => return Err(ParseError::NoOpSet), + } + Ok(()) + } + + fn reset(&mut self) { + self.who = 0; + self.op = None; + self.bits = 0; + } + + #[must_use] + pub fn mode(&self) -> u32 { + self.mode + } + + /// Parses a numerical mode from either an octal string or symbolic representation + /// and applies those permissions to the starting set of permissions. + /// # Errors + /// Returns `ParseError` if: + /// - invalid digit + /// - no operation specified + /// - more than one operation specified at a time (multiple operations can + /// be specified separated by comma) + /// - the specified bit cannot be applied to the specified `Who` + /// - bits are specified before operations + /// - the specified octal mode is greater than 0o7777 + pub fn parse(&mut self, value: &str) -> Result { + match Self::parse_octal(value) { + Ok(mode) => { + self.mode = mode; + return Ok(mode); + } + Err(e) => { + if e == ParseError::OutsideRange { + return Err(e); + } + } + } + for c in value.chars() { + match c { + 'u' => self.add_who(Who::User)?, + 'g' => self.add_who(Who::Group)?, + 'o' => self.add_who(Who::Other)?, + 'a' => { + self.add_who(Who::User)?; + self.add_who(Who::Group)?; + self.add_who(Who::Other)?; + } + '-' => self.set_op(Op::Remove)?, + '+' => self.set_op(Op::Add)?, + '=' => self.set_op(Op::Equals)?, + 'r' => self.push_read_bits()?, + 'w' => self.push_write_bits()?, + 'x' => self.push_exec_bits()?, + 's' => self.push_suid_sgid()?, + 't' => self.push_sticky()?, + ',' => { + self.set_bits()?; + self.reset(); + } + _ => return Err(ParseError::InvalidChar), + } + } + self.set_bits()?; + self.reset(); + Ok(self.mode) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn add() { + let mut parser = Parser::new(0o644); + let mode = parser.parse("ug+x"); + assert_eq!(mode, Ok(0o754)); + } + + #[test] + fn remove() { + let mut parser = Parser::new(0o777); + let mode = parser.parse("go-wx"); + assert_eq!(mode, Ok(0o744)); + } + + #[test] + fn octal() { + let mut parser = Parser::default(); + let mode = parser.parse("4755"); + assert_eq!(mode, Ok(0o4755)); + } + + #[test] + fn equals() { + let mut parser = Parser::new(0o701); + let mode = parser.parse("ugo=rw"); + assert_eq!(mode, Ok(0o666)); + } + + #[test] + fn o_equals() { + let mut parser = Parser::new(0o752); + let mode = parser.parse("o=rx"); + assert_eq!(mode, Ok(0o755)); + } + + #[test] + fn empty_who() { + let mut parser = Parser::new(0o644); + let mode = parser.parse("+x"); + assert_eq!(mode, Ok(0o755)); + } + + #[test] + fn compound_ops() { + let mut parser = Parser::new(0o666); + let mode = parser.parse("u+x,g-w,o-r"); + assert_eq!(mode, Ok(0o742)); + } + + #[test] + fn compount_ops2() { + let mut parser = Parser::new(0o4444); + let mode = parser.parse("u+w,ug+x,o=rx,u-s"); + assert_eq!(mode, Ok(0o755)); + } + + #[test] + fn invalid_sticky_bit() { + let mut parser = Parser::default(); + let mode = parser.parse("u+t"); + assert_eq!(mode, Err(ParseError::InvalidBit)); + } + + #[test] + fn invalid_s() { + let mut parser = Parser::default(); + let mode = parser.parse("+s"); + assert_eq!(mode, Err(ParseError::InvalidBit)); + } + + #[test] + fn outside_range() { + let mut parser = Parser::default(); + let mode = parser.parse("10000"); + assert_eq!(mode, Err(ParseError::OutsideRange)) + } + + #[test] + fn no_op() { + let mut parser = Parser::default(); + let mode = parser.parse("rws"); + assert_eq!(mode, Err(ParseError::NoOpSet)); + } + + #[test] + fn ordering_error() { + let mut parser = Parser::default(); + let mode = parser.parse("ux+s"); + assert_eq!(mode, Err(ParseError::NoOpSet)); + } + + #[test] + fn ordering_error1() { + let mut parser = Parser::default(); + let mode = parser.parse("x+s"); + assert_eq!(mode, Err(ParseError::NoOpSet)); + } +} diff --git a/unix-mode/src/who.rs b/unix-mode/src/who.rs new file mode 100644 index 0000000..f1f21c8 --- /dev/null +++ b/unix-mode/src/who.rs @@ -0,0 +1,48 @@ +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}; + +#[derive(PartialEq)] +/// The granularity of the given permissions +pub enum Who { + /// applies for the current user + User = 0o100, + /// applies for the current group + Group = 0o10, + /// applies for everyone else + Other = 0o1, +} + +impl BitAnd for u32 { + type Output = u32; + + fn bitand(self, rhs: Who) -> Self::Output { + self & rhs as u32 + } +} + +impl BitAnd for Who { + type Output = u32; + + fn bitand(self, rhs: u32) -> Self::Output { + self as u32 & rhs + } +} + +impl BitAndAssign for u32 { + fn bitand_assign(&mut self, rhs: Who) { + *self = *self & rhs; + } +} + +impl BitOr for u32 { + type Output = u32; + + fn bitor(self, rhs: Who) -> Self::Output { + self | rhs as u32 + } +} + +impl BitOrAssign for u32 { + fn bitor_assign(&mut self, rhs: Who) { + *self = *self | rhs; + } +}