diff --git a/Makefile b/Makefile index 6e92fd9..98e2c9f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2023–2024 Emma Tebibyte +# Copyright (c) 2023–2025 Emma Tebibyte # Copyright (c) 2023–2024 DTB # Copyright (c) 2023 Sasha Koshka # Copyright (c) 2024 Aaditya Aryal @@ -42,7 +42,7 @@ BIN = build/bin default: all test .PHONY: all -all: dj false fop hru intcmp mm npc peek rpn scrut str strcmp swab true +all: dj false fileis fop hru intcmp mm npc peek rpn str strcmp swab true # keep build/include until bindgen(1) has stdin support # https://github.com/rust-lang/rust-bindgen/issues/2703 @@ -112,6 +112,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 @@ -147,11 +152,6 @@ rpn: build/bin/rpn build/bin/rpn: src/rpn.rs build rustlibs $(RUSTC) $(RUSTFLAGS) -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 92% rename from docs/scrut.1 rename to docs/fileis.1 index 2b95bee..9650f73 100644 --- a/docs/scrut.1 +++ b/docs/fileis.1 @@ -1,15 +1,15 @@ .\" Copyright (c) 2024 DTB -.\" Copyright (c) 2024 Emma Tebibyte +.\" Copyright (c) 2024–2025 Emma Tebibyte .\" .\" 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 FILEIS 1 2025-02-24 "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 +}