From 7abc6c007af75504fc90be3dca00adb693edd759 Mon Sep 17 00:00:00 2001 From: Arcterus Date: Sat, 25 Oct 2014 20:27:45 -0700 Subject: [PATCH] Implement most of chmod --- Cargo.toml | 4 + Makefile | 1 + src/chmod/chmod.rs | 296 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 src/chmod/chmod.rs diff --git a/Cargo.toml b/Cargo.toml index 1297d8674..0c7483bdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ path = "basename/basename.rs" name = "cat" path = "cat/cat.rs" +[[bin]] +name = "chmod" +path = "chmod/chmod.rs" + [[bin]] name = "chroot" path = "chroot/chroot.rs" diff --git a/Makefile b/Makefile index b46a86187..6d0996bca 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ PROGS := \ base64 \ basename \ cat \ + chmod \ cksum \ comm \ cp \ diff --git a/src/chmod/chmod.rs b/src/chmod/chmod.rs new file mode 100644 index 000000000..4517e0e71 --- /dev/null +++ b/src/chmod/chmod.rs @@ -0,0 +1,296 @@ +#![crate_name = "chmod"] + +/* + * This file is part of the uutils coreutils package. + * + * (c) Arcterus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#![feature(macro_rules, phase)] + +#![allow(unused_variables)] // only necessary while the TODOs still exist + +extern crate getopts; +extern crate libc; +extern crate native; +extern crate regex; +#[phase(plugin)] extern crate regex_macros; + +use std::io::fs; +use std::io::fs::PathExtensions; +use std::u64; +use regex::Regex; + +#[path = "../common/util.rs"] +mod util; + +const NAME: &'static str = "chmod"; +const VERSION: &'static str = "1.0.0"; + +pub fn uumain(args: Vec) -> int { + let program = args[0].clone(); + + let opts = [ + getopts::optflag("c", "changes", "like verbose but report only when a change is made (unimplemented)"), + getopts::optflag("f", "quiet", "suppress most error messages (unimplemented)"), // TODO: support --silent + getopts::optflag("v", "verbose", "output a diagnostic for every file processed (unimplemented)"), + getopts::optflag("", "no-preserve-root", "do not treat '/' specially (the default)"), + getopts::optflag("", "preserve-root", "fail to operate recursively on '/'"), + getopts::optflagopt("", "reference", "use RFILE's mode instead of MODE values", "RFILE"), + getopts::optflag("R", "recursive", "change files and directories recursively"), + getopts::optflag("h", "help", "display this help and exit"), + getopts::optflag("V", "version", "output version information and exit") + ]; + // TODO: sanitize input for - at beginning (e.g. chmod -x testfile). Solution is to add a to -x, making a-x + let mut matches = match getopts::getopts(args.tail(), opts) { + Ok(m) => m, + Err(f) => { + crash!(1, "{}", f) + } + }; + if matches.opt_present("help") { + println!("{name} v{version} + +Usage: + {program} [OPTION]... MODE[,MODE]... FILE... + {program} [OPTION]... OCTAL-MODE FILE... + {program} [OPTION]... --reference=RFILE FILE... + +{usage} +Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.", + name = NAME, version = VERSION, program = program, + usage = getopts::usage("Change the mode of each FILE to MODE. \ + With --reference, change the mode of \ + each FILE to that of RFILE.", opts)); + } else if matches.opt_present("version") { + println!("{} v{}", NAME, VERSION); + } else if matches.free.is_empty() && matches.opt_present("reference") || matches.free.len() < 2 { + show_error!("missing an argument"); + show_error!("for help, try '{} --help'", program); + return 1; + } else { + let changes = matches.opt_present("changes"); + let quiet = matches.opt_present("quiet"); + let verbose = matches.opt_present("verbose"); + let preserve_root = matches.opt_present("preserve-root"); + let recursive = matches.opt_present("recursive"); + let fmode = matches.opt_str("reference").and_then(|fref| { + match native::io::file::stat(&fref.to_c_str()) { + Ok(stat) => Some(stat.perm), + Err(f) => crash!(1, "{}", f) + } + }); + let cmode = + if fmode.is_none() { + let mode = matches.free.remove(0).unwrap(); // there are at least two elements + match verify_mode(mode.as_slice()) { + Ok(_) => Some(mode), + Err(f) => { + show_error!("{}", f); + return 1; + } + } + } else { + None + }; + match chmod(matches.free, changes, quiet, verbose, preserve_root, + recursive, fmode, cmode.as_ref()) { + Ok(()) => {} + Err(e) => return e + } + } + + 0 +} + +#[cfg(unix)] +#[inline] +fn verify_mode(modes: &str) -> Result<(), String> { + static REGEXP: regex::Regex = regex!(r"^[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+$"); + for mode in modes.split(',') { + if !REGEXP.is_match(mode) { + return Err(format!("invalid mode '{}'", mode)); + } + } + Ok(()) +} + +#[cfg(windows)] +#[inline] +// XXX: THIS IS NOT TESTED!!! +fn verify_mode(mode: &str) -> Result<(), String> { + static REGEXP: regex::Regex = regex!(r"^[ugoa]*(?:[-+=](?:([rwxXst]*)|[ugo]))+|[-+=]?([0-7]+)$"); + for mode in modes.split(',') { + match REGEXP.captures(mode) { + Some(cap) => { + let symbols = cap.at(1); + let numbers = cap.at(2); + if symbols.contains("s") || symbols.contains("t") { + return Err("The 's' and 't' modes are not supported on Windows".to_string()); + } else if numbers.len() >= 4 && numbers.slice_to(num_len - 3).find(|ch| ch != '0').is_some() { + return Err("Setuid, setgid, and sticky modes are not supported on Windows".to_string()); + } + } + None => return Err(format!("invalid mode '{}'", mode)) + } + } + Ok(()) +} + +fn chmod(files: Vec, changes: bool, quiet: bool, verbose: bool, preserve_root: bool, recursive: bool, fmode: Option, cmode: Option<&String>) -> Result<(), int> { + let mut r = Ok(()); + + for filename in files.iter() { + let filename = filename.as_slice(); + let file = Path::new(filename); + if file.exists() { + if file.is_dir() { + if !preserve_root || filename != "/" { + if recursive { + let walk_dir = match fs::walk_dir(&file) { + Ok(m) => m, + Err(f) => { + crash!(1, "{}", f.to_string()); + } + }; + r = chmod(walk_dir.map(|x| x.as_str().unwrap().to_string()).collect(), changes, quiet, verbose, preserve_root, recursive, fmode, cmode).and(r); + r = chmod_file(&file, filename, changes, quiet, verbose, fmode, cmode).and(r); + } + } else { + show_error!("could not change permissions of directory '{}'", + filename); + r = Err(1); + } + } else { + r = chmod_file(&file, filename.as_slice(), changes, quiet, verbose, fmode, cmode).and(r); + } + } else { + show_error!("no such file or directory '{}'", filename); + r = Err(1); + } + } + + r +} + +fn chmod_file(file: &Path, name: &str, changes: bool, quiet: bool, verbose: bool, fmode: Option, cmode: Option<&String>) -> Result<(), int> { + let path = name.to_c_str(); + match fmode { + Some(mode) => { + match native::io::file::chmod(&path, mode as uint) { + Ok(_) => { /* TODO: handle changes, quiet, and verbose */ } + Err(f) => { + show_error!("{}", f); + return Err(1); + } + } + } + None => { + // TODO: make the regex processing occur earlier (i.e. once in the main function) + static REGEXP: regex::Regex = regex!(r"^(([ugoa]*)((?:[-+=](?:[rwxXst]*|[ugo]))+))|([-+=]?[0-7]+)$"); + let mut fperm = match native::io::file::stat(&path) { + Ok(stat) => stat.perm, + Err(f) => { + show_error!("{}", f); + return Err(1); + } + }; + for mode in cmode.unwrap().as_slice().split(',') { // cmode is guaranteed to be Some in this case + let cap = REGEXP.captures(mode).unwrap(); // mode was verified earlier, so this is safe + if cap.at(1) != "" { + // symbolic + let mut levels = cap.at(2); + if levels.len() == 0 { + levels = "a"; + } + let change_str = cap.at(3).to_string() + "+"; + let change = change_str.as_slice(); + let mut action = change.char_at(0); + let mut rwx = 0; + let mut special = 0; + let mut special_changed = false; + for ch in change.slice_from(1).chars() { + match ch { + '+' | '-' | '=' => { + for level in levels.chars() { + let (rwx, mask) = match level { + 'u' => (rwx << 6, 0o7077), + 'g' => (rwx << 3, 0o7707), + 'o' => (rwx, 0o7770), + 'a' => ((rwx << 6) | (rwx << 3) | rwx, 0o7000), + _ => unreachable!() + }; + match action { + '+' => fperm |= rwx, + '-' => fperm &= !rwx, + '=' => fperm = (fperm & mask) | rwx, + _ => unreachable!() + } + } + if special_changed { + match action { + '+' => fperm |= special, + '-' => fperm &= !special, + '=' => fperm &= special | 0o0777, + _ => unreachable!() + } + } + action = ch; + rwx = 0; + special = 0; + special_changed = false; + } + 'r' => rwx |= 0o004, + 'w' => rwx |= 0o002, + 'x' => rwx |= 0o001, + 'X' => { + if file.is_dir() || (fperm & 0o0111) != 0 { + rwx |= 0o001; + } + } + 's' => { + special |= 0o4000 | 0o2000; + special_changed = true; + } + 't' => { + special |= 0o1000; + special_changed = true; + } + 'u' => rwx = (fperm >> 6) & 0o007, + 'g' => rwx = (fperm >> 3) & 0o007, + 'o' => rwx = (fperm >> 0) & 0o007, + _ => unreachable!() + } + } + } else { + // numeric + let change = cap.at(4); + let ch = change.char_at(0); + let (action, slice) = match ch { + '+' | '-' | '=' => (ch, change.slice_from(1)), + _ => ('=', change) + }; + let mode = u64::parse_bytes(slice.as_bytes(), 8).unwrap(); // already verified + match action { + '+' => fperm |= mode, + '-' => fperm &= !mode, + '=' => fperm = mode, + _ => unreachable!() + } + } + match native::io::file::chmod(&path, fperm as uint) { + Ok(_) => { /* TODO: see above */ } + Err(f) => { + show_error!("{}", f); + return Err(1); + } + } + } + } + } + + Ok(()) +}