use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use anyhow::Result;
use crate::common::{
NORMAL_PERMISSIONS_STR, SETGID_PERMISSIONS_STR, SETUID_PERMISSIONS_STR, STICKY_PERMISSIONS_STR,
};
use crate::io::execute_without_output;
use crate::modes::Flagged;
use crate::{log_info, log_line};
type Mode = u32;
pub struct Permissions;
pub const MAX_FILE_MODE: Mode = 0o777;
pub const MAX_SPECIAL_MODE: Mode = 0o7777;
impl Permissions {
pub fn set_permissions_of_flagged(mode_str: &str, flagged: &Flagged) -> Result<()> {
log_info!("set_permissions_of_flagged mode_str {mode_str}");
if let Some(mode) = ModeParser::from_str(mode_str) {
for path in &flagged.content {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode.numeric()))?;
}
log_line!("Changed permissions to {mode_str}");
} else if Self::validate_chmod_args(mode_str) {
Self::execute_chmod_for_flagged(mode_str, flagged)?;
}
Ok(())
}
fn validate_chmod_args(mode_str: &str) -> bool {
let chars: Vec<_> = mode_str.chars().collect();
match chars.len() {
3 => {
let (dest, action, permission) = (chars[0], chars[1], chars[2]);
Self::validate_chmod_3(dest, action, permission)
}
2 => {
let (action, permission) = (chars[0], chars[1]);
Self::validate_chmod_2(action, permission)
}
_ => {
log_info!("{mode_str} isn't a valid chmod argument. Length should be 2 or 3.");
false
}
}
}
fn validate_chmod_3(dest: char, action: char, permission: char) -> bool {
if !"agou".contains(dest) {
log_info!("{dest} isn't a valid chmod argument. The first char should be 'a', 'g', 'o' or 'u'.");
return false;
}
Self::validate_chmod_2(action, permission)
}
fn validate_chmod_2(action: char, permission: char) -> bool {
if !"+-".contains(action) {
log_info!(
"{action} isn't a valid chmod argument. The second char should be '+' or '-'."
);
return false;
}
if !"XrstwxT".contains(permission) {
log_info!("{permission} isn't a valid chmod argument. The third char should be 'X', 'r', 's', 't', 'w' or 'x' or 'T'.");
return false;
}
true
}
fn execute_chmod_for_flagged(mode_str: &str, flagged: &Flagged) -> Result<()> {
let flagged: Vec<_> = flagged
.content
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
let flagged = flagged.join(" ");
let chmod_args: &str = &format!("chmod {mode_str} {flagged}");
let executable = "/usr/bin/sh";
let args = vec!["-c", chmod_args];
execute_without_output(executable, &args)?;
Ok(())
}
}
trait AsOctal<T> {
fn as_octal(&self) -> T;
}
impl AsOctal<u32> for str {
fn as_octal(&self) -> u32 {
u32::from_str_radix(self, 8).unwrap_or_default()
}
}
type IsValid = bool;
pub fn parse_input_permission(mode_str: &str) -> (Arc<str>, IsValid) {
log_info!("parse_input_permission: {mode_str}");
if mode_str.chars().any(|c| c.is_alphabetic()) {
(Arc::from(""), true)
} else if mode_str.chars().all(|c| c.is_digit(8)) {
(permission_mode_to_str(mode_str.as_octal()), true)
} else {
(Arc::from("Unreadable mode"), false)
}
}
struct ModeParser(Mode);
impl ModeParser {
const fn numeric(&self) -> Mode {
self.0
}
fn from_str(mode_str: &str) -> Option<Self> {
if let Some(mode) = Self::from_numeric(mode_str) {
return Some(mode);
}
Self::from_alphabetic(mode_str)
}
fn from_numeric(mode_str: &str) -> Option<Self> {
if let Ok(mode) = Mode::from_str_radix(mode_str, 8) {
if Self::is_valid_permissions(mode) {
return Some(Self(mode));
}
}
None
}
fn from_alphabetic(mode_str: &str) -> Option<Self> {
if mode_str.len() != 9 {
return None;
}
let mut mode = 0;
for (index, current_char) in mode_str.chars().enumerate() {
let Some(increment) = Self::evaluate_index_char(index, current_char) else {
log_info!("Invalid char in permissions '{current_char}' at position {index}");
return None;
};
mode += increment;
}
if Self::is_valid_permissions(mode) {
return Some(Self(mode));
}
None
}
fn evaluate_index_char(index: usize, current_char: char) -> Option<u32> {
match current_char {
'-' | '.' => Some(0o000),
'r' if index == 0 => Some(0o0400),
'w' if index == 1 => Some(0o0200),
'x' if index == 2 => Some(0o0100),
'S' if index == 2 => Some(0o4000),
's' if index == 2 => Some(0o4100),
'r' if index == 3 => Some(0o0040),
'w' if index == 4 => Some(0o0020),
'x' if index == 5 => Some(0o0010),
'S' if index == 5 => Some(0o2000),
's' if index == 5 => Some(0o2010),
'r' if index == 6 => Some(0o0004),
'w' if index == 7 => Some(0o0002),
'x' if index == 8 => Some(0o0001),
'T' if index == 8 => Some(0o1000),
't' if index == 8 => Some(0o1001),
_ => None,
}
}
const fn is_valid_permissions(mode: Mode) -> bool {
mode <= MAX_SPECIAL_MODE
}
}
trait ToBool {
fn to_bool(self) -> bool;
}
impl ToBool for u32 {
fn to_bool(self) -> bool {
(self & 1) == 1
}
}
#[inline]
fn extract_setuid_flag(special: u32) -> bool {
(special >> 2).to_bool()
}
#[inline]
fn extract_setgid_flag(special: u32) -> bool {
(special >> 1).to_bool()
}
#[inline]
fn extract_sticky_flag(special: u32) -> bool {
special.to_bool()
}
pub fn permission_mode_to_str(mode: u32) -> Arc<str> {
let mode = mode & 0o7777;
let special = mode >> 9;
let owner_strs = if extract_setuid_flag(special) {
SETUID_PERMISSIONS_STR
} else {
NORMAL_PERMISSIONS_STR
};
let group_strs = if extract_setgid_flag(special) {
SETGID_PERMISSIONS_STR
} else {
NORMAL_PERMISSIONS_STR
};
let sticky_strs = if extract_sticky_flag(special) {
STICKY_PERMISSIONS_STR
} else {
NORMAL_PERMISSIONS_STR
};
let normal_mode = (mode & MAX_FILE_MODE) as usize;
let s_o = convert_octal_mode(owner_strs, normal_mode >> 6);
let s_g = convert_octal_mode(group_strs, (normal_mode >> 3) & 7);
let s_a = convert_octal_mode(sticky_strs, normal_mode & 7);
Arc::from([s_o, s_g, s_a].join(""))
}
fn convert_octal_mode(permission_str: [&'static str; 8], mode: usize) -> &'static str {
permission_str[mode]
}