fm/modes/menu/
permissions.rs

1use std::os::unix::fs::PermissionsExt;
2use std::sync::Arc;
3
4use anyhow::Result;
5
6use crate::common::{
7    NORMAL_PERMISSIONS_STR, SETGID_PERMISSIONS_STR, SETUID_PERMISSIONS_STR, STICKY_PERMISSIONS_STR,
8};
9use crate::io::execute_without_output;
10use crate::modes::Flagged;
11use crate::{log_info, log_line};
12
13type Mode = u32;
14
15/// Empty struct used to regroup some methods.
16pub struct Permissions;
17
18/// Maximum possible mode for a file, ignoring special bits, 0o777 = 511 (decimal), aka "rwx".
19pub const MAX_FILE_MODE: Mode = 0o777;
20pub const MAX_SPECIAL_MODE: Mode = 0o7777;
21
22impl Permissions {
23    /// Change permission of the flagged files.
24    /// Once the user has typed an octal permission like 754, it's applied to
25    /// the file.
26    /// Nothing is done if the user typed nothing or an invalid permission like
27    /// 955.
28    ///
29    /// # Errors
30    ///
31    /// It may fail if the permissions can't be set by the user.
32    pub fn set_permissions_of_flagged(mode_str: &str, flagged: &Flagged) -> Result<()> {
33        log_info!("set_permissions_of_flagged mode_str {mode_str}");
34        if let Some(mode) = ModeParser::from_str(mode_str) {
35            for path in &flagged.content {
36                std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode.numeric()))?;
37            }
38            log_line!("Changed permissions to {mode_str}");
39        } else if Self::validate_chmod_args(mode_str) {
40            Self::execute_chmod_for_flagged(mode_str, flagged)?;
41        }
42        Ok(())
43    }
44
45    /// True if a `mode_str` is a valid chmod argument.
46    /// This function only validates input in the form "a+x" or "-r"
47    ///
48    /// The length should be 2 or 3.
49    /// If any, the first char should be a, g, o or u
50    /// The second char should be + or -
51    /// The third char should be X r s t w x
52    fn validate_chmod_args(mode_str: &str) -> bool {
53        let chars: Vec<_> = mode_str.chars().collect();
54        match chars.len() {
55            3 => {
56                let (dest, action, permission) = (chars[0], chars[1], chars[2]);
57                Self::validate_chmod_3(dest, action, permission)
58            }
59            2 => {
60                let (action, permission) = (chars[0], chars[1]);
61                Self::validate_chmod_2(action, permission)
62            }
63            _ => {
64                log_info!("{mode_str} isn't a valid chmod argument. Length should be 2 or 3.");
65                false
66            }
67        }
68    }
69
70    fn validate_chmod_3(dest: char, action: char, permission: char) -> bool {
71        if !"agou".contains(dest) {
72            log_info!("{dest} isn't a valid chmod argument. The first char should be 'a', 'g', 'o' or 'u'.");
73            return false;
74        }
75        Self::validate_chmod_2(action, permission)
76    }
77
78    fn validate_chmod_2(action: char, permission: char) -> bool {
79        if !"+-".contains(action) {
80            log_info!(
81                "{action} isn't a valid chmod argument. The second char should be '+' or '-'."
82            );
83            return false;
84        }
85        if !"XrstwxT".contains(permission) {
86            log_info!("{permission} isn't a valid chmod argument. The third char should be 'X', 'r', 's', 't', 'w' or 'x' or 'T'.");
87            return false;
88        }
89        true
90    }
91
92    /// The executor doesn't check if the user has the right permissions for this file.
93    fn execute_chmod_for_flagged(mode_str: &str, flagged: &Flagged) -> Result<()> {
94        let flagged: Vec<_> = flagged
95            .content
96            .iter()
97            .map(|p| p.to_string_lossy().to_string())
98            .collect();
99        let flagged = flagged.join(" ");
100        let chmod_args: &str = &format!("chmod {mode_str} {flagged}");
101
102        let executable = "/usr/bin/sh";
103        let args = vec!["-c", chmod_args];
104        execute_without_output(executable, &args)?;
105        Ok(())
106    }
107}
108
109trait AsOctal<T> {
110    /// Converts itself to an octal if possible, 0 otherwise.
111    fn as_octal(&self) -> T;
112}
113
114impl AsOctal<u32> for str {
115    fn as_octal(&self) -> u32 {
116        u32::from_str_radix(self, 8).unwrap_or_default()
117    }
118}
119
120type IsValid = bool;
121
122/// Parse an inputstring into a displayed textual permission.
123/// Converts `644` into `rw-r--r--` and like so,
124/// Converts `944` into `???r--r--` and like so,
125/// Converts `66222` into "Mode is too long".
126/// It also returns a flag for any char, set to true if the char
127/// is a valid permission.
128/// It's used to display a valid mode or not.
129pub fn parse_input_permission(mode_str: &str) -> (Arc<str>, IsValid) {
130    log_info!("parse_input_permission: {mode_str}");
131    if mode_str.chars().any(|c| c.is_alphabetic()) {
132        (Arc::from(""), true)
133    } else if mode_str.chars().all(|c| c.is_digit(8)) {
134        (permission_mode_to_str(mode_str.as_octal()), true)
135    } else {
136        (Arc::from("Unreadable mode"), false)
137    }
138}
139
140struct ModeParser(Mode);
141
142impl ModeParser {
143    const fn numeric(&self) -> Mode {
144        self.0
145    }
146
147    fn from_str(mode_str: &str) -> Option<Self> {
148        if let Some(mode) = Self::from_numeric(mode_str) {
149            return Some(mode);
150        }
151        Self::from_alphabetic(mode_str)
152    }
153
154    fn from_numeric(mode_str: &str) -> Option<Self> {
155        if let Ok(mode) = Mode::from_str_radix(mode_str, 8) {
156            if Self::is_valid_permissions(mode) {
157                return Some(Self(mode));
158            }
159        }
160        None
161    }
162
163    /// Convert a string of 9 chars a numeric mode.
164    /// It will only accept basic strings like "rw.r...." or "rw-r----".
165    /// Special chars (s, S, t, T) are recognized correctly.
166    ///
167    /// If `mode_str` isn't 9 chars long, it's rejected.
168    /// mode is set to 0.
169    /// We simply read the chars and add :
170    ///     - Reject if the position isn't possible (S can't be the last char (index=8), it should be in .-xtT)
171    ///         This step is just for logging, we could simply add u32::MAX and let the last step do the rejection.
172    ///     - Add the corresponding value to the mode.
173    fn from_alphabetic(mode_str: &str) -> Option<Self> {
174        // rwxrwxrwx
175        if mode_str.len() != 9 {
176            return None;
177        }
178
179        let mut mode = 0;
180        for (index, current_char) in mode_str.chars().enumerate() {
181            let Some(increment) = Self::evaluate_index_char(index, current_char) else {
182                log_info!("Invalid char in permissions '{current_char}' at position {index}");
183                return None;
184            };
185            mode += increment;
186        }
187        if Self::is_valid_permissions(mode) {
188            return Some(Self(mode));
189        }
190
191        None
192    }
193
194    /// Since every symbol has a value according to its position, we simply associate it.
195    /// It should be impossible to have an invalid char
196    fn evaluate_index_char(index: usize, current_char: char) -> Option<u32> {
197        match current_char {
198            '-' | '.' => Some(0o000),
199
200            'r' if index == 0 => Some(0o0400),
201            'w' if index == 1 => Some(0o0200),
202            'x' if index == 2 => Some(0o0100),
203            'S' if index == 2 => Some(0o4000),
204            's' if index == 2 => Some(0o4100),
205
206            'r' if index == 3 => Some(0o0040),
207            'w' if index == 4 => Some(0o0020),
208            'x' if index == 5 => Some(0o0010),
209            'S' if index == 5 => Some(0o2000),
210            's' if index == 5 => Some(0o2010),
211
212            'r' if index == 6 => Some(0o0004),
213            'w' if index == 7 => Some(0o0002),
214            'x' if index == 8 => Some(0o0001),
215            'T' if index == 8 => Some(0o1000),
216            't' if index == 8 => Some(0o1001),
217
218            _ => None,
219        }
220    }
221
222    const fn is_valid_permissions(mode: Mode) -> bool {
223        mode <= MAX_SPECIAL_MODE
224    }
225}
226
227trait ToBool {
228    fn to_bool(self) -> bool;
229}
230
231impl ToBool for u32 {
232    fn to_bool(self) -> bool {
233        (self & 1) == 1
234    }
235}
236
237#[inline]
238fn extract_setuid_flag(special: u32) -> bool {
239    (special >> 2).to_bool()
240}
241
242#[inline]
243fn extract_setgid_flag(special: u32) -> bool {
244    (special >> 1).to_bool()
245}
246
247#[inline]
248fn extract_sticky_flag(special: u32) -> bool {
249    special.to_bool()
250}
251/// Reads the permission and converts them into a string.
252pub fn permission_mode_to_str(mode: u32) -> Arc<str> {
253    let mode = mode & 0o7777;
254    let special = mode >> 9;
255    let owner_strs = if extract_setuid_flag(special) {
256        SETUID_PERMISSIONS_STR
257    } else {
258        NORMAL_PERMISSIONS_STR
259    };
260    let group_strs = if extract_setgid_flag(special) {
261        SETGID_PERMISSIONS_STR
262    } else {
263        NORMAL_PERMISSIONS_STR
264    };
265    let sticky_strs = if extract_sticky_flag(special) {
266        STICKY_PERMISSIONS_STR
267    } else {
268        NORMAL_PERMISSIONS_STR
269    };
270    let normal_mode = (mode & MAX_FILE_MODE) as usize;
271    let s_o = convert_octal_mode(owner_strs, normal_mode >> 6);
272    let s_g = convert_octal_mode(group_strs, (normal_mode >> 3) & 7);
273    let s_a = convert_octal_mode(sticky_strs, normal_mode & 7);
274    Arc::from([s_o, s_g, s_a].join(""))
275}
276
277/// Convert an integer like `Oo7` into its string representation like `"rwx"`
278fn convert_octal_mode(permission_str: [&'static str; 8], mode: usize) -> &'static str {
279    permission_str[mode]
280}