diff --git a/Makefile b/Makefile index 5e2c70d..b7bce98 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ RUSTLIBS = --extern getopt=build/o/libgetopt.rlib \ CFLAGS += -I$(SYSEXITS) .PHONY: all -all: dj false fop hru intcmp mm npc rpn scrut str strcmp swab true +all: dj false fileis fop hru intcmp mm npc rpn scrut str strcmp swab true # keep build/include until bindgen(1) has stdin support # https://github.com/rust-lang/rust-bindgen/issues/2703 @@ -97,6 +97,11 @@ false: build/bin/false build/bin/false: src/false.c build $(CC) $(CFLAGS) -o $@ src/false.c +.PHONY: fileis +fileis: build/bin/fileis +build/bin/fileis: src/fileis.rs build + $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/fileis.rs + .PHONY: fop fop: build/bin/fop build/bin/fop: src/fop.rs build rustlibs @@ -127,11 +132,6 @@ rpn: build/bin/rpn build/bin/rpn: src/rpn.rs build rustlibs $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/rpn.rs -.PHONY: scrut -scrut: build/bin/scrut -build/bin/scrut: src/scrut.c build - $(CC) $(CFLAGS) -o $@ src/scrut.c - .PHONY: str str: build/bin/str build/bin/str: src/str.c build diff --git a/docs/scrut.1 b/docs/fileis.1 similarity index 95% rename from docs/scrut.1 rename to docs/fileis.1 index 2b95bee..ed578a6 100644 --- a/docs/scrut.1 +++ b/docs/fileis.1 @@ -4,12 +4,12 @@ .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . .\" -.TH SCRUT 1 2024-06-06 "Harakit X.X.X" +.TH SCRUT 1 2024-07-18 "Harakit X.X.X" .SH NAME -scrut \(en scrutinize file properties +fileis \(en scrutinize file properties .SH SYNOPSIS -scrut +fileis .RB [ -LSbcdefgkprsuwx ] .B file... .\" @@ -82,5 +82,6 @@ Copyright \(co 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later .\" .SH SEE ALSO .BR access (3p), +.BR chown (1p), .BR lstat (3p), .BR test (1p) diff --git a/src/fileis.rs b/src/fileis.rs new file mode 100644 index 0000000..b81b624 --- /dev/null +++ b/src/fileis.rs @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 DTB + * Copyright (c) 2024 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +use std::{ + env::args, + fs::metadata, + os::unix::fs::{ FileTypeExt, MetadataExt }, + process::ExitCode, +}; + +extern crate getopt; +extern crate strerror; +extern crate sysexits; + +use getopt::GetOpt; +use strerror::StrError; +use sysexits::EX_USAGE; + +const OPTS: &str = "bcdefgkprsuwxLS"; + +fn usage(argv0: &str) -> ExitCode { + eprintln!("Usage: {} [-{}] file...", argv0, OPTS); + ExitCode::from(EX_USAGE) +} + +fn main() -> ExitCode { + let argv = args().collect::>(); + + let mut sel = String::with_capacity(OPTS.len()); // selected options + let mut optind: usize = 1; // argv[0] + + while let Some(opt) = argv.getopt(OPTS) { + if let Ok(optchr) = opt.opt() { sel.push_str(optchr); } + else { return usage(&argv[0]); } + + optind = opt.ind(); + } + + if optind == argv.len() { return usage(&argv[0]); } + + for arg in argv.iter().skip(optind) { + let fmeta = match metadata(arg) { + Ok(m) => m, + Err(e) => { // no perms or nonexistent + eprintln!("{}: {}: {}", argv[0], arg, e.strerror()); + return ExitCode::FAILURE; + }, + }; + + let fmode = fmeta.mode(); + let ftype = fmeta.file_type(); + + for selection in sel.chars() { // run all selected tests + match selection { + 'b' if ftype.is_block_device() => (), + 'c' if ftype.is_char_device() => (), + 'e' => (), // exists or metadata would have errored + 'd' if fmeta.is_dir() => (), + 'f' if fmeta.is_file() => (), + 'g' if fmode & 0o2000 /* S_ISGID */ != 0 => (), // setgid + 'k' if fmode & 0o1000 /* S_ISVTX */ != 0 => (), // setvtx + 'p' if ftype.is_fifo() => (), + 'r' if fmode & 0o0400 /* S_IRUSR */ != 0 => (), // read access + 'u' if fmode & 0o4000 /* S_ISUID */ != 0 => (), // setuid + 'w' if fmode & 0o0200 /* S_IWUSR */ != 0 => (), // write access + 'x' if fmode & 0o0100 /* S_IXUSR */ != 0 => (), // exec access + 'L' if fmeta.is_symlink() => (), + 'S' if ftype.is_socket() => (), + _ => { return ExitCode::FAILURE; } + } + } + } + + ExitCode::SUCCESS +} diff --git a/src/scrut.c b/src/scrut.c deleted file mode 100644 index d85d243..0000000 --- a/src/scrut.c +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2023–2024 DTB - * Copyright (c) 2024 Emma Tebibyte - * SPDX-License-Identifier: AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, either version 3 of the License, or (at your option) any - * later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ - -#include /* fprintf(3), stderr, NULL */ -#include /* EXIT_FAILURE, EXIT_SUCCESS */ -#include /* memset(3), strchr(3) */ -#ifndef EX_USAGE -# include -#endif -#include /* access(3), getopt(3), F_OK, R_OK, W_OK, X_OK */ -#include /* lstat(3), stat struct, S_ISBLK, S_ISCHR, S_ISDIR, - * S_ISFIFO, S_ISGID, S_ISREG, S_ISLNK, S_ISSOCK, - * S_ISUID, S_ISVTX */ - -static char args[] = "bcdefghkprsuwxLS"; -static char ops[(sizeof args) / (sizeof *args)]; -static char *program_name = "scrut"; - -int main(int argc, char *argv[]){ - struct stat buf; - int c; - size_t i; - char *p; - - if(argc < 2) - goto usage; - - memset(ops, '\0', sizeof ops); - while((c = getopt(argc, argv, args)) != -1) - if((p = strchr(args, c)) == NULL) - goto usage; - else - ops[p - args] = c; - /* straighten out ops */ - for(i = 0, p = ops; i < (sizeof ops) / (sizeof *ops); ++i) - if(ops[i] != '\0'){ - *p = ops[i]; - if(&ops[i] != p++) - ops[i] = '\0'; - } - - if(optind == argc) - goto usage; - - argv += optind; - do{ if(access(*argv, F_OK) != 0 || lstat(*argv, &buf) == -1) - return EXIT_FAILURE; /* doesn't exist or isn't stattable */ - - for(i = 0; ops[i] != '\0'; ++i) - if(ops[i] == 'e') - continue; - else if(ops[i] == 'h'){ -usage: fprintf(stderr, "Usage: %s [-%s] file...\n", - argv[0] == NULL - ? program_name - : argv[0], - args); - - return EX_USAGE; - }else if( - (ops[i] == 'b' - && !S_ISBLK(buf.st_mode)) - || (ops[i] == 'c' - && !S_ISCHR(buf.st_mode)) - || (ops[i] == 'd' - && !S_ISDIR(buf.st_mode)) - || (ops[i] == 'f' - && !S_ISREG(buf.st_mode)) - || (ops[i] == 'g' - && !(buf.st_mode & S_ISGID)) - || (ops[i] == 'k' - && !(buf.st_mode & S_ISVTX)) - || (ops[i] == 'p' - && !S_ISFIFO(buf.st_mode)) - || (ops[i] == 'r' - && access(*argv, R_OK) != 0) - || (ops[i] == 'u' - && !(buf.st_mode & S_ISUID)) - || (ops[i] == 'w' - && access(*argv, W_OK) != 0) - || (ops[i] == 'x' - && access(*argv, X_OK) != 0) - || (ops[i] == 'L' - && !S_ISLNK(buf.st_mode)) - || (ops[i] == 'S' - && !S_ISSOCK(buf.st_mode))) - return EXIT_FAILURE; - }while(*++argv != NULL); - - return EXIT_SUCCESS; -}