diff --git a/CONDUCT b/CONDUCT new file mode 100644 index 0000000..4dd97a9 --- /dev/null +++ b/CONDUCT @@ -0,0 +1,83 @@ +Code of Conduct + +This Code of Conduct is derived from the 10 Pāramitās of Theravadin Buddhism. +You can read more about them in Ṭhānissaro Bhikkhu’s Ten Perfections: A Study +Guide [0]. + +1. Generosity (Dāna) + +Give contributions freely and willingly under the terms of the GNU Affero +General Public License, version 3 or later, or a compatible license. + +2. Ethics (Sīla) + +Do not use nonfree code or uncredited code in contributions. Do not contribute +code of dubious origins, such as code generated by large language models or +unlicensed snippets found online [1]. Do not take credit for others’ +contributions. Make sure to utilize the copyright header and license notice on +source files to credit yourself and others for their work. + +3. Renunciation (Nekkhamma) + +Stay committed to the principles of simplicity and interoperability embodied by +the project. Keep your personal will and desire out of the project, for it can +only prove harmful to its success. + +4. Wisdom (Pañña) + +Look to established sources for standards, best practices, and important +implementation details when setting new precedence. Follow the existing +precedence where it applies. + +5. Energy (Viriya) + +Focus on the currently-open, currently-assigned, and currently-in-progress +issues, pull requests, and other endeavors in order to keep yourself and others +from being overwhelmed with responsibility, either from your zeal or your +negligence. + +If you notice an issue, open an issue as soon as you can. If you see a neglected +branch, open a pull request or comment on an existing one, if applicable. Be +diligent in your commitment to making this project work. + +6. Patience (Khanti) + +Be patient with maintainers and other contributors. We all have our own lives +going on and may need significant time to get to things. + +7. Truthfulness (Sacca) + +Communicate honestly and openly. Do not embellish facts to get your way. Make +sure to let maintainers know about any issues along the way and keep ample +communication channels open. + +8. Determination (Adhiṭṭhāna) + +Stay focused on long-term objectives and cultivate attainment to that +achievement by utilizing to the fullest extent possible the tools available to +you for managing the workload. + +9. Loving-Kindness (Mettā) + +Treat everyone with respect, even if they treat you poorly. This does not mean +you have to put up with abuse, but make sure to respond with kindness and with +love in your heart. Support and uplift maintainers and other contributors with +your words and actions. + +Do not use angry or hateful language toward contributors, such as demeaning +phrases and slurs. Make sure that if you do not know the pronouns of a +contributor to ask for them and, in the meantime, use gender-neutral they/them +or equivalent pronouns. + +10. Equanimity (Upekkhā) + +Keep a balanced perspective on all suggestions and contributions and make +judgements not from a place of ego and personal preference but on their +usefulness and suitability to the project. Make sure to keep an eye on the +bigger picture as implementing individual features may seem intuitive at first +but scale poorly in practical use. Keep a level head about your own work: it is +not shameful to make a mistake in this vein, and fixing it usually leads to +more insight. + +[0] +[1] diff --git a/CONTRIBUTING b/CONTRIBUTING index d7fdc8e..267918b 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -1,3 +1,5 @@ +Make sure to read our code of conduct in the CONDUCT file. + When contributing a pull request to the main branch, please sign your commits with a PGP key and add your name and the year to the bottom of the list of copyright holders for the file. For example, an existing copyright header might @@ -97,9 +99,10 @@ their editor or terminal. For usage text and help messages, do not implement a -h option. Instead, print usage information when any erroneous option is specified. Follow the NetBSD -style guide for the usage text’s output format [1]. +style guide for the usage text’s output format [0]. -[1] +If committing a new utility, please include tests and documentation (see +tests/ and docs/) for the new tool. If committing a new source file, format the commit message following these guidelines: @@ -128,6 +131,7 @@ $ git commit -m 'tool(1): fix #42 & add feature x' Commit messages should be written in the present tense. +[0] -- This work © 2023–2024 by Emma Tebibyte is licensed under CC BY-SA 4.0. To view a copy of this license, visit diff --git a/Makefile b/Makefile index dd318e3..d547b3b 100644 --- a/Makefile +++ b/Makefile @@ -8,70 +8,100 @@ # permitted in any medium without royalty provided the copyright notice and this # notice are preserved. This file is offered as-is, without any warranty. -.POSIX: +# The octal escape \043 is utilized twice in this file as make(1p) will +# interpret a hash in a rule as an inline comment. -# if using BSD make(1), remove these pragmas because they break it -.PRAGMA: posix_202x # future POSIX standard support à la pdpmake(1) -.PRAGMA: command_comment # breaks without this? +.POSIX: DESTDIR ?= dist PREFIX ?= /usr/local -SYSEXITS != printf '\043include \n' | cpp -M - | sed 's/ /\n/g' \ +# for conditionally compiling OS features +OS != uname +OS_INCLUDE != test -e include/$(OS).mk && printf 'include/$(OS).mk\n' \ + || include/None.mk + +# normalized prefix +PREFIX_N != dirname $(PREFIX)/. +MANDIR != test $(PREFIX_N) = / && printf '/usr/share/man\n' \ + || printf '/share/man\n' +SYSEXITS != printf '\043include \n' | cpp -M - | tr ' ' '\n' \ | sed -n 's/sysexits\.h//p' || printf 'include\n' CC ?= cc RUSTC ?= rustc -RUSTLIBS = --extern getopt=build/o/libgetopt.rlib \ - --extern sysexits=build/o/libsysexits.rlib \ - --extern strerror=build/o/libstrerror.rlib +RUSTFLAGS += --extern getopt=build/o/libgetopt.rlib \ + --extern strerror=build/o/libstrerror.rlib \ + --extern sysexits=build/o/libsysexits.rlib CFLAGS += -I$(SYSEXITS) +# testing requires the absolute path to the bin directory set +BIN = build/bin + +.PHONY: default +default: all test + .PHONY: all all: dj false fop hru intcmp mm npc rpn scrut strcmp stris swab true +# keep build/include until bindgen(1) has stdin support +# https://github.com/rust-lang/rust-bindgen/issues/2703 build: - # keep build/include until bindgen(1) has stdin support - # https://github.com/rust-lang/rust-bindgen/issues/2703 - mkdir -p build/bin build/include build/lib build/o build/test + mkdir -p build/bin build/docs build/include build/lib build/o build/test .PHONY: clean clean: rm -rf build dist -dist: all - mkdir -p $(DESTDIR)/$(PREFIX)/bin $(DESTDIR)/$(PREFIX)/share/man/man1 +dist: all docs + mkdir -p $(DESTDIR)/$(PREFIX)/bin $(DESTDIR)/$(PREFIX)/$(MANDIR)/man1 cp build/bin/* $(DESTDIR)/$(PREFIX)/bin - cp docs/*.1 $(DESTDIR)/$(PREFIX)/share/man/man1 + cp build/docs/*.1 $(DESTDIR)/$(PREFIX)/$(MANDIR)/man1 .PHONY: install install: dist cp -r $(DESTDIR)/* / +include tests/tests.mk + .PHONY: test -test: build - tests/posix-compat.sh - $(RUSTC) --test src/getopt-rs/lib.rs -o build/test/getopt +test: all $(TESTS) /tmp/getopt + @echo $(TESTS) + /tmp/getopt + +/tmp/getopt: src/libgetopt.rs + $(RUSTC) --test -o /tmp/getopt src/libgetopt.rs + +.PHONY: docs +docs: docs/ build + for file in docs/*; do original="$$(sed -n '/^\.TH/p' <"$$file")"; \ + title="$$(printf '%s\n' "$$original" | sed \ + "s/X\.X\.X/$$(git describe --tags --long | cut -d'-' -f1)/g")"; \ + sed "s/$$original/$$title/g" <"$$file" >"build/$$file"; done + +# include OS feature libraries for compilation +include $(OS_INCLUDE) .PHONY: rustlibs -rustlibs: build/o/libsysexits.rlib build/o/libgetopt.rlib \ - build/o/libstrerror.rlib +rustlibs: build/o/libgetopt.rlib build/o/libstrerror.rlib \ + build/o/libsysexits.rlib $(OSLIB) -build/o/libgetopt.rlib: build src/getopt-rs/lib.rs +build/o/libgetopt.rlib: build src/libgetopt.rs $(RUSTC) $(RUSTFLAGS) --crate-type=lib --crate-name=getopt \ - -o $@ src/getopt-rs/lib.rs + -o $@ src/libgetopt.rs -build/o/libstrerror.rlib: build src/strerror.rs +build/o/libstrerror.rlib: build src/libstrerror.rs $(RUSTC) $(RUSTFLAGS) --crate-type=lib -o $@ \ - src/strerror.rs + src/libstrerror.rs -build/o/libsysexits.rlib: build $(SYSEXITS)sysexits.h - # bandage solution until bindgen(1) gets stdin support - printf '#define EXIT_FAILURE 1\n' | cat - $(SYSEXITS)sysexits.h \ - > build/include/sysexits.h +build/o/libsysexits.rlib: build/include/sysexits.h bindgen --default-macro-constant-type signed --use-core --formatter=none \ build/include/sysexits.h | $(RUSTC) $(RUSTFLAGS) --crate-type lib -o $@ - +# bandage solution until bindgen(1) gets stdin support +build/include/sysexits.h: build $(SYSEXITS)sysexits.h + printf '\043define EXIT_FAILURE 1\n' | cat - $(SYSEXITS)sysexits.h > $@ + .PHONY: dj dj: build/bin/dj build/bin/dj: src/dj.c build @@ -85,33 +115,32 @@ build/bin/false: src/false.c build .PHONY: fop fop: build/bin/fop build/bin/fop: src/fop.rs build rustlibs - $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/fop.rs + $(RUSTC) $(RUSTFLAGS) -o $@ src/fop.rs .PHONY: hru hru: build/bin/hru build/bin/hru: src/hru.rs build rustlibs - $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/hru.rs + $(RUSTC) $(RUSTFLAGS) -o $@ src/hru.rs .PHONY: intcmp intcmp: build/bin/intcmp -build/bin/intcmp: src/intcmp.c build - $(CC) $(CFLAGS) -o $@ src/intcmp.c +build/bin/intcmp: src/intcmp.rs build rustlibs + $(RUSTC) $(RUSTFLAGS) -o $@ src/intcmp.rs .PHONY: mm mm: build/bin/mm -build/bin/mm: src/mm.c build - $(CC) $(CFLAGS) -o $@ src/mm.c - +build/bin/mm: src/mm.rs build rustlibs + $(RUSTC) $(RUSTFLAGS) -o $@ src/mm.rs .PHONY: npc npc: build/bin/npc build/bin/npc: src/npc.c build - $(CC) $(CFLAGAS) -o $@ src/npc.c + $(CC) $(CFLAGS) -o $@ src/npc.c .PHONY: rpn rpn: build/bin/rpn build/bin/rpn: src/rpn.rs build rustlibs - $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/rpn.rs + $(RUSTC) $(RUSTFLAGS) -o $@ src/rpn.rs .PHONY: scrut scrut: build/bin/scrut @@ -120,11 +149,8 @@ build/bin/scrut: src/scrut.c build .PHONY: stris stris: build/bin/stris -build/bin/stris: src/stris.rs build build/o/libgetopt.rlib \ - build/o/libsysexits.rlib - $(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \ - --extern sysexits=build/o/libsysexits.rlib \ - -o $@ src/stris.rs +build/bin/stris: src/stris.rs build rustlibs + $(RUSTC) $(RUSTFLAGS) -o $@ src/stris.rs .PHONY: strcmp strcmp: build/bin/strcmp @@ -133,11 +159,8 @@ build/bin/strcmp: src/strcmp.c build .PHONY: swab swab: build/bin/swab -build/bin/swab: src/swab.rs build build/o/libgetopt.rlib \ - build/o/libsysexits.rlib - $(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \ - --extern sysexits=build/o/libsysexits.rlib \ - -o $@ src/swab.rs +build/bin/swab: src/swab.rs build rustlibs + $(RUSTC) $(RUSTFLAGS) -o $@ src/swab.rs .PHONY: true true: build/bin/true diff --git a/README b/README index accfc7e..fdb05fb 100644 --- a/README +++ b/README @@ -1,28 +1,29 @@ “Seek not to walk the path of the masters; seek what they sought.” – Matsuo Basho -The Bonsai core utilities are the result of the careful examination of the -current state of POSIX and Unix utilies. The Unix Philosophy, “do one thing and -do it well” is its core but these tools do not cling to the names of the past. +The Bonsai harakit utilities are a replacement for standard POSIX utilities +which aim to fill its niche while expanding on their capabilities. These new +tools are the result of the careful examination of the current state of POSIX +and Unix utilies. The Unix Philosophy of “do one thing and do it well” are their +core but they avoid clinging to the past. The era of the original Unix tools has been long and fruitful, but they have -their flaws. The new, non-POSIX era of this project started with frustration -with the way certain tools work and how other projects that extend POSIX don’t -make anything better. +their flaws. This project originated from frustrations with the way certain +tools work and how other projects that extend POSIX don’t make anything better. This project will not follow in the footsteps of GNU; extensions of POSIX will not be found here. GNU extensions are a gateway to the misuse of the shell. The -Bonsai core utilities will intentionally discourage use of the shell for -purposes beyond its scope. +harakit utilities will intentionally discourage use of the shell for purposes +beyond its scope. See docs/ for more on the specific utilities currently implemented. Building -The coreutils require a POSIX-compliant environment to compile, including a C -compiler and preprocessor (cc(1) and cpp(1) by default) with the -idirafter -flag, a Rust compiler (rustc(1) by default), bindgen(1), and a POSIX-compliant -make(1) utility. +Harakit utilities require a POSIX-compliant environment to compile, including a +C compiler and preprocessor (cc(1) and cpp(1) by default), an edition 2023 Rust +compiler (rustc(1) by default), bindgen(1), and a POSIX-compliant make(1) +utility. To build and install: @@ -38,7 +39,7 @@ To test the utilities: $ make test -To remove all untracked files: +To remove all build and distributable files: $ make clean diff --git a/STYLE b/STYLE new file mode 100644 index 0000000..5322661 --- /dev/null +++ b/STYLE @@ -0,0 +1,124 @@ +The following guidelines are conducive to clear and readable code that is +consistent with the style of the rest of the Bonsai Computer System. + +0. Braces are mandatory for all control flow. + +1. Nested indentation should be kept to a minimum. + +2. Empty lines should be placed between different kinds of statements: + + int t; + + assert(io->bufuse > 0); + assert(io->bufuse <= io->bs); + + if ((t = write(io->fd, io->buf, io->bufuse)) < 0) { + io->error = errno; + t = 0; + } else if (t > 0) { + memmove(io->buf, &(io->buf)[t], (io->bufuse -= t)); + } + + io->bytes += t; + io->prec += (t > 0 && io->bufuse > 0); + io->rec += (t > 0 && io->bufuse == 0); + + return io; + +3. Each block of code should be indented once more than the keyword which +initiated the block: + + switch (c) { + case 'e': mode |= EQUAL; break; + case 'g': mode |= GREATER; break; + case 'l': mode |= LESS; break; + default: return usage(s); + } + +4. In C, spaces should be placed in control flow statements after the keyword +and before the opening brace: + + for (i = 2; i < argc; ++i) { + +5. If a function, a C control flow statement, or a Rust macro has arguments that +cause the statement to be broken into multiple lines, this should be done by +placing the arguments on a new line inside the parentheses: + + let usage = format!( + "Usage: {} [-d delimiter] index command [args...]", + argv[0], + ); + +6. If Rust function arguments or fields are on their own lines, they should +always have a trailing comma: + + return Err(EvaluationError { + message: format!("{}: Invalid token", i), + code: EX_DATAERR, + }) + +7. If text is on the same line as a brace, spaces should be placed after an +opening curly brace and before a closing one: + + use sysexits::{ EX_DATAERR, EX_IOERR, EX_UNAVAILABLE, EX_USAGE }; + +8. If a control flow statement is short enough to be easily understood in a +glance, it may be placed on a single line: + + if !(argc < 0) { usage(program_name); } + +9. In C, note everything you use from a library in a comment subsequent to its +#include statement: + + #include /* close(2), getopt(3), lseek(2), read(2), write(2), + * optarg, optind, STDIN_FILENO, STDOUT_FILENO */ + +10. In Rust, place extern statements after use statements that include standard +library crates. Group alike statements: + + use std::fs::Path; + + extern crate strerror; + extern crate sysexits; + + use strerror::StrError; + use sysexits::{ EX_OSERR, EX_USAGE }; + +11. Do not use do while loops in C. + +12. Adhere to the following rules from the paper The Power of 10: Rules for +Developing Safety-Critical Code [0]: + 1. Avoid complex flow constructs, such as goto and recursion. + 2. All loops must have fixed bounds. This prevents runaway code. + 3. Avoid heap memory allocation. + 4. Restrict functions to the length of a single printed page. + + 6. Restrict the scope of data to the smallest possible. + 7. Check the return value of all non-void functions, or cast to void to + indicate the return value is useless (such as in the case of using + fprintf(3p) to print to the standard error). + 8. Use the preprocessor sparingly. + 9. Limit pointer use to a single dereference, and do not use function + pointers. + 10. Compile with all possible warnings active; all warnings should then be + addressed before release of the software (for C compilers, compile with + -Wpedantic). + +13. Remember this quote from The Elements of Programming Style by Brian +Kernighan: + Everyone knows that debugging is twice as hard as writing a program in the + first place. So if you're as clever as you can be when you write it, how + will you ever debug it? + + +References +========== + +[0] + +-- +Copyright © 2024 Emma Tebibyte +Copyright © Wikipedia contributors + +This work is licensed under CC BY-SA 4.0. To view a copy of this license, visit +. diff --git a/docs/dj.1 b/docs/dj.1 index 1e286f5..79096d5 100644 --- a/docs/dj.1 +++ b/docs/dj.1 @@ -1,160 +1,226 @@ .\" Copyright (c) 2024 DTB +.\" Copyright (c) 2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH dj 1 - +.\" +.TH DJ 1 2024-07-14 "Harakit X.X.X" .SH NAME - dj \(en disk jockey - +.\" .SH SYNOPSIS dj -.RB ( -AdHnq ) -.RB ( -a -.RB [ byte ]) -.RB ( -c -.RB [ count ]) +.RB [ -Hn ] +.RB [ -a\ byte ] +.RB [ -c\ count ] -.RB ( -i -.R [ -.B input file -.R ]) -.RB ( -b -.R [ -.B input block size -.R ]) -.RB ( -s -.R [ -.B input offset -.R ]) +.RB [ -i\ file ] +.RB [ -b\ block_size ] +.RB [ -s\ offset ] -.RB ( -o -.R [ -.B output file -.R ]) -.RB ( -B -.R [ -.B output block size -.R ]) -.RB ( -S -.R [ -.B output offset -.R ]) +.RB [ -o\ file ] +.RB [ -B\ block_size ] +.RB [ -S\ offset ] +.\" +.SH DESCRIPTION -.SH USAGE +Perform precise read and write operations on files. This utility is useful for +reading and writing binary data to and from disks. -The -.B -i -option takes a path as an argument to open and use in place of standard input. -The -.B -o -option does the same in place of standard output. Dj does not truncate output -files and instead writes over the bytes in the existing file. -.PP -The -.B -b -option takes a numeric argument as the size in bytes of the input buffer and -the -.B -B -option does the same for the output buffer, the default for both being 1024 -bytes, or one kibibyte (KiB). -.PP -The -.B -s -option takes a numeric argument as the number of bytes to skip into the input -before starting to read, and the -.B -S -option skips a number of bytes through the output before starting to write from -the input. If the input is a stream the bytes are read and discarded. If the -output is a stream, nul characters are printed. -.PP -The -.B -a -option takes one argument of one byte in length and pads the input buffer with -that byte in the event that a read doesn't fill the input buffer, and the -.B -A -option takes no arguments and pads with nuls. -The -.B -c -option specifies an amount of reads to make, and if 0 (the default) dj will -continue reading until a partial or empty read. -.PP -On a partial or empty read, dj prints a diagnostic message (unless the -.B -q -option is specified) and exits (unless the -.B -n -option is specified, in which case only two consecutive empty reads will cause -dj to exit). -At exit, usage statistics are printed unless the option -.B -q -is specified a second time. The -.B -H -option will make these diagnostics human-readable. +This manual page uses the terms \(lqskip\(rq and \(lqseek\(rq to refer to moving +to a specified byte by index in the input and output of the program +respectively. This language is inherited from the +.BR dd (1p) +utility and used here to decrease ambiguity. +The offset used when skipping or seeking refers to how many bytes are skipped +or sought. Running +.BR dj (1) +with a skip offset of 1 skips one byte into the input and reads from the second +byte onwards. A programmer may think of a file as a zero-indexed array of +bytes; in this analogy, the offset given is the index of the byte at which to +start reading or writing. +.\" +.SH OPTIONS + +.IP \fB-i\fP\ \fIfile\fP +Takes a file path as an argument and opens it for use as an input. +.IP \fB-b\fP\ \fIblock_size\fP +Takes a numeric argument as the size in bytes of the input buffer, the default +being 1024. +.IP \fB-s\fP +Takes a numeric argument as the index of the byte at which reading will +commence; \(lqskips\(rq that number of bytes. If the standard input is used, +bytes read to this point are discarded. +.IP \fB-o\fP +Takes a file path as an argument and opens it for use as an output. +.IP \fB-B\fP\ \fIblock_size\fP +Takes a numeric argument as the size in bytes of the output buffer, the default +being 1024. Note that this option only affects the size of output writes and not +the amount of output data itself. See the CAVEATS section. +.IP \fB-S\fP +Takes a numeric argument as the index of the byte at which writing will +commence; \(lqseeks\(rq that number of bytes. If the standard output is used, +null characters are printed. +.IP \fB-a\fP +Accepts a single literal byte with which the input buffer is padded in the event +of an incomplete read from the input file. If the option argument is empty, the +null byte is used. +.IP \fB-c\fP +Specifies a number of blocks to read. The default is 0, in which case the input +is read until a partial or empty read is made. +.IP \fB-H\fP +Prints diagnostic messages in a human-readable manner as described in the +DIAGNOSTICS section. +.IP \fB-n\fP +Retries failed reads once before exiting. +.\" +.SH STANDARD INPUT + +The standard input shall be used as an input if no inputs are specified or if +input file is \(lq-\(rq. +.\" +.SH STANDARD OUTPUT +The standard output shall be used as an output if no inputs are specified or if +the output file is \(lq-\(rq. +.\" +.SH EXAMPLES + +The following +.BR sh (1p) +line: + +.RS +printf 'Hello, world!\(rsn' | dj -c 1 -b 7 -s 7 2>/dev/null +.RE + +Produces the following output: + +.RS +world! +.RE + +The following +.BR sh (1p) +lines run sequentially: + +.RS +tr '\(rs0' 0 ' {records written} -.R '+' {partial records written} ';' {bytes read} '>' {bytes written} -.R {ASCII line feed} -.PP -The -.B -q -option suppresses error messages which print when a read or write is partial or -empty and when used twice suppresses diagnostic output entirely. -.PP -In non-recoverable errors that don't pertain to dj's read-write cycle, a -diagnostic message is printed and dj exits with the appropriate sysexits(3) -status. +On a partial or empty read, a diagnostic message is printed. Then, the program +exits unless the +.B -n +option is specified. +By default, statistics are printed for input and output to the standard error in +the following format: + +.RS +{records read} {ASCII unit separator} {partial records read} +{ASCII record separator} {records written} {ASCII unit separator} +{partial records written} {ASCII group separator} {bytes read} +{ASCII record separator} {bytes written} {ASCII file separator} +.RE + +This format for diagnostic output is designed to be machine-parseable for +convenience. For a more human-readable format, the +.B -H +option may be specified. In this event, the following format is used instead: + +.RS +{records read} '+' {partial records read} '>' {records written} +'+' {partial records written} ';' {bytes read} '>' {bytes written} +{ASCII line feed} +.RE + +In non-recoverable errors that don\(cqt pertain to the read-write cycle, a +diagnostic message is printed and the program exits with the appropriate +.BR sysexits.h (3) +status. +.\" .SH BUGS If .B -n -is specified along with a specified count, actual byte output may be lower than -expected (the product of the count multiplied by the input block size). If the +is specified along with the +.B -c +option and a count, actual byte output is the product of the count and the input +block size and therefore may be lower than expected. If the .B -a -or -.B -A -options are used this could make data written nonsensical. -.PP -Many lowercase options have capitalized variants and vice-versa which can be -confusing. Capitalized options tend to affect output or are more intense -versions of lowercase options. +option is specified, this could make written data nonsensical. +.\" +.SH CAVEATS +Existing files are not truncated on ouput and are instead overwritten. + +Option variants that have lowercase and uppercase forms could be confused for +each other. The former affects input and the latter affects output. + +The +.B -B +option could be mistaken for the count in bytes of data written to the output. +This conception is intuitive but incorrect, as the +.B -c +option controls the number of blocks to read and the +.B -b +option sets the size of the blocks. The +.B -B +option is similar to the latter but sets the size of blocks to be written, +regardless of the amount of data that will actually be written. In practice, +this means the input buffer should be very large to make use of modern hardware +input and output speeds. + +The skipped or sought bytes while processing irregular files, such as streams, +are reported in the diagnostic output, because they were actually read or +written. This is as opposed to bytes skipped while processing regular files, +which are not reported. +.\" .SH RATIONALE -Dj was modeled after the dd utility specified in POSIX but adds additional -features: typical option formatting, allowing seeks to be specified in bytes -rather than in blocks, allowing arbitrary bytes as padding, and printing in a -format that's easy to parse for machines. It also neglects character -conversion, which may be dd's original intent but is irrelevant to its modern -use. - +This program was based on the +.BR dd (1p) +utility as specified in POSIX. While character conversion may have been the +original intent of +.BR dd (1p), +it is irrelevant to its modern use. Because of this, this program eschews +character conversion and adds typical option formatting, allowing seeks to be +specified in bytes rather than in blocks, allowing arbitrary bytes as padding, +and printing in a format that\(cqs easy for machines to parse. +.\" .SH COPYRIGHT -Copyright (C) 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later +Copyright \(co 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -dd(1) +.BR dd (1p) +.BR lseek (3p) +.BR mm (1) diff --git a/docs/false.1 b/docs/false.1 index 6883802..b940909 100644 --- a/docs/false.1 +++ b/docs/false.1 @@ -1,35 +1,35 @@ .\" Copyright (c) 2022, 2024 DTB -.\" Copyright (c) 2023 Emma Tebibyte +.\" Copyright (c) 2023–2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH FALSE 1 - +.\" +.TH FALSE 1 2024-06-06 "Harakit X.X.X" .SH NAME - false \(en do nothing, unsuccessfully - +.\" .SH DESCRIPTION -False does nothing regardless of operands or standard input. -False will always return an exit code of 1. - +Do nothing regardless of operands or standard input. An exit code of 1 will +always be returned. +.\" .SH RATIONALE -False exists for the construction of control flow and loops based on a failure. - -False functions as described in POSIX.1-2017. - +In POSIX.1-2017, +.BR false (1p) +exists for the construction of control flow and loops based on a failure. This +implementation functions as described in that standard. +.\" .SH AUTHOR -Written by Emma Tebibyte . - +Written by Emma Tebibyte +.MT emma@tebibyte.media +.ME . +.\" .SH COPYRIGHT This work is marked with CC0 1.0. To see a copy of this license, visit . - +.\" .SH SEE ALSO - -true(1p) +.BR true (1p) diff --git a/docs/fop.1 b/docs/fop.1 new file mode 100644 index 0000000..b96033a --- /dev/null +++ b/docs/fop.1 @@ -0,0 +1,62 @@ +.\" Copyright (c) 2024 DTB +.\" Copyright (c) 2024 Emma Tebibyte +.\" +.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, +.\" visit . +.\" +.TH FOP 1 2024-06-17 "Harakit X.X.X" +.SH NAME +fop \(en field operator +.\" +.SH SYNOPSIS + +fop +.RB ( -d ) +.RB [ delimiter ] +.RB index +.RB program... +.\" +.SH DESCRIPTION + +Performs operations on specified fields in data read from the standard input. +.\" +.SH OPTIONS + +.IP \fB-d\fP\ \fIdelimiter\fP +Sets a delimiter by which the input data will be split into fields. The default +is an ASCII record separator. +.\" +.SH CAVEATS + +Field indices are zero-indexed, which may be unexpected behavior for some users. +.\" +.SH RATIONALE + +With the assumption that tools will output data separated with ASCII field +separators, there is a need for the ability to modify select fields in this data +easily and quickly. + +The idea for this utility originated in the fact that the GNU +.BR ls (1) +utility contains a +.B -h +option which enables human-readable units in file size outputs. This +functionality was broken out into +.BR hru (1), +but there was no easy way to modify the field in the ouput of +.BR ls (1p) +without creating a new tool. +.\" +.SH AUTHOR + +Written by Emma Tebibyte +.MT emma@tebibyte.media +.ME . +.\" +.SH COPYRIGHT + +Copyright \(co 2024 Emma Tebibyte. License AGPLv3+: GNU AGPL version 3 or later +. +.\" +.SH SEE ALSO +.BR sed (1p) diff --git a/docs/hru.1 b/docs/hru.1 index c75cb73..6929d51 100644 --- a/docs/hru.1 +++ b/docs/hru.1 @@ -2,56 +2,68 @@ .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH rpn 1 - +.\" +.TH HRU 1 2024-06-17 "Harakit X.X.X" .SH NAME - hru \(en human readable units - +.\" .SH SYNOPSIS hru - +.\" .SH DESCRIPTION -Hru reads byte counts in the form of whole numbers from the standard input and -writes to the standard output the same number converted one of the units of data -defined by the International System of Units. +Convert counts to higher units. + +Byte counts will be read in the form of whole numbers from the standard input +and be written to the standard output the same number converted to a higher unit +of data as defined by the \fIInternational System of Units\fP. The program will convert the byte count to the highest unit possible where the value is greater than one. - +.\" .SH DIAGNOSTICS -If encountering non-integer characters in the standard input, hru will exit with -the appropriate error code as defined by sysexits.h(3) and print an error -message. - +If encountering non-integer characters in the standard input, the program will +exit with the appropriate error code as defined by +.BR sysexits.h (3) +and print an error message. +.\" .SH RATIONALE -The GNU project’s ls(1) implementation contains a human-readable option (-h) -that, when specified, makes the tool print size information in a format more -immediately readable. This functionality is useful not only in the context of -ls(1) so the decision was made to split it into a new tool. The original -functionality in GNU’s ls(1) can be emulated with fop(1) combined with this -program. - +The GNU project\(cqs +.BR ls (1) +implementation contains a human-readable option (\fB-h\fP) that, when specified, +makes the tool print size information in a format more immediately +readable. This functionality is useful not only in this context, so the decision +was made to split it into a new tool. The original functionality from GNU\(cqs +.BR ls (1) +can be emulated with +.BR fop (1) +combined with this program. +.\" .SH STANDARDS -Hru follows the standard unit prefixes as specified by the Bureau International -des Poids et Mesures (BIPM) in the ninth edition of The International System of -Units (SI). - +The standard unit prefixes as specified by the +.I Bureau International des Poids et Mesures +.RI ( BIPM ) +in the ninth edition of +.I The International System of Units +.RI ( SI ) +are utilized for the ouput of conversions. +.\" .SH AUTHOR -Written by Emma Tebibyte . - +Written by Emma Tebibyte +.MT emma@tebibyte.media +.ME . +.\" .SH COPYRIGHT -Copyright (c) 2024 Emma Tebibyte. License AGPLv3+: GNU AGPL version 3 or later +Copyright \(co 2024 Emma Tebibyte. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -GNU ls(1), The International System of Units (SI) 9th Edition +GNU +.BR ls (1), +.I The International System of Units (SI) 9th Edition diff --git a/docs/intcmp.1 b/docs/intcmp.1 index f370755..b90f23a 100644 --- a/docs/intcmp.1 +++ b/docs/intcmp.1 @@ -1,78 +1,103 @@ .\" Copyright (c) 2023–2024 DTB -.\" Copyright (c) 2023 Emma Tebibyte +.\" Copyright (c) 2023–2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH intcmp 1 - +.\" +.TH INTCMP 1 2024-06-06 "Harakit X.X.X" .SH NAME - intcmp \(en compare integers - +.\" .SH SYNOPSIS intcmp -.RB ( -eghl ) -.RB [ integer ] -.RB [ integer... ] - +.RB [ -egl ]\ integer\ integer... .SH DESCRIPTION +Compare integers to each other. +.\" +.SH OPTIONS -Intcmp compares integers. - -.SH USAGE - -The -e option permits given integers to be equal to each other. If combined -with -g or -l, only adjacent integers in the argument sequence can be equal. -.PP -The -g option permits a given integer to be greater than the following integer. -.PP -The -l option permits a given integer to be less than the following integer. -.PP -It may help to think of the -e, -g, and -l options as equivalent to the -infix algebraic “=”, “>”, and “<” operators respectively, with each option -putting its symbol between every given integer. For example, -.R intcmp -l 1 2 3 -is equivalent to evaluating "1 < 2 < 3". +.IP \fB-e\fP +Permits given integers to be equal to each other. +.IP \fB-g\fP +Permits a given integer to be greater than the following integer. +.IP \fB-l\fP +Permits a given integer to be less than the following integer. +.\" +.SH EXAMPLES +It may help to think of the +.BR -e , +.BR -g , +and +.B -l +options as equivalent to the infix algebraic \(lq=\(rq, \(lq>\(rq, and \(lq<\(rq +operators respectively, with each option putting its symbol between every given +integer. The following example is equivalent to evaluating \(lq1 < 2 < 3\(rq: +\" +.RS +intcmp -l 1 2 3 +.RE +.\" .SH DIAGNOSTICS -Intcmp exits 0 for a valid expression and 1 for an invalid expression. -.PP -Intcmp prints a debug message and exits with the appropriate sysexits(3) error -code in the event of an error. +The program will exit with a successfully for a valid expression and with an +error code of 1 for an invalid expression. +In the event of an error, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code. +.\" .SH BUGS -There are multiple ways to express compound comparisons; “less than or equal -to” can be -le or -el, for example. -.PP -The inequality comparison is -gl or -lg for “less than or greater than”; this -is elegant but unintuitive. -.PP --egl, "equal to or less than or greater than", exits 0 no matter what for valid -program usage and may be abused to function as an integer validator. -Use str(1) instead. +.BR -egl , +\(lqequal to or less than or greater than\(rq, always exits successfully for +valid program usage and may be abused to function as an integer validator. Use +.BR str (1) +instead. +.\" +.SH CAVEATS +There are multiple ways to express compound comparisons; \(lqless than or equal +to\(rq can be +.B -le +or +.BR -el , +for example. + +The inequality comparison is +.B -gl +.B or +.B -lg +for \(lqless than or greater than\(rq; +this is elegant but unintuitive. +.\" .SH RATIONALE The traditional tool for integer comparisons in POSIX and other Unix shells has -been test(1). This tool also handles string comparisons and file scrutiny. -These parts of its functionality have been broken out into multiple utilities. - -Strcmp’s functionality may be performed on a POSIX-compliant system with -test(1p). +been +.BR test (1). +This tool also handles string comparisons and file scrutiny. These parts of its +functionality have been broken out into multiple utilities. +This program\(cqs functionality may be performed on a POSIX-compliant system +with +.BR test (1p). +.\" .SH AUTHOR -Written by DTB . - +Written by DTB +.MT trinity@trinity.moe +.ME . +.\" .SH COPYRIGHT - -Copyright © 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later +.\" +Copyright \(co 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -strcmp(1), scrut(1), str(1), test(1p) +.BR scrut (1), +.BR strcmp (1), +.BR str (1), +.BR test (1p) diff --git a/docs/mm.1 b/docs/mm.1 index 2244588..f641156 100644 --- a/docs/mm.1 +++ b/docs/mm.1 @@ -2,75 +2,72 @@ .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH mm 1 - +.\" +.TH MM 1 2024-07-14 "Harakit X.X.X" .SH NAME - mm \(en middleman - +.\" .SH SYNOPSIS mm -.RB ( -aenu ) -.RB ( -i -.RB [ input ]) -.RB ( -o -.RB [ output ]) - +.RB [ -aetu ] +.RB [ -i\ input ] +.RB [ -o\ output ] +.\" .SH DESCRIPTION -Mm catenates input files and writes them to the start of each output file. - +Catenate input files and write them to the start of each output file or stream. +.\" .SH OPTIONS -Mm, upon receiving the -.B -a -option, will open subsequent outputs for appending rather than updating. -.PP -The -.B -i -option opens a path as an input. Without any inputs specified mm will use -standard input. Standard input itself can be specified by giving the path '-'. -.PP -The -.B -o -option opens a path as an output. Without any outputs specified mm will use -standard output. Standard output itself can be specified by giving the -path '-'. Standard error itself can be specified with the +.IP \fB-a\fP +Opens outputs for appending rather than updating. +.IP \fB-e\fP +Use the standard error as an output. +.IP \fB-t\fP +Causes outputs to be overwritten instead of being truncated. +.IP \fB-u\fP +Ensures neither input or output will be buffered. +.IP \fB-i\fP\ \fIinput\fP +Opens a path as an input. If one or more of the input files is \(lq-\(rq or if +no inputs are specified, the standard input shall be used. If specified as the +last option and if there are trailing arguments to the program, they shall be +appended to the list of files to use as inputs. +.IP \fB-o\fP\ \fIoutput\fP +Opens a path as an output. If one or more of the output files is \(lq-\(rq or if +no outputs are specified and the .B -e -option. -.PP -The -.B -u -option ensures neither input or output will be buffered. -.PP -The -.B -n -option tells mm to ignore SIGINT signals. - +option is not specified, the standard output shall be used. If specified as the +last option and if there are trailing arguments to the program, they shall be +appended to the list of files to use as outputs. +.\" .SH DIAGNOSTICS -If an output can no longer be written mm prints a diagnostic message, ceases -writing to that particular output, and if there are more outputs specified, -continues, eventually exiting unsuccessfully. -.PP -On error mm prints a diagnostic message and exits with the appropriate -sysexits.h(3) status. - -.SH BUGS - -Mm does not truncate existing files, which may lead to unexpected results. +If an output cannot be written to, an error occurs; however, exiting will be +deferred until writing to any other specified outputs completes. +When an error is encountered, a diagnostic message is printed and the program +exits with the appropriate +.BR sysexits.h (3) +status. +.\" .SH RATIONALE -Mm was modeled after the cat and tee utilities specified in POSIX. - +The +.BR cat (1p) +and +.BR tee (1p) +programs specified in POSIX together provide similar functionality. The +separation of the two sets of functionality into separate APIs seemed +unncessary. +.\" .SH COPYRIGHT -Copyright (c) 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later +Copyright \(co 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -cat(1p), dd(1), dj(1), tee(1p) +.BR cat (1p), +.BR dd (1), +.BR dj (1), +.BR tee (1p) diff --git a/docs/npc.1 b/docs/npc.1 index 7f66c3d..5acee9e 100644 --- a/docs/npc.1 +++ b/docs/npc.1 @@ -1,68 +1,74 @@ .\" Copyright (c) 2023–2024 DTB -.\" Copyright (c) 2023 Emma Tebibyte +.\" Copyright (c) 2023–2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH npc 1 - +.\" +.TH NPC 1 2024-06-17 "Harakit X.X.X" .SH NAME - npc \(en show non-printing characters - +.\" .SH SYNOPSIS npc -.RB ( -eht ) - +.RB [ -et ] +.\" .SH DESCRIPTION -Npc reads from standard input and writes to standard output, replacing non- -printing characters with printable equivalents. Control characters print as a -carat ('^') followed by the character '@' through '_' corresponding to the -character replaced (e.g. control-X becomes "^X"). The delete character (0x7F) -becomes "^?". Characters with the high bit set (>127) are printed as "M-" +Print normally non-printing characters. + +The program reads from standard input and writes to standard output, replacing +non-printing characters with printable equivalents. Control characters print as +a carat ('^') followed by the character '@' through '_' corresponding to the +character replaced (e.g. control-X becomes '^X'). The delete character (0x7F) +becomes '^?'. Characters with the high bit set (>127) are printed as 'M-' followed by the graphical representation for the same character without the high bit set. -.PP -The -.B -e -option prints a currency sign ('$') before each line ending. -.PP -The -.B -t -option prints tab characters as "^I" rather than a literal horizontal tab. +.\" +.SH OPTIONS +.IP \fB-e\fP +Prints a dollar sign ('$') before each newline. +.IP \fB-t\fP +Prints tab characters as '^I' rather than a literal horizontal tab. +.\" .SH DIAGNOSTICS -Npc prints a debug message and exits with the appropriate sysexits(3) error -code in the event of an error, otherwise it exits successfully. - +In the event of an error, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code. +.\" .SH BUGS -Npc operates in single-byte chunks regardless of intended encoding. - +The program operates in single-byte chunks regardless of intended encoding. +.\" .SH RATIONALE POSIX currently lacks a way to display non-printing characters in the terminal -using a standard tool. A popular extension to cat(1p), the -v option, is the -bandage solution GNU and other software suites use. - -This functionality should be a separate tool because its usefulness extends -beyond that of cat(1p). +using a standard tool. A popular extension to +.BR cat (1p), +the +.B -v +option, is the bandage solution GNU and other software suites use. +This functionality is included in a separate tool because its usefulness extends +beyond that of +.BR cat (1p). +.\" .SH AUTHOR -Written by DTB . - +Written by DTB +.MT trinity@trinity.moe +.ME . +.\" .SH COPYRIGHT Copyright © 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -cat(1p), cat-v(1) - +.BR cat (1p), +.BR cat-v (1), .I UNIX Style, or cat -v Considered Harmful by Rob Pike diff --git a/docs/rpn.1 b/docs/rpn.1 index 2197fbe..8c8cd84 100644 --- a/docs/rpn.1 +++ b/docs/rpn.1 @@ -3,68 +3,84 @@ .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH rpn 1 - +.\" +.TH RPN 1 2024-06-17 "Harakit X.X.X" .SH NAME - rpn \(en reverse polish notation evaluation - +.\" .SH SYNOPSIS rpn -.RB [numbers...]\ [operators...] - +.RB [ numbers... ] +.RB [ operators... ] +.\" .SH DESCRIPTION -Rpn evaluates reverse polish notation expressions either read from the standard -input or parsed from provided arguments. See the STANDARD INPUT section. +Evaluate reverse polish notation. -Upon evaluation, rpn will print the resulting number on the stack to the -standard output. Any further specified numbers will be placed at the end of the +The program evaluates reverse polish notation expressions read either from the +standard input or parsed from provided arguments. See the STANDARD INPUT +section. + +Upon evaluation, the resulting number on the stack will be printed to the +standard output. Any further numbers specified will be placed at the end of the stack. -For information on for reverse polish notation syntax, see rpn(7). - +For information on for reverse polish notation syntax, see +.BR rpn (7). +.\" .SH STANDARD INPUT -If arguments are passed to rpn, it interprets them as an expression to be -evaluated. Otherwise, it reads whitespace-delimited numbers and operations from +If arguments are specified, they are interpreted as an expression to be +evaluated. Otherwise, whitespace-delimited numbers and operations are read from the standard input. - +.\" .SH DIAGNOSTICS -If encountering a syntax error, rpn will exit with the appropriate error code -as defined by sysexits.h(3) and print an error message. - +In the event of an error, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code; however, in the event of a syntax error, the program will print an +error message and continue accepting input. +.\" .SH CAVEATS Due to precision constraints and the way floats are represented in accordance -with the IEEE Standard for Floating Point Arithmetic (IEEE 754), floating-point -arithmetic has rounding errors. This is somewhat curbed by using the -machine epsilon as provided by the Rust standard library to which to round -numbers. Because of this, variation is expected in the number of decimal places -rpn can handle based on the platform and hardware of any given machine. - +with the +.I IEEE Standard for Floating Point Arithmetic +(\fIIEEE 754\fP), floating-point arithmetic has rounding errors. This is +somewhat curbed by using the machine epsilon as provided by the Rust standard +library to which numbers are rounded. Because of this, variation is expected in +the number of decimal places the program can handle based on the platform and +hardware of any given machine. +.\" .SH RATIONALE -An infix notation calculation utility, bc(1p), is included in the POSIX -standard, but does not accept expressions as arguments; in scripts, any -predefined, non-interactive input must be piped into the program. A dc(1) -pre-dates the standardized bc(1p), the latter originally being a preprocessor -for the former, and was included in UNIX v2 onward. While it implements reverse -polish notation, it still suffers from being unable to accept an expression as -an argument. - +An infix notation calculation utility, +.BR bc (1p), +is included in the POSIX standard, but does not accept expressions as arguments; +in scripts, any predefined, non-interactive input must be piped into the +program. A +.BR dc (1) +pre-dates the standardized +.BR bc (1p), +the latter originally being a preprocessor for the former, and was included in +Second Edition UNIX and onward. While it implements reverse polish notation, it +still suffers from being unable to accept an expression as an argument. +.\" .SH AUTHOR -Written by Emma Tebibyte . - +Written by Emma Tebibyte +.MT emma@tebibyte.media +.ME . +.\" .SH COPYRIGHT Copyright (c) 2024 Emma Tebibyte. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -bc(1p), dc(1), rpn(7), IEEE 754 +.BR bc (1p), +.BR dc (1), +.BR rpn (7), +.I IEEE 754 diff --git a/docs/scrut.1 b/docs/scrut.1 index 7a5107a..2b95bee 100644 --- a/docs/scrut.1 +++ b/docs/scrut.1 @@ -1,93 +1,86 @@ .\" Copyright (c) 2024 DTB +.\" Copyright (c) 2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH scrut 1 - +.\" +.TH SCRUT 1 2024-06-06 "Harakit X.X.X" .SH NAME - scrut \(en scrutinize file properties - .SH SYNOPSIS scrut -.RB ( -bcdefgkprsuwxLS ) -.RB [ file... ] - +.RB [ -LSbcdefgkprsuwx ] +.B file... +.\" .SH DESCRIPTION -Scrut determines if given files comply with the opted requirements. - +Determine if files comply with requirements. If the given files comply with the +specified requirements, the program will exit successfully. Otherwise, it exits +unsuccessfully. +.\" .SH OPTIONS -.B -b -requires the given files to exist and be block special files. -.PP -.B -c -requires the given files to exist and be character special files. -.PP -.B -d -requires the given files to exist and be directories. -.PP -.B -e -requires the given files to exist, and is redundant to any other option. -.PP -.B -e -requires the given files to exist and be regular files. -.PP -.B -g -requires the given files to exist and have their set group ID flags set. -.PP -.B -k -requires the given files to exist and have their sticky bit set. -.PP -.B -p -requires the given files to exist and be named pipes. -.PP -.B -r -requires the given files to exist and be readable. -.PP -.B -u -requires the given files to exist and have their set user ID flags set. -.PP -.B -w -requires the given files to exist and be writable. -.PP -.B -x -requires the given files to exist and be executable. -.PP -.B -L -requires the given files to exist and be symbolic links. -.PP -.B -S -requires the given files to exist and be sockets. +.IP \fB-L\fB +Requires the given files to exist and be symbolic links. +.IP \fB-S\fP +Requires the given files to exist and be sockets. +.IP \fB-b\fP +Requires the given files to exist and be block special files. +.IP \fB-c\fP +Requires the given files to exist and be character special files. +.IP \fB-d\fP +Requires the given files to exist and be directories. +.IP \fB-e\fP +Requires the given files to exist, and is redundant to any other option. +.IP \fB-f\fP +Requires the given files to exist and be regular files. +.IP \fB-g\fP +Requires the given files to exist and have their set group ID flags set. +.IP \fB-k\fP +Requires the given files to exist and have their sticky bit set. +.IP \fB-p\fP +Requires the given files to exist and be named pipes. +.IP \fB-r\fP +Requires the given files to exist and be readable. +.IP \fB-u\fP +Requires the given files to exist and have their set user ID flags set. +.IP \fB-w\fP +Requires the given files to exist and be writable. +.IP \fB-x\fP +Requires the given files to exist and be executable. +.\" +.SH DIAGNOSTICS -.SH EXIT STATUS +When invoked incorrectly, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code. +.\" +.SH RATIONALE -Scrut prints a debug message and exits unsuccessfully with the appropriate -sysexits.h(3) error code if invoked incorrectly. Scrut exits successfully if -the given files comply with their requirements and unsuccessfully otherwise. - -.SH STANDARDS - -Scrut is nearly compatible with POSIX's test utility though it is narrower in -scope. Notably, the +The +.BR test (1p) +utility contains functionality that was broken out into separate programs. Thus, +the scope of this program is narrower than it. Notably, the .B -h option is now invalid and therefore shows usage information instead of being an alias to the modern .B -L option. - +.\" .SH AUTHOR -Written by DTB . - +Written by DTB +.MT trinity@trinity.moe +.ME . +.\" .SH COPYRIGHT -Copyright © 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later +Copyright \(co 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -access(3p), lstat(3p), test(1p) +.BR access (3p), +.BR lstat (3p), +.BR test (1p) diff --git a/docs/strcmp.1 b/docs/strcmp.1 index 14c0d0d..db6d3e8 100644 --- a/docs/strcmp.1 +++ b/docs/strcmp.1 @@ -1,62 +1,75 @@ .\" Copyright (c) 2023–2024 DTB -.\" Copyright (c) 2023 Emma Tebibyte +.\" Copyright (c) 2023–2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH STRCMP 1 - +.\" +.TH STRCMP 1 2024-07-15 "Harakit X.X.X" .SH NAME - strcmp \(en compare strings - +.\" .SH SYNOPSIS strcmp -.RM [ string ] -.RB [ strings... ] - +.B string string... +.\" .SH DESCRIPTION -Strcmp checks whether the given strings are the same. -Strcmp exits successfully if the strings are identical. Otherwise, strcmp exits -with the value 1 if an earlier string has a greater byte value than a later -string (e.g. -.R strcmp b a -) -and 255 if an earlier string has a lesser byte value (e.g. -.R strcmp a b -). - +Check whether string arguments are the same. +.\" .SH DIAGNOSTICS -Strcmp will print an error message and exit unsuccessfully with a status -described in sysexits(3) if used incorrectly (given less than two operands). +The program will exit successfully if the strings are identical. Otherwise, it +will exit with an error code less than 128 if a string passed has a lesser byte +value than one of the prior strings: -.SH UNICODE +.RS +strcmp b a +.RE -Strcmp will exit unsuccessfully if the given strings are not identical; -Unicode strings may need to be normalized if the intent is to check visual -similarity and not byte similarity. +or with an error code greater than 128 if it has a greater byte value than one +of the prior strings: +.RS +strcmp a b +.RE + +When invoked incorrectly, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code. +.\" +.SH CAVEATS + +The program will exit unsuccessfully if the given strings are not identical; +therefore, Unicode strings may need to be normalized if the intent is to check +visual similarity and not byte similarity. +.\" .SH RATIONALE The traditional tool for string comparisons in POSIX and other Unix shells has -been test(1). This tool also handles integer comparisons and file scrutiny. -These parts of its functionality have been broken out into multiple utilities. - -Strcmp’s functionality may be performed on a POSIX-compliant system with -test(1p). +been +.BR test (1). +This tool also handles integer comparisons and file scrutiny. These parts of its +functionality have been broken out into multiple utilities. +This program\(cqs functionality may be performed on a POSIX-compliant system +with +.BR test (1p). +.\" .SH AUTHOR -Written by DTB . - +Written by DTB +.MT trinity@trinity.moe +.ME . +.\" .SH COPYRIGHT -Copyright © 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later +Copyright \(co 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -strcmp(3), intcmp(1), scrut(1), test(1p) +.BR strcmp (3), +.BR intcmp (1), +.BR scrut (1), +.BR test (1p) diff --git a/docs/swab.1 b/docs/swab.1 index a0c1be3..c364814 100644 --- a/docs/swab.1 +++ b/docs/swab.1 @@ -1,71 +1,75 @@ .\" Copyright (c) 2024 DTB +.\" Copyright (c) 2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH swab 1 - +.\" +.TH SWAB 1 2024-06-17 "Harakit X.X.X" .SH NAME - swab \(en swap bytes - +.\" .SH SYNOPSIS swab -.RB ( -f ) -.RB ( -w -.R [ -.B word size -.R ]) +.RB [ -w\ word_size ] +.\" +.SH DESCRIPTION -.SH USAGE - -Swab swaps the latter and former halves of a block of bytes. +Swap the latter and former halves of a block of bytes. +.\" +.SH OPTIONS +.IP \fB-w\fP\ \fIword_size\fP +Configures the word size; that is, the size in bytes of the block size on which +to operate. The default word size is 2. The word size must be cleanly divisible +by 2, otherwise the block of bytes being processed can\(cqt be halved. +.\" .SH EXAMPLES -The following sh(1p) line: +The following +.BR sh (1p) +line: -.R printf 'hello world!\n' | swab +.RS +printf 'hello world!\(rsn' | swab +.RE +.\" If you change this, make sure to change it in tests/bonsai/swab.mk too. Produces the following output: -.R ehll oowlr!d - -.SH OPTIONS - -The -.B -f -option ignores system call interruptions. -.PP -The -.B -w -option configures the word size; that is, the size in bytes of the block size -on which to operate. By default the word size is 2. The word size must be -cleanly divisible by 2, otherwise the block of bytes being processed can't be -halved. - +.RS +ehll oowlr!d +.RE +.\" .SH DIAGNOSTICS -If an error is encountered in input, output, or invocation, a diagnostic -message will be written to standard error and swab will exit with the -appropriate status from sysexits.h(3). - +In the event of an error, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code. +.\" .SH RATIONALE -Swab was modeled after the -.R conv=swab -functionality specified in the POSIX dd utility but additionally allows the -word size to be configured. -.PP -Swab is useful for fixing the endianness of binary files produced on other -machines. +This program was modeled and named after the +.B conv=swab +functionality specified +in the +.BR dd (1p) +utility. It additionally allows the word size to be configured. +This functionality is useful for fixing the endianness of binary files produced +on other machines. +.\" +.SH AUTHOR + +Written by DTB +.MT trinity@trinity.moe +.ME . +.\" .SH COPYRIGHT -Copyright (c) 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later +Copyright \(co 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later . - +.\" .SH SEE ALSO - -dd(1p) +.BR dd (1p) diff --git a/docs/true.1 b/docs/true.1 index 3c292d8..97af65b 100644 --- a/docs/true.1 +++ b/docs/true.1 @@ -1,35 +1,36 @@ .\" Copyright (c) 2022, 2024 DTB -.\" Copyright (c) 2023 Emma Tebibyte +.\" Copyright (c) 2023–2024 Emma Tebibyte .\" .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . - -.TH TRUE 1 - +.\" +.TH TRUE 1 2024-06-06 "Harakit X.X.X" .SH NAME - true \(en do nothing, successfully - +.\" .SH DESCRIPTION -True does nothing regardless of operands or standard input. -True will always return an exit code of 0. - +Do nothing regardless of operands or standard input. An exit code of 0 will +always be returned. +.\" .SH RATIONALE -True exists for the construction of control flow and loops based on a success. - -True functions as described in POSIX.1-2017. - +In \fIPOSIX.1-2017\fP, +.BR true (1p) +exists for the construction of control flow and loops based on a success. This +implementation functions as described in that standard. +.\" .SH AUTHOR -Written by Emma Tebibyte . - +Written by Emma Tebibyte +.MT emma@tebibyte.media +.ME . +.\" .SH COPYRIGHT This work is marked with CC0 1.0. To see a copy of this license, visit . - +.\" .SH SEE ALSO - -false(1p) +.BR false (1p), +.BR true (1p) diff --git a/include/FreeBSD.mk b/include/FreeBSD.mk new file mode 100644 index 0000000..8d679b4 --- /dev/null +++ b/include/FreeBSD.mk @@ -0,0 +1,6 @@ +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. diff --git a/include/None.mk b/include/None.mk new file mode 100644 index 0000000..e69de29 diff --git a/include/OpenBSD.mk b/include/OpenBSD.mk new file mode 100644 index 0000000..9624629 --- /dev/null +++ b/include/OpenBSD.mk @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +OSLIB = build/o/libopenbsd.rlib +RUSTFLAGS += --extern openbsd=$(OSLIB) + +$(OSLIB): src/libopenbsd.rs + $(RUSTC) $(RUSTFLAGS) --crate-type=lib --crate-name=openbsd \ + -o $@ src/libopenbsd.rs diff --git a/src/dj.c b/src/dj.c index 8a6732c..6d6a117 100644 --- a/src/dj.c +++ b/src/dj.c @@ -1,5 +1,6 @@ /* * 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 @@ -16,437 +17,422 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -#include /* isupper(3), tolower(3) */ +#include /* assert(3) */ #include /* errno */ #include /* open(2) */ +#include /* bool */ #include /* fprintf(3), stderr */ -#include /* free(3), malloc(3), strtol(3), size_t */ +#include /* malloc(3), strtol(3), size_t */ #include /* memcpy(3), memmove(3), memset(3) */ -#include /* EX_OK, EX_USAGE */ +#include /* EX_OK, EX_OSERR, EX_USAGE */ #include /* close(2), getopt(3), lseek(2), read(2), write(2), - * optarg, optind, STDIN_FILENO, STDOUT_FILENO */ -extern int errno; + * pledge(2), unveil(2), optarg, optind, STDIN_FILENO, + * STDOUT_FILENO */ +#include /* S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR */ + +char *program_name = "dj"; /* dj uses two structures that respectively correspond to the reading and * writing ends of its jockeyed "pipe". User-configurable members are noted * with their relevant options. */ -struct Io{ - int bs; /* buffer size (-bB) */ +struct Io { + char *buf; /* buffer */ + char *fn; /* file name (-io) */ + size_t bs; /* buffer size (-bB) */ size_t bufuse; /* buffer usage */ - char *buf; /* buffer */ - int bytes; /* bytes processed */ - int fd; /* file descriptor */ - int fl; /* file opening flags */ - char *fn; /* file name (may be stdin_name or stdout_name) (-io) */ - int prec; /* partial records processed */ - int rec; /* records processed */ - long seek; /* bytes to seek/skip (will be 0 after skippage) (-sS) */ -} ep[2]; /* "engineered pipe"; also "extended play", for the deejay */ + size_t bytes; /* bytes processed */ + size_t prec; /* partial records processed */ + size_t rec; /* records processed */ + long seek; /* remaining bytes to seek/skip (-sS) */ + int error; /* errno */ + int fd; /* file descriptor */ + int fl; /* file opening flags */ +}; -/* Additionally, the following global variables are used to store user options. - */ - -/* (-a) */ static int align; /* Only the lower 8b are used but align is - * negative if no alignment is being done. */ - -/* (-c) */ static int count; /* 0 if dj(1) runs until no more reads are - * possible. */ - -/* ASCII field separator delimited statistics */ - static char *fmt_asv = "%d\037%d\036%d\037%d\035%d\036%d\034"; -/* human-readable statistics */ - static char *fmt_human = "%d+%d > %d+%d; %d > %d\n"; -/* pointer to chosen formatting */ -/* (-H) */ static char *fmt_output; /* fmt_asv (default) or fmt_human (-H) */ - -/* (-dq) */ static char debug; /* - * -d increments dj -qq | 0 - no diagnostic output whatsoever - * -q decrements dj -q | 1 - typical output without - * | notifications on partial reads or - * | writes - * dj | 2 - typical output (default) - * dj -d | 3 - verbose status messages */ - -/* (-n) */ static char noerror; /* 0 - exits on partial reads or writes - * (default) - * 1 - retries on partial reads/writes - * (-n) */ - -/* Non-configurable defaults. */ -#define bs_default 1024 /* GNU dd(1) default; twice POSIX but a neat 2^10 */ -static char *program_name = ""; static char *stdin_name = ""; static char *stdout_name = ""; -static int read_flags = O_RDONLY; /* These flags are consistent with Busybox */ -static int write_flags = O_WRONLY | O_CREAT; /* dd(1). */ -/* Macro to set defaults for user-configurable options. */ -#define setdefaults do{ \ - align = -1; \ - count = 0; \ - debug = 2; \ - fmt_output = fmt_asv; \ - noerror = 0; \ - ep[0].fl = read_flags; \ - Io_setdefaults(&ep[0]); \ - ep[1].fl = write_flags; \ - Io_setdefaults(&ep[1]); }while(0) +static int creat_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH + | S_IWOTH; /* Consistent with touch(1p). */ +static int read_flags = O_RDONLY; /* Consistent with Busybox dd(1). */ +static int write_flags = O_WRONLY | O_CREAT; #define MIN(a, b) (((a) < (b)) ? (a) : (b)) -/* Macro to check if fd is a std* file, e.g. stdin. */ -#define fdisstd(fd) \ - ((fd) == STDIN_FILENO \ - || (fd) == STDOUT_FILENO \ - || (fd) == STDERR_FILENO) +/* Macro to check if fd is stdin or stdout */ +#define fdisstd(fd) ((fd) == STDIN_FILENO || (fd) == STDOUT_FILENO) -/* Macro to call the cleanup functions that operate on struct io on the - * particular io[2] used in main. Error conditions are not checked because this - * is only used when the program is about to terminate (hence its name). */ -#define terminate(io) do{ \ - Io_buffree(&(io)[0]); \ - Io_buffree(&(io)[1]); \ - Io_fdclose(&(io)[0]); \ - Io_fdclose(&(io)[1]); }while(0) - -/* Allocates *io's buffer. Returns NULL if unsuccessful. */ -static void * -Io_bufalloc(struct Io *io){ - - return (io->buf = malloc(io->bs * (sizeof *io->buf))); -} - -/* Frees *io's buffer. Returns io. */ +/* Completes one Io block read */ static struct Io * -Io_buffree(struct Io *io){ - - free(io->buf); - - return io; -} - -/* Fills the unused portion of io's buffer with padding, updating io->bufuse. - * Returns io. */ -static struct Io * -Io_bufrpad(struct Io *io, int padding){ - - memset(io->buf + io->bufuse, padding, io->bs - io->bufuse); - io->bufuse = io->bs; - - return io; -} - -/* Copies from the buffer in src as much as possible to the free space in the - * dest buffer, removing the copied units from src and permuting the remaining - * units in the src buffer to the start of the buffer, modifying both the src - * and dest bufuse and returning dest. */ -static struct Io* -Io_bufxapp(struct Io *dest, struct Io *src){ - int n; - - n = MIN(src->bufuse, dest->bs - dest->bufuse); - memcpy(dest->buf + dest->bufuse, src->buf, n); - dest->bufuse += n; - memmove(src->buf, src->buf + n, src->bs - n); - src->bufuse -= n; - - return dest; -} - -/* Copies from the buffer in src to the buffer in dest no more than n units, - * removing the copied units from src and permuting the remaining units in the - * src buffer to the start of the buffer, modifying both the src and dest - * bufuse and returning dest. */ -static struct Io* -Io_bufxfer(struct Io *dest, struct Io *src, int n){ - - memcpy(dest->buf, src->buf, (dest->bufuse = n)); - memmove(src->buf, src->buf + n, (src->bufuse -= n)); - - return dest; -} - -/* Closes io->fn and returns -1 on error, otherwise io->fd. */ -static int -Io_fdclose(struct Io *io){ - - return fdisstd(io->fd) - ? 0 - : close(io->fd); -} - -/* Opens io->fn and saves the file descriptor into io->fd. Returns io->fd, - * which will be -1 if an error occured. */ -static int -Io_fdopen(struct Io *io, char *fn){ - int fd; - - if((fd = open(fn, io->fl, - /* these are the flags used by touch(1p) */ - S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)) - != -1 - && Io_fdclose(io) == 0){ - io->fd = fd; - io->fn = fn; - } - - return fd; -} - -/* Seeks io->seek bytes through *io's file descriptor, (counter-intuitively) - * returning -1 if successful and a sysexits.h exit code if an unrecoverable - * error occurred. io->buf will be cleared of useful bytes and io->seek will - * be set to zero to indicate the seek occurred. */ -static int -Io_fdseek(struct Io *io){ - int (*op)(int, void *, size_t); - - if(!fdisstd(io->fd) && lseek(io->fd, io->seek, SEEK_SET) != -1) - return -1; - - /* repeated code to get the condition out of the loop */ - if(io->fl == write_flags){ - memset(io->buf, '\0', io->bs); - /* We're going to cheat and use bufuse as the retval for write(2), - * which is fine because it'll be zeroed as this function returns - * anyway. */ - do{ - if((io->bufuse = write(io->fd, io->buf, MIN(io->bs, io->seek))) - == 0) - /* second chance */ - io->bufuse = write(io->fd, io->buf, MIN(io->bs, io->seek)); - }while((io->seek -= io->bufuse) > 0 && io->bufuse != 0); - }else if(io->fl == read_flags){ - do{ - if((io->bufuse = read(io->fd, io->buf, MIN(io->bs, io->seek))) - == 0) - /* second chance */ - io->bufuse = read(io->fd, io->buf, MIN(io->bs, io->seek)); - }while((io->seek -= io->bufuse) > 0 && io->bufuse != 0); - }else - return EX_SOFTWARE; - - io->bufuse = 0; - - return -1; -} - -/* Reads io->bs bytes from *io's file descriptor into io->buf, storing the - * number of read bytes in io->bufuse and updating io->bytes. If io->bufuse is - * 0, errno will probably be set. Returns io. */ -static struct Io * -Io_read(struct Io *io){ - - io->bytes += (io->bufuse = read(io->fd, io->buf, io->bs)); - - return io; -} - -/* Sets the variables in a struct *io to the defaults. Identifies the read/ - * write ends of the "pipe" by checking io->fl. Returns io. */ -static struct Io * -Io_setdefaults(struct Io *io){ - - io->bs = bs_default; - io->buf = NULL; - io->bytes = 0; - io->fd = (io->fl == read_flags) ? STDIN_FILENO : STDOUT_FILENO; - io->fn = (io->fl == read_flags) ? stdin_name : stdout_name; - io->prec = 0; - io->rec = 0; - io->seek = 0; - - return io; -} - -/* Writes io->bufuse units from io->buf to io->fd, permuting any unwritten - * bytes to the start of io->buf and updating io->bufuse. If io->bufuse doesn't - * change, errno will probably be set. Returns io. */ -static struct Io * -Io_write(struct Io *io){ +Io_read(struct Io *io) { int t; - if((t = write(io->fd, io->buf, io->bufuse)) > 0) - memmove(io->buf, io->buf + t, (io->bufuse -= t)); - io->bytes += t; + assert(io->bs > 0); + assert(io->bufuse < io->bs); + + if ((t = read(io->fd, &(io->buf)[io->bufuse], io->bs - io->bufuse)) < 0) { + io->error = errno; + t = 0; + } + + io->bufuse += t; + io->bytes += t; + io->prec += (0 < io->bufuse && io->bufuse < io->bs); + io->rec += (io->bufuse == io->bs); + + assert(io->bufuse <= io->bs); return io; } -/* Prints an error message suitable for the event of an operating system error, - * with the error itself to be described in the string s. */ -static int -oserr(char *s){ +/* Completes one Io block write */ +static struct Io * +Io_write(struct Io *io) { + int t; - fprintf(stderr, "%s: %s: %s\n", program_name, s, strerror(errno)); + assert(io->bufuse > 0); + assert(io->bufuse <= io->bs); + + if ((t = write(io->fd, io->buf, io->bufuse)) < 0) { + io->error = errno; + t = 0; + } else if (t > 0) { + (void)memmove(io->buf, &(io->buf)[t], (io->bufuse -= t)); + } + + io->bytes += t; + io->prec += (t > 0 && io->bufuse > 0); + io->rec += (t > 0 && io->bufuse == 0); + + return io; +} + +static int +oserr(char *e, int n) { /* program_name: [failing component:] error */ + (void)fprintf(stderr, "%s: ", program_name); + if (e != NULL) { (void)fprintf(stderr, "%s: ", e); } + (void)fprintf(stderr, "%s\n", strerror(n)); return EX_OSERR; } /* Prints statistics regarding the use of dj, particularly partially and - * completely read and written records, accessing debug, ep, and fmt_output. */ -static void -output(void){ - - if(debug >= 1) - fprintf(stderr, fmt_output, - ep[0].rec, ep[0].prec, ep[1].rec, ep[1].prec, - ep[0].bytes, ep[1].bytes); - - return; + * completely read and written records. */ +static int +fprintio(FILE *stream, char *fmt, struct Io io[2]) { + return fprintf( + stream, + fmt, + io[0].rec, + io[0].prec, + io[1].rec, + io[1].prec, + io[0].bytes, + io[1].bytes + ); } +/* To be assigned to main:fmt and used with printio(). */ +static char *fmt_asv = + "%d" /* io[0].rec */ "\037" /* ASCII US */ + "%d" /* io[0].prec */ "\036" /* ASCII RS */ + "%d" /* io[1].rec */ "\037" /* ASCII US */ + "%d" /* io[1].prec */ "\035" /* ASCII GS */ + "%d" /* io[0].bytes */ "\036" /* ASCII RS */ + "%d" /* io[1].bytes */ "\034" /* ASCII FS */ + "\n" +; +static char *fmt_human = "%d+%d > %d+%d; %d > %d\n"; + /* Parses the string s to an integer, returning either the integer or in the * case of an error a negative integer. This is used for argument parsing * (e.g. -B [int]) in dj and no negative integer would be valid anyway. */ static long -parse(char *s){ +parse(char *s) { long r; errno = 0; r = strtol(s, &s, 0); - return (*s == '\0' /* no chars left unparsed */ && errno == 0) - ? r - : -1; + return (*s == '\0' /* no chars left unparsed */ && errno == 0) ? r : -1; } static int -usage(void){ - - fprintf(stderr, "Usage: %s (-AdfHqQ) (-a [byte]) (-c [count])\n" - "\t(-i [input file]) (-b [input block size]) (-s [input offset])\n" - "\t(-o [output file]) (-B [output block size]) (-S [output offset])\n", - program_name); +usage(char *argv0) { + (void)fprintf( + stderr, + "Usage: %s [-Hn] [-a byte] [-c count]\n" + "\t[-i file] [-b block_size] [-s offset]\n" + "\t[-o file] [-B block_size] [-S offset]\n", + argv0 + ); return EX_USAGE; } -int main(int argc, char *argv[]){ - int c; - int i; +int main(int argc, char *argv[]) { + int align; /* low 8b used, negative if no alignment is being done */ + int count; /* -1 if dj(1) runs until no more reads are possible */ + char *fmt; /* set to fmt_asv (default) or fmt_human (-H) */ + size_t i; /* side of io (in or out) being modified */ + bool retry; /* false if exits on partial reads or writes */ + struct Io io[2 /* { in, out } */]; - setdefaults; +#ifdef __OpenBSD__ + if (pledge("cpath rpath stdio unveil wpath", NULL) == -1) { + return oserr(NULL, errno); + } +#endif + + /* Set defaults. */ + align = -1; + count = -1; + fmt = fmt_asv; + retry = 0; + for (i = 0; i < (sizeof io) / (sizeof *io); ++i) { + io[i].bs = 1024 /* 1 KiB */; /* GNU dd(1) default; POSIX says 512B */ + io[i].bufuse = 0; + io[i].bytes = 0; + io[i].fd = i == 0 ? STDIN_FILENO : STDOUT_FILENO; + io[i].fn = i == 0 ? stdin_name : stdout_name; + io[i].fl = i == 0 ? read_flags : write_flags; + io[i].error = 0; + io[i].prec = 0; + io[i].rec = 0; + io[i].seek = 0; + } + + if (argc > 0) { + int c; - if(argc > 0){ program_name = argv[0]; - while((c = getopt(argc, argv, "a:Ab:B:c:di:hHnqs:S:o:")) != -1) - switch(c){ - case 'i': case 'o': - i = (c == 'o'); - if(optarg[0] == '-' && optarg[1] == '\0'){ /* optarg == "-" */ - ep[i].fd = (i == 0) ? STDIN_FILENO : STDOUT_FILENO; - ep[i].fn = (i == 0) ? stdin_name : stdout_name; - break; - }else if(Io_fdopen(&ep[i], optarg) != -1) - break; - terminate(ep); - return oserr(optarg); - case 'A': align = '\0'; break; - case 'd': ++debug; break; - case 'n': noerror = 1; break; - case 'H': fmt_output = fmt_human; break; - case 'q': --debug; break; - case 'a': - if(optarg[0] != '\0' && optarg[1] == '\0'){ - align = optarg[0]; - break; - } - /* FALLTHROUGH */ - case 'c': case 'b': case 's': case 'B': case 'S': - if(c == 'c' && (count = parse(optarg)) >= 0) - break; - i = isupper(c); - c = tolower(c); - if((c == 'b' && (ep[i].bs = parse(optarg)) > 0) - || (c == 's' && (ep[i].seek = parse(optarg)) >= 0)) - break; - /* FALLTHROUGH */ - default: - terminate(ep); - return usage(); + while ((c = getopt(argc, argv, "a:b:B:c:i:hHns:S:o:")) != -1) { + switch (c) { + case 'i': case 'o': /* input, output */ + i = (c == 'o'); + + /* optarg == "-" (stdin/stdout) */ + if (optarg[0] == '-' && optarg[1] == '\0') { + io[i].fd = i == 0 ? STDIN_FILENO : STDOUT_FILENO; + io[i].fn = i == 0 ? stdin_name : stdout_name; + break; + } else { + int fd; + +#ifdef __OpenBSD__ + if (unveil(optarg, i == 0 ? "r" : "wc") == -1) { + return oserr(NULL, errno); + } +#endif + + if ( + (fd = open(optarg, io[i].fl, creat_mode)) != -1 + && (fdisstd(io[i].fd) || close(io[i].fd) == 0) + ) { + io[i].fd = fd; + io[i].fn = optarg; + break; + } + } + + return oserr(optarg, errno); + /* UNREACHABLE */ + case 'n': retry = 1; break; /* retry failed reads once */ + case 'H': fmt = fmt_human; break; /* human-readable output */ + case 'a': /* input buffer padding */ + if (optarg[0] == '\0' || optarg[1] == '\0') { + align = optarg[0]; + break; + } + /* FALLTHROUGH */ + case 'c': /* number of reads */ + case 'b': case 'B': /* input/output block size */ + case 's': case 'S': /* (s)kip/(S)eek in input/output */ + if (c == 'c' && (count = parse(optarg)) >= 0) { break; } + + i = (c >= 'A' && c <= 'Z'); + c |= 0x20; /* 0b 0010 0000 (ASCII) make lowercase */ + + if ( /* if -b or -s is parsed out correctly */ + (c == 'b' && (io[i].bs = parse(optarg)) > 0) + || (c == 's' && (io[i].seek = parse(optarg)) >= 0) + ) { break; } /* don't error */ + + /* FALLTHROUGH */ + default: + return usage(program_name); } + } } - if(debug >= 3) - fprintf(stderr, - "argv0=%s\n" - "in=%s\tibs=%d\tskip=%ld\talign=%hhx\tcount=%d\n" - "out=%s\tobs=%d\tseek=%ld\tdebug=%2d\tnoerror=%d\n", - program_name, - ep[0].fn, ep[0].bs, ep[0].seek, align, count, - ep[1].fn, ep[1].bs, ep[1].seek, debug, noerror); +#ifdef __OpenBSD__ + if (unveil(NULL, NULL) == -1) { return oserr(NULL, errno); } +#endif - if(argc > optind){ - terminate(ep); - return usage(); - } + assert(io->fd != STDIN_FILENO || io->fl == read_flags); + assert(io->fd != STDOUT_FILENO || io->fl == write_flags); - for(i = 0; i <= 1; ++i){ - if(Io_bufalloc(&ep[i]) == NULL){ - fprintf(stderr, "%s: Failed to allocate %d bytes\n", - program_name, ep[i].bs); - terminate(ep); + if (argc > optind) { return usage(program_name); } + + for (i = 0; i < (sizeof io) / (sizeof *io); ++i) { + /* buffer allocation */ + if ((io[i].buf = malloc(io[i].bs * (sizeof *(io[i].buf)))) == NULL) { + (void)fprintf( + stderr, "%s: Failed to allocate %zd bytes\n", + program_name, io[i].bs + ); return EX_OSERR; - }else if(ep[i].seek > 0) - switch(Io_fdseek(&ep[i])){ - case EX_OK: - output(); - terminate(ep); - return EX_OK; - } + } + + /* easy seeking */ + if (!fdisstd(io[i].fd) && lseek(io[i].fd, io[i].seek, SEEK_SET) != -1) { + io[i].seek = 0; + } } - do{ /* read */ - Io_read(&ep[0]); - if(!noerror && ep[0].bufuse == 0) - Io_read(&ep[0]); /* second chance */ - if(ep[0].bufuse == 0) /* that's all she wrote */ - break; - else if(ep[0].bufuse < ep[0].bs){ - ++ep[0].prec; - if(debug >= 2){ - fprintf(stderr, "%s: Partial read:\n\t", program_name); - output(); + assert(io[1].bufuse == 0); /* requirement for hard seeking */ + + /* hard seeking; t is io[1].bufuse, before Io_write subtracts from it */ + for(size_t t; io[1].seek > 0; io[1].seek -= (t - io[1].bufuse)) { + (void)memset( + io[1].buf, '\0', /* set buf to all nulls */ + (t = io[1].bufuse = MIN(io[1].bs, io[1].seek)) /* saturate block */ + ); + + if (Io_write(&io[1])->bufuse == t && !retry && io[1].error == 0) { + (void)Io_write(&io[1]); /* second chance */ + } + + if (io[1].error != 0) { return oserr(io[1].fn, io[1].error); } + + if (io[1].bufuse == t) { break; } /* all writes failed! */ + } + + io[1].bufuse = 0; /* reset after hard seek */ + + if (io[1].seek > 0) { /* hard seeking failed */ + (void)fprintio(stderr, fmt, io); + return oserr(io[1].fn, errno); + } + + for ( ; + count == -1 || count > 0; + count -= (count != -1) /* decrement if counting */ + ) { + assert(io[0].bufuse == 0); + + { /* read */ + long skipping; + size_t t; + + /* hack to intentionally get a partial read from Io_read */ + if ((skipping = MIN(io[0].seek, io[0].bs)) > 0) { + io[0].bufuse = io[0].bs - (size_t)skipping; } - if(!noerror) - count = 1; - if(align >= 0) - Io_bufrpad(&ep[0], align); - }else - ++ep[0].rec; - /* write */ - do{ if(ep[1].bs > ep[0].bs){ /* io[1].bs > io[0].bs */ - Io_bufxapp(&ep[1], &ep[0]); - if(ep[0].bs + ep[1].bufuse <= ep[1].bs && count != 1) - continue; /* we could write more */ - }else - Io_bufxfer(&ep[1], &ep[0], MIN(ep[0].bufuse, ep[1].bs)); + t = io[0].bufuse; + if (Io_read(&io[0])->bufuse == t && !retry && io[0].error == 0) { + (void)Io_read(&io[0]); /* second chance */ + } - c = ep[1].bufuse; - Io_write(&ep[1]); - if(!noerror && ep[1].bufuse == c) - Io_write(&ep[1]); /* second chance */ - if(c == ep[1].bufuse){ /* no more love */ - count = 1; - break; - }else if(c > ep[1].bufuse && ep[1].bufuse > 0){ - ep[1].prec += 1; - if(debug >= 2){ - fprintf(stderr, "%s: Partial write:\n\t", program_name); - output(); + assert(io[0].bufuse >= t); + + if (io[0].bufuse == t) { break; } /* that's all she wrote */ + + if (/* t < io[0].bufuse && */ io[0].bufuse < io[0].bs) { + (void)fprintf(stderr, "%s: Partial read:\n\t", program_name); + (void)fprintio(stderr, fmt, io); + + if (!retry) { count = 1; } + + if (align >= 0) { + /* fill the rest of the ibuf with padding */ + (void)memset( + &(io[0].buf)[io[0].bufuse], + align, + io[0].bs - io[0].bufuse + ); + + io->bufuse = io->bs; } - if(!noerror) - count = 1; - }else if(ep[1].bufuse == 0 && c < ep[1].bs) - ++ep[1].prec; - else - ++ep[1].rec; - }while(ep[0].bufuse > 0); - }while(count == 0 || --count > 0); + } - output(); - terminate(ep); + if (skipping > 0) { + io[0].seek -= skipping; + io[0].bufuse = 0; + count += (count != -1); /* increment if counting */ + continue; + } + } + + assert(io[0].bufuse > 0); + + while (io[0].bufuse > 0) { /* write */ + if (io[0].bs <= io[1].bs) { + int n; + + (void)memcpy( /* saturate obuf */ + io[1].buf, io[0].buf, + (io[1].bufuse = (n = MIN(io[0].bufuse, io[1].bs))) + ); + + /* permute the copied units out of ibuf */ + (void)memmove(io[0].buf, &(io[0].buf)[n], (io[0].bufuse -= n)); + } else /* if(io[0].bs > io[1].bs) */ { + int n; + + /* drain what we can from ibuf */ + (void)memcpy( + &(io[1].buf)[io[1].bufuse], io[0].buf, + (n = MIN(io[0].bufuse, io[1].bs - io[1].bufuse)) + ); + io[1].bufuse += n; + + /* permute out the copied units */ + (void)memmove(io[0].buf, &(io[0].buf)[n], io[0].bs - n); + io[0].bufuse -= n; + + if(io[0].bs + io[1].bufuse <= io[1].bs && count != 1) { + continue; /* obuf not saturated - we could write more */ + } + } + + { /* writes actually happen, or die */ + size_t t; + + t = io[1].bufuse; + if (Io_write(&io[1])->bufuse == t + && !retry + && io[1].error == 0) { + (void)Io_write(&io[1]); /* second chance */ + } + + assert(io[1].error == 0 || io[1].bufuse == t); + /* if the Io_writes errored, bufuse wouldn't have changed, and + * the error will be reported at the end of the read/write + * loop */ + + assert(io[1].bufuse <= t); + + if (io[1].bufuse == t) { /* no more love */ + count = 1; + break; + } + } + + if (0 < io[1].bufuse /* && io[1].bufuse < t */) { + (void)fprintf(stderr, "%s: Partial write:\n\t", program_name); + (void)fprintio(stderr, fmt, io); + + if(!retry) { count = 1; } + } + } + } + + (void)fprintio(stderr, fmt, io); + + for (i = 0; i < (sizeof io) / (sizeof *io); ++i) { + if (io[i].error) { return oserr(io[i].fn, io[i].error); } + } return EX_OK; } diff --git a/src/false.c b/src/false.c index 3b6ec2a..1617b03 100644 --- a/src/false.c +++ b/src/false.c @@ -1,9 +1,18 @@ /* - * Copyright (c) 2023 Emma Tebibyte + * Copyright (c) 2023–2024 Emma Tebibyte * SPDX-License-Identifier: CC0 * * This work is marked with CC0 1.0. To view a copy of this license, visit * . */ -int main() { return 1; } +#ifdef __OpenBSD__ +# include /* pledge(2) */ +#endif + +int main() { +#ifdef __OpenBSD__ + pledge(NULL, NULL); +#endif +return 1; +} diff --git a/src/fop.rs b/src/fop.rs index b829602..66b7e82 100644 --- a/src/fop.rs +++ b/src/fop.rs @@ -26,61 +26,90 @@ extern crate getopt; extern crate strerror; extern crate sysexits; -use getopt::{ Opt, Parser }; +use getopt::GetOpt; use strerror::StrError; use sysexits::{ EX_DATAERR, EX_IOERR, EX_UNAVAILABLE, EX_USAGE }; +#[cfg(target_os="openbsd")] use sysexits::EX_OSERR; +#[cfg(target_os="openbsd")] extern crate openbsd; +#[cfg(target_os="openbsd")] use openbsd::{ Promises, pledge }; + fn main() { let argv = args().collect::>(); - let mut d = '␞'; - let mut arg_parser = Parser::new(&argv, "d:"); + let mut d = '\u{1E}'.to_string(); /* ASCII record separator */ + let mut optind = 1; - while let Some(opt) = arg_parser.next() { - match opt { - Ok(Opt('d', Some(arg))) => { - let arg_char = arg.chars().collect::>(); - if arg_char.len() > 1 { - eprintln!("{}: {}: Not a character.", argv[0], arg); - exit(EX_USAGE); - } else { d = arg_char[0]; } + if cfg!(target_os="openbsd") { + let promises = Promises::new("stdio proc exec"); + if let Err(e) = pledge(Some(promises), None) { + eprintln!("{}: {}", argv[0], e.strerror()); + exit(EX_OSERR); + } + } + + let usage = format!( + "Usage: {} [-d delimiter] index command [args...]", + argv[0], + ); + + while let Some(opt) = argv.getopt("d:") { + match opt.opt() { + Ok("d") => { + /* delimiter */ + d = opt.arg().unwrap(); + optind = opt.ind(); }, - _ => {}, + _ => { + eprintln!("{}", usage); + exit(EX_USAGE); + } }; } - let index_arg = arg_parser.index(); - let command_arg = arg_parser.index() + 1; - - argv.get(command_arg).unwrap_or_else(|| { - eprintln!("Usage: {} [-d delimiter] index command [args...]", argv[0]); - exit(EX_USAGE); - }); - - let index = argv[index_arg].parse::().unwrap_or_else(|e| { + /* parse the specified index as a number we can use */ + let index = argv[optind].parse::().unwrap_or_else(|e| { eprintln!("{}: {}: {}", argv[0], argv[1], e); exit(EX_DATAERR); }); - let mut buf = String::new(); - let _ = stdin().read_to_string(&mut buf); - let mut fields = buf.split(d).collect::>(); + /* index of the argv[0] for the operator command */ + let command_arg = optind as usize + 1; - let opts = argv + /* argv[0] of the operator command */ + let operator = argv.get(command_arg).unwrap_or_else(|| { + eprintln!("{}", usage); + exit(EX_USAGE); + }); + + /* read entire standard input into memory */ + let mut buf = String::new(); + if let Err(e) = stdin().read_to_string(&mut buf) { + eprintln!("{}: {}", argv[0], e.strerror()); + exit(EX_IOERR); + }; + + /* split the buffer by the delimiter (by default, '\u{1E}') */ + let mut fields = buf.split(&d).collect::>(); + + /* collect arguments for the operator command */ + let command_args = argv .iter() .clone() - .skip(command_arg + 1) + .skip(command_arg + 1) /* skip the command name */ .collect::>(); - let mut spawned = Command::new(argv.get(command_arg).unwrap()) - .args(opts) + /* spawn the command to operate on the field */ + let mut spawned = Command::new(operator) + .args(command_args) /* spawn with the specified arguments */ .stdin(Stdio::piped()) - .stdout(Stdio::piped()) + .stdout(Stdio::piped()) /* piped stdout to handle output ourselves */ .spawn() .unwrap_or_else( |e| { eprintln!("{}: {}: {}", argv[0], argv[command_arg], e.strerror()); exit(EX_UNAVAILABLE); }); + /* get field we want to pipe into spawned program */ let field = fields.get(index).unwrap_or_else(|| { eprintln!( "{}: {}: No such index in input", @@ -90,9 +119,10 @@ fn main() { exit(EX_DATAERR); }); + /* get the stdin of the newly spawned program and feed it the field val */ if let Some(mut child_stdin) = spawned.stdin.take() { let _ = child_stdin.write_all(field.as_bytes()); - drop(child_stdin); + drop(child_stdin); /* stay safe! drop your children! */ } let output = spawned.wait_with_output().unwrap_or_else(|e| { @@ -100,17 +130,27 @@ fn main() { exit(EX_IOERR); }); + /* get the output with which the original field will be replaced */ let mut replace = output.stdout.clone(); - if replace.pop() != Some(b'\n') { replace = output.stdout; } + /* pop trailing newline out if the input did not contain it */ + if fields[index].chars().last() != Some('\n') /* no newline */ + && replace.pop() != Some(b'\n') { /* pop last char of replacement */ + /* restore replacement to original command output if popped char was not + * a newline */ + replace = output.stdout; + } + /* convert the output of the program to UTF-8 */ let new_field = String::from_utf8(replace).unwrap_or_else(|e| { eprintln!("{}: {}: {}", argv[0], argv[command_arg], e); exit(EX_IOERR); }); + /* store the new field in the old fields vector */ fields[index] = &new_field; + /* fop it */ stdout().write_all( fields.join(&d.to_string()).as_bytes() ).unwrap_or_else(|e| { diff --git a/src/getopt-rs/error.rs b/src/getopt-rs/error.rs deleted file mode 100644 index 322af02..0000000 --- a/src/getopt-rs/error.rs +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2023 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/. - * - * This file incorporates work covered by the following copyright and permission - * notice: - * The Clear BSD License - * - * Copyright © 2017-2023 David Wildasin - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted (subject to the limitations in the disclaimer - * below) provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions, and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions, and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED - * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -use std::{ error, fmt }; - -use crate::ErrorKind::{ self, * }; - -/// A basic error type for [`Parser`](struct.Parser.html) -#[derive(Debug, Eq, PartialEq)] -pub struct Error { - culprit: char, - kind: ErrorKind, -} - -impl Error { - /// Creates a new error using a known kind and the character that caused the - /// issue. - pub fn new(kind: ErrorKind, culprit: char) -> Self { - Self { culprit, kind } - } - - /// Returns the [`ErrorKind`](enum.ErrorKind.html) for this error. - pub fn kind(self) -> ErrorKind { - self.kind - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.kind { - MissingArgument => write!( - f, - "option requires an argument -- {:?}", - self.culprit, - ), - UnknownOption => write!(f, "unknown option -- {:?}", self.culprit), - } - } -} - -impl error::Error for Error { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - None - } -} diff --git a/src/getopt-rs/errorkind.rs b/src/getopt-rs/errorkind.rs deleted file mode 100644 index 5475d8e..0000000 --- a/src/getopt-rs/errorkind.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2023 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/. - * - * This file incorporates work covered by the following copyright and permission - * notice: - * The Clear BSD License - * - * Copyright © 2017-2023 David Wildasin - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted (subject to the limitations in the disclaimer - * below) provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions, and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions, and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED - * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/// What kinds of errors [`Parser`](struct.Parser.html) can return. -#[derive(Debug, Eq, PartialEq)] -pub enum ErrorKind { - /// An argument was not found for an option that was expecting one. - MissingArgument, - /// An unknown option character was encountered. - UnknownOption, -} diff --git a/src/getopt-rs/lib.rs b/src/getopt-rs/lib.rs deleted file mode 100644 index 62f0e0d..0000000 --- a/src/getopt-rs/lib.rs +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2023 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/. - * - * This file incorporates work covered by the following copyright and permission - * notice: - * The Clear BSD License - * - * Copyright © 2017-2023 David Wildasin - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted (subject to the limitations in the disclaimer - * below) provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions, and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions, and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED - * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -//! # getopt -//! -//! `getopt` provides a minimal, (essentially) POSIX-compliant option parser. - -pub use crate::{ - error::Error, - errorkind::ErrorKind, - opt::Opt, - parser::Parser, - result::Result -}; - -mod error; -mod errorkind; -mod opt; -mod parser; -mod result; -#[cfg(test)] -mod tests; diff --git a/src/getopt-rs/opt.rs b/src/getopt-rs/opt.rs deleted file mode 100644 index 05b51e6..0000000 --- a/src/getopt-rs/opt.rs +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2023 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/. - * - * This file incorporates work covered by the following copyright and permission - * notice: - * The Clear BSD License - * - * Copyright © 2017-2023 David Wildasin - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted (subject to the limitations in the disclaimer - * below) provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions, and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions, and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED - * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -use std::fmt; - -/// A single option. -/// -/// For `Opt(x, y)`: -/// - `x` is the character representing the option. -/// - `y` is `Some` string, or `None` if no argument was expected. -/// -/// # Example -/// -/// ``` -/// # fn main() -> Result<(), Box> { -/// use getopt::Opt; -/// -/// // args = ["program", "-abc", "foo"]; -/// # let args: Vec = vec!["program", "-abc", "foo"] -/// # .into_iter() -/// # .map(String::from) -/// # .collect(); -/// let optstring = "ab:c"; -/// let mut opts = getopt::Parser::new(&args, optstring); -/// -/// assert_eq!(Opt('a', None), opts.next().transpose()?.unwrap()); -/// assert_eq!(Opt('b', Some("c".to_string())), opts.next().transpose()?.unwrap()); -/// assert_eq!(None, opts.next().transpose()?); -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Opt(pub char, pub Option); - -impl fmt::Display for Opt { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Opt({:?}, {:?})", self.0, self.1) - } -} diff --git a/src/getopt-rs/parser.rs b/src/getopt-rs/parser.rs deleted file mode 100644 index 6f06cc3..0000000 --- a/src/getopt-rs/parser.rs +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Copyright (c) 2023 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/. - * - * This file incorporates work covered by the following copyright and permission - * notice: - * The Clear BSD License - * - * Copyright © 2017-2023 David Wildasin - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted (subject to the limitations in the disclaimer - * below) provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions, and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions, and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED - * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -use std::collections::HashMap; - -use crate::{ error::Error, errorkind::ErrorKind, opt::Opt, result::Result }; - -/// The core of the `getopt` crate. -/// -/// `Parser` is implemented as an iterator over the options present in the given -/// argument vector. -/// -/// The method [`next`](#method.next) does the heavy lifting. -/// -/// # Examples -/// -/// ## Simplified usage: -/// ``` -/// # fn main() -> Result<(), Box> { -/// use getopt::Opt; -/// -/// // args = ["program", "-abc", "foo"]; -/// # let args: Vec = vec!["program", "-abc", "foo"] -/// # .into_iter() -/// # .map(String::from) -/// # .collect(); -/// let mut opts = getopt::Parser::new(&args, "ab:c"); -/// -/// assert_eq!(Some(Opt('a', None)), opts.next().transpose()?); -/// assert_eq!(1, opts.index()); -/// assert_eq!(Some(Opt('b', Some("c".to_string()))), opts.next().transpose()?); -/// assert_eq!(2, opts.index()); -/// assert_eq!(None, opts.next()); -/// assert_eq!(2, opts.index()); -/// assert_eq!("foo", args[opts.index()]); -/// # Ok(()) -/// # } -/// ``` -/// -/// ## A more idiomatic example: -/// ``` -/// # fn main() -> Result<(), Box> { -/// use getopt::Opt; -/// -/// // args = ["program", "-abc", "-d", "foo", "-e", "bar"]; -/// # let mut args: Vec = vec!["program", "-abc", "-d", "foo", "-e", "bar"] -/// # .into_iter() -/// # .map(String::from) -/// # .collect(); -/// let mut opts = getopt::Parser::new(&args, "ab:cd:e"); -/// -/// let mut a_flag = false; -/// let mut b_flag = String::new(); -/// let mut c_flag = false; -/// let mut d_flag = String::new(); -/// let mut e_flag = false; -/// -/// loop { -/// match opts.next().transpose()? { -/// None => break, -/// Some(opt) => match opt { -/// Opt('a', None) => a_flag = true, -/// Opt('b', Some(arg)) => b_flag = arg.clone(), -/// Opt('c', None) => c_flag = true, -/// Opt('d', Some(arg)) => d_flag = arg.clone(), -/// Opt('e', None) => e_flag = true, -/// _ => unreachable!(), -/// }, -/// } -/// } -/// -/// let new_args = args.split_off(opts.index()); -/// -/// assert_eq!(true, a_flag); -/// assert_eq!("c", b_flag); -/// assert_eq!(false, c_flag); -/// assert_eq!("foo", d_flag); -/// assert_eq!(true, e_flag); -/// -/// assert_eq!(1, new_args.len()); -/// assert_eq!("bar", new_args.first().unwrap()); -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Eq, PartialEq)] -pub struct Parser { - opts: HashMap, - args: Vec>, - index: usize, - point: usize, -} - -impl Parser { - /// Create a new `Parser`, which will process the arguments in `args` - /// according to the options specified in `optstring`. - /// - /// For compatibility with - /// [`std::env::args`](https://doc.rust-lang.org/std/env/fn.args.html), - /// valid options are expected to begin at the second element of `args`, and - /// `index` is - /// initialised to `1`. - /// If `args` is structured differently, be sure to call - /// [`set_index`](#method.set_index) before the first invocation of - /// [`next`](#method.next). - /// - /// `optstring` is a string of recognised option characters; if a character - /// is followed by a colon (`:`), that option takes an argument. - /// - /// # Note: - /// Transforming the OS-specific argument strings into a vector of `String`s - /// is the sole responsibility of the calling program, as it involves some - /// level of potential information loss (which this crate does not presume - /// to handle unilaterally) and error handling (which would complicate the - /// interface). - pub fn new(args: &[String], optstring: &str) -> Self { - let optstring: Vec = optstring.chars().collect(); - let mut opts = HashMap::new(); - let mut i = 0; - let len = optstring.len(); - - while i < len { - let j = i + 1; - - if j < len && optstring[j] == ':' { - opts.insert(optstring[i], true); - i += 1; - } else { - opts.insert(optstring[i], false); - } - i += 1; - } - - Self { - opts, - // "explode" the args into a vector of character vectors, to allow - // indexing - args: args.iter().map(|e| e.chars().collect()).collect(), - index: 1, - point: 0, - } - } - - /// Return the current `index` of the parser. - /// - /// `args[index]` will always point to the the next element of `args`; when - /// the parser is - /// finished with an element, it will increment `index`. - /// - /// After the last option has been parsed (and [`next`](#method.next) is - /// returning `None`), - /// `index` will point to the first non-option argument. - pub fn index(&self) -> usize { - self.index - } - - // `point` must be reset to 0 whenever `index` is changed - - /// Modify the current `index` of the parser. - pub fn set_index(&mut self, value: usize) { - self.index = value; - self.point = 0; - } - - /// Increment the current `index` of the parser. - /// - /// This use case is common enough to warrant its own optimised method. - pub fn incr_index(&mut self) { - self.index += 1; - self.point = 0; - } -} - -impl Iterator for Parser { - type Item = Result; - - /// Returns the next option, if any. - /// - /// Returns an [`Error`](struct.Error.html) if an unexpected option is - /// encountered or if an - /// expected argument is not found. - /// - /// Parsing stops at the first non-hyphenated argument; or at the first - /// argument matching "-"; - /// or after the first argument matching "--". - /// - /// When no more options are available, `next` returns `None`. - /// - /// # Examples - /// - /// ## "-" - /// ``` - /// use getopt::Parser; - /// - /// // args = ["program", "-", "-a"]; - /// # let args: Vec = vec!["program", "-", "-a"] - /// # .into_iter() - /// # .map(String::from) - /// # .collect(); - /// let mut opts = Parser::new(&args, "a"); - /// - /// assert_eq!(None, opts.next()); - /// assert_eq!("-", args[opts.index()]); - /// ``` - /// - /// ## "--" - /// ``` - /// use getopt::Parser; - /// - /// // args = ["program", "--", "-a"]; - /// # let args: Vec = vec!["program", "--", "-a"] - /// # .into_iter() - /// # .map(String::from) - /// # .collect(); - /// let mut opts = Parser::new(&args, "a"); - /// - /// assert_eq!(None, opts.next()); - /// assert_eq!("-a", args[opts.index()]); - /// ``` - /// - /// ## Unexpected option: - /// ``` - /// use getopt::Parser; - /// - /// // args = ["program", "-b"]; - /// # let args: Vec = vec!["program", "-b"] - /// # .into_iter() - /// # .map(String::from) - /// # .collect(); - /// let mut opts = Parser::new(&args, "a"); - /// - /// assert_eq!( - /// "unknown option -- 'b'".to_string(), - /// opts.next().unwrap().unwrap_err().to_string() - /// ); - /// ``` - /// - /// ## Missing argument: - /// ``` - /// use getopt::Parser; - /// - /// // args = ["program", "-a"]; - /// # let args: Vec = vec!["program", "-a"] - /// # .into_iter() - /// # .map(String::from) - /// # .collect(); - /// let mut opts = Parser::new(&args, "a:"); - /// - /// assert_eq!( - /// "option requires an argument -- 'a'".to_string(), - /// opts.next().unwrap().unwrap_err().to_string() - /// ); - /// ``` - fn next(&mut self) -> Option> { - if self.point == 0 { - /* - * Rationale excerpts below taken verbatim from "The Open Group Base - * Specifications Issue 7, 2018 edition", IEEE Std 1003.1-2017 - * (Revision of IEEE Std 1003.1-2008). - * Copyright © 2001-2018 IEEE and The Open Group. - */ - - /* - * If, when getopt() is called: - * argv[optind] is a null pointer - * *argv[optind] is not the character '-' - * argv[optind] points to the string "-" - * getopt() shall return -1 without changing optind. - */ - if self.index >= self.args.len() - || self.args[self.index].is_empty() - || self.args[self.index][0] != '-' - || self.args[self.index].len() == 1 - { - return None; - } - - /* - * If: - * argv[optind] points to the string "--" - * getopt() shall return -1 after incrementing index. - */ - if self.args[self.index][1] == '-' && self.args[self.index].len() == 2 { - self.incr_index(); - return None; - } - - // move past the starting '-' - self.point += 1; - } - - let opt = self.args[self.index][self.point]; - self.point += 1; - - match self.opts.get(&opt) { - None => { - if self.point >= self.args[self.index].len() { - self.incr_index(); - } - Some(Err(Error::new(ErrorKind::UnknownOption, opt))) - } - Some(false) => { - if self.point >= self.args[self.index].len() { - self.incr_index(); - } - - Some(Ok(Opt(opt, None))) - } - Some(true) => { - let arg: String = if self.point >= self.args[self.index].len() { - self.incr_index(); - if self.index >= self.args.len() { - return Some(Err(Error::new( - ErrorKind::MissingArgument, - opt, - ))); - } - self.args[self.index].iter().collect() - } else { - self.args[self.index] - .clone() - .split_off(self.point) - .iter() - .collect() - }; - - self.incr_index(); - - Some(Ok(Opt(opt, Some(arg)))) - } - } - } -} diff --git a/src/getopt-rs/result.rs b/src/getopt-rs/result.rs deleted file mode 100644 index 015a402..0000000 --- a/src/getopt-rs/result.rs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2023 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/. - * - * This file incorporates work covered by the following copyright and permission - * notice: - * The Clear BSD License - * - * Copyright © 2017-2023 David Wildasin - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted (subject to the limitations in the disclaimer - * below) provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions, and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions, and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED - * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -use std::result; - -use crate::error::Error; - -/// A specialized `Result` type for use with [`Parser`](struct.Parser.html) -pub type Result = result::Result; diff --git a/src/getopt-rs/tests.rs b/src/getopt-rs/tests.rs deleted file mode 100644 index c53d517..0000000 --- a/src/getopt-rs/tests.rs +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (c) 2023 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/. - * - * This file incorporates work covered by the following copyright and permission - * notice: - * The Clear BSD License - * - * Copyright © 2017-2023 David Wildasin - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted (subject to the limitations in the disclaimer - * below) provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions, and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions, and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED - * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -use crate::{Opt, Parser}; - -macro_rules! basic_test { - ($name:ident, $expect:expr, $next:expr, [$($arg:expr),+], $optstr:expr) => ( - #[test] - fn $name() -> Result<(), String> { - let expect: Option = $expect; - let args: Vec = vec![$($arg),+] - .into_iter() - .map(String::from) - .collect(); - let next: Option = $next; - let mut opts = Parser::new(&args, $optstr); - - match opts.next().transpose() { - Err(error) => { - return Err(format!("next() returned {:?}", error)) - }, - Ok(actual) => if actual != expect { - return Err( - format!("expected {:?}; got {:?}", expect, actual) - ) - }, - }; - - match next { - None => if opts.index() < args.len() { - return Err(format!( - "expected end of args; got {:?}", args[opts.index()] - )) - }, - Some(n) => if args[opts.index()] != n { - return Err(format!( - "next arg: expected {:?}; got {:?}", - n, - args[opts.index()] - )) - }, - }; - - Ok(()) - } - ) -} - -#[rustfmt::skip] basic_test!( - blank_arg, None, Some(String::new()), ["x", ""], "a" -); -#[rustfmt::skip] basic_test!( - double_dash, None, Some("-a".to_string()), ["x", "--", "-a", "foo"], "a" -); -#[rustfmt::skip] basic_test!(no_opts_1, None, None, ["x"], "a"); -#[rustfmt::skip] basic_test!( - no_opts_2, None, Some("foo".to_string()), ["x", "foo"], "a" -); -#[rustfmt::skip] basic_test!( - no_opts_3, None, Some("foo".to_string()), ["x", "foo", "-a"], "a" -); -#[rustfmt::skip] basic_test!( - single_dash, None, Some("-".to_string()), ["x", "-", "-a", "foo"], "a" -); -#[rustfmt::skip] basic_test!( - single_opt, - Some(Opt('a', None)), - Some("foo".to_string()), - ["x", "-a", "foo"], - "a" -); -#[rustfmt::skip] basic_test!( - single_optarg, - Some(Opt('a', Some("foo".to_string()))), - None, - ["x", "-a", "foo"], - "a:" -); - -macro_rules! error_test { - ($name:ident, $expect:expr, [$($arg:expr),+], $optstr:expr) => ( - #[test] - fn $name() -> Result<(), String> { - let expect: String = $expect.to_string(); - let args: Vec = vec![$($arg),+] - .into_iter() - .map(String::from) - .collect(); - let mut opts = Parser::new(&args, $optstr); - - match opts.next() { - None => { - return Err(format!( - "unexpected successful response: end of options" - )) - }, - Some(Err(actual)) => { - let actual = actual.to_string(); - - if actual != expect { - return Err( - format!("expected {:?}; got {:?}", expect, actual) - ); - } - }, - Some(Ok(opt)) => { - return Err( - format!("unexpected successful response: {:?}", opt) - ) - }, - }; - - Ok(()) - } - ) -} - -#[rustfmt::skip] error_test!( - bad_opt, - "unknown option -- 'b'", - ["x", "-b"], - "a" -); - -#[rustfmt::skip] error_test!( - missing_optarg, - "option requires an argument -- 'a'", - ["x", "-a"], - "a:" -); - -#[test] -fn multiple() -> Result<(), String> { - let args: Vec = vec!["x", "-abc", "-d", "foo", "-e", "bar"] - .into_iter() - .map(String::from) - .collect(); - let optstring = "ab:d:e".to_string(); - let mut opts = Parser::new(&args, &optstring); - - macro_rules! check_result { - ($expect:expr) => { - let expect: Option = $expect; - match opts.next().transpose() { - Err(error) => { - return Err(format!("next() returned {:?}", error)); - }, - Ok(actual) => { - if actual != expect { - return Err( - format!("expected {:?}; got {:?}", expect, actual) - ); - } - } - }; - }; - } - - check_result!(Some(Opt('a', None))); - check_result!(Some(Opt('b', Some("c".to_string())))); - check_result!(Some(Opt('d', Some("foo".to_string())))); - check_result!(Some(Opt('e', None))); - check_result!(None); - - Ok(()) -} - -#[test] -fn continue_after_error() { - let args: Vec = vec!["x", "-z", "-abc"] - .into_iter() - .map(String::from) - .collect(); - let optstring = "ab:d:e".to_string(); - for _opt in Parser::new(&args, &optstring) { - // do nothing, should not panic - } -} diff --git a/src/hru.rs b/src/hru.rs index b7937f7..c6009a7 100644 --- a/src/hru.rs +++ b/src/hru.rs @@ -27,42 +27,51 @@ extern crate strerror; extern crate sysexits; use strerror::StrError; -use sysexits::{ EX_DATAERR, EX_IOERR, EX_SOFTWARE }; +use sysexits::{ EX_DATAERR, EX_IOERR, EX_SOFTWARE, EX_USAGE }; +#[cfg(target_os="openbsd")] use sysexits::EX_OSERR; +#[cfg(target_os="openbsd")] extern crate openbsd; +#[cfg(target_os="openbsd")] use openbsd::{ Promises, pledge }; + +/* list of SI prefixes */ const LIST: [(u32, &str); 10] = [ - (3, "k"), - (6, "M"), - (9, "G"), - (12, "T"), - (15, "P"), - (18, "E"), - (21, "Z"), - (24, "Y"), - (27, "R"), - (30, "Q") + (3, "k"), /* kilo */ + (6, "M"), /* mega */ + (9, "G"), /* giga */ + (12, "T"), /* tera */ + (15, "P"), /* peta */ + (18, "E"), /* exa */ + (21, "Z"), /* zetta */ + (24, "Y"), /* yotta */ + (27, "R"), /* ronna */ + (30, "Q"), /* quetta */ ]; fn convert(input: u128) -> Result<(f64, (u32, &'static str)), String> { + /* preserve decimal places in output by casting to a float */ + let mut out = (input as f64, (0_u32, "")); - let mut out = (input as f64, (0_u32, "")); - if input < 1000 { return Ok(out); } + if input < 1000 { return Ok(out); } /* too low to convert */ for (n, p) in LIST { let c = match 10_u128.checked_pow(n) { Some(c) => c, - None => { + None => { /* too big for the laws of computing :( */ return Err(format!("10^{}: Integer overflow", n.to_string())); }, }; match c.cmp(&input) { - Ordering::Less => { + Ordering::Less => { /* c < input */ + /* the program will keep assigning out every loop until either + * the list runs out of higher prefix bases or the input is + * greater than the prefix base */ out = (input as f64 / c as f64, (n, p)); }, - Ordering::Equal => { + Ordering::Equal => { /* c == input */ return Ok((input as f64 / c as f64, (n, p))); }, - Ordering::Greater => {}, + Ordering::Greater => {}, /* c > input */ }; } @@ -71,7 +80,22 @@ fn convert(input: u128) -> Result<(f64, (u32, &'static str)), String> { fn main() -> ExitCode { let argv = args().collect::>(); + + if let Some(_) = argv.get(1) { + eprintln!("Usage: {}", argv[0]); + return ExitCode::from(EX_USAGE as u8); + } + + if cfg!(target_os="openbsd") { + let promises = Promises::new("stdio"); + if let Err(e) = pledge(Some(promises), None) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + let mut buf = String::new(); + while let Ok(_) = stdin().read_line(&mut buf) { if buf.is_empty() { return ExitCode::SUCCESS; } @@ -96,6 +120,7 @@ fn main() -> ExitCode { let si_prefix = format!("{}B", prefix.1); + /* round output number to one decimal place */ let out = ((number * 10.0).round() / 10.0).to_string(); stdout().write_all(format!("{} {}\n", out, si_prefix).as_bytes()) diff --git a/src/intcmp.c b/src/intcmp.c deleted file mode 100644 index 408474b..0000000 --- a/src/intcmp.c +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2023 DTB - * 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 /* errno */ -#include /* fprintf(3), stderr */ -#include /* strtol(3), size_t, EXIT_FAILURE */ -#include /* getopt(3), optind */ -#include - -/* 0b00? */ /* Equal | -e | 0b001 | 1 */ -#define EQUAL 0x01 /* Greater | -g | 0b010 | 2 */ -/* 0b0?0 */ /* Greater or Equal | -ge | 0b011 | 3 */ -#define GREATER 0x02 /* Less | -l | 0b100 | 4 */ -/* 0b?00 */ /* Less or Equal | -le | 0b101 | 5 */ -#define LESS 0x04 /* Inequal (Greater or Less) | -gl | 0b110 | 6 */ - -static char *program_name = "intcmp"; - -int main(int argc, char *argv[]){ - int c; - size_t i; - unsigned char mode; - int r; /* reference integer */ - - mode = 0; - - if(argc < 3) - goto usage; - - while((c = getopt(argc, argv, "egl")) != -1) - switch(c){ - case 'e': mode |= EQUAL; break; - case 'g': mode |= GREATER; break; - case 'l': mode |= LESS; break; - default: goto usage; - } - - if(optind + 2 /* ref cmp */ > argc){ -usage: fprintf(stderr, - "Usage: %s (-eghl) [integer] [integer...]\n", - argv[0] == NULL ? program_name : argv[0]); - return EX_USAGE; - } - - i = optind; - - do{ r = c; - c = strtol(argv[i], &argv[i], 10); - if(*argv[i] != '\0' || errno != 0){ - fprintf(stderr, "%s: argument #%d: Invalid integer\n", - argv[0], (int)i); - return EX_USAGE; - } - - if(i == optind) - continue; - - /* rule enforcement; if a mode isn't permitted and the numbers - * correspond to it, return 1 */ - if( (!(mode & EQUAL) && r == c) - || (!(mode & GREATER) && r > c) - || (!(mode & LESS) && r < c)) - return 1; - }while(++i < argc); - - return 0; -} diff --git a/src/intcmp.rs b/src/intcmp.rs new file mode 100644 index 0000000..b304ac0 --- /dev/null +++ b/src/intcmp.rs @@ -0,0 +1,98 @@ +/* + * 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/. + */ + +use std::{ + env::args, + process::ExitCode +}; + +extern crate getopt; +extern crate sysexits; + +use getopt::GetOpt; +use sysexits::EX_USAGE; + +#[cfg(target_os="openbsd")] use sysexits::EX_OSERR; +#[cfg(target_os="openbsd")] extern crate openbsd; +#[cfg(target_os="openbsd")] extern crate strerror; +#[cfg(target_os="openbsd")] use openbsd::{ Promises, pledge }; +#[cfg(target_os="openbsd")] use strerror::StrError; + + +fn usage(s: &str) -> ExitCode { + eprintln!("Usage: {} [-egl] integer integer...", s); + ExitCode::from(EX_USAGE as u8) +} + +fn main() -> ExitCode { + let argv = args().collect::>(); + + if cfg!(target_os="openbsd") { + let promises = Promises::new("stdio"); + if let Err(e) = pledge(Some(promises), None) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + + let mut e = false; /* args can be == */ + let mut g = false; /* args can be > */ + let mut l = false; /* args can be < */ + let mut optind = 0; + + if argv.len() < 3 { return usage(&argv[0]); } + + while let Some(opt) = argv.getopt("egl") { + match opt.opt() { + Ok("e") => e = true, + Ok("g") => g = true, + Ok("l") => l = true, + _ => return usage(&argv[0]), + } + optind = opt.ind(); + } + + if !e & !g & !l { return usage(&argv[0]); } + + if argv.len() - optind < 2 /* see usage */ { return usage(&argv[0]); } + + let mut prev: Option = None; /* no previous operand */ + let mut currn: usize; + + for arg in argv.iter().skip(optind) { /* iterate operands */ + match arg.parse::() { /* parse current operand */ + Ok(n) => currn = n, + Err(e) => { + eprintln!("{}: {}: {}", &argv[0], arg, e); + return ExitCode::from(EX_USAGE as u8); + } + } + + if let Some(prevn) = prev { /* if there was a previous opr., test */ + if (!e && prevn == currn) + || (!g && prevn > currn) + || (!l && prevn < currn) + { return ExitCode::FAILURE; } + } + + prev = Some(currn); /* there is a previous operand */ + } + + ExitCode::SUCCESS +} diff --git a/src/libgetopt.rs b/src/libgetopt.rs new file mode 100644 index 0000000..8064c81 --- /dev/null +++ b/src/libgetopt.rs @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2023–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::ffi::{ c_int, c_char, CString, CStr }; + +/* binding to getopt(3p) */ +extern "C" { + static mut optarg: *mut c_char; + static mut _opterr: c_int; + static mut optind: c_int; + static mut optopt: c_int; + + fn getopt( + ___argc: c_int, + ___argv: *const *mut c_char, + __shortopts: *const c_char, + ) -> c_int; +} + +#[derive(Clone, Debug)] +pub enum OptError { + MissingArg(String), + UnknownOpt(String), +} + +#[derive(Clone, Debug)] +pub struct Opt { + arg: Option, /* option argument */ + ind: *mut i32, /* option index */ + opt: Result, /* option option */ +} + +impl Opt { + pub fn arg(&self) -> Option { self.arg.clone() } + + /* sets optarg if default is desired */ + pub fn arg_or(&self, default: impl std::fmt::Display) -> String { + default.to_string() + } + + /* makes matching the output of this method more bearable */ + pub fn opt(&self) -> Result<&str, OptError> { + self.opt.as_ref().map(|o| o.as_str()).map_err(OptError::clone) + } + + /* From getopt(3p): + * + * The variable optind is the index of the next element of the argv[] + * vector to be processed. It shall be initialized to 1 by the system, and + * getopt() shall update it when it finishes with each element of argv[]. + * If the application sets optind to zero before calling getopt(), the + * behavior is unspecified. When an element of argv[] contains multiple + * option characters, it is unspecified how getopt() determines which + * options have already been processed. */ + pub fn ind(&self) -> usize { unsafe { *self.ind as usize } } + + /* this is patently terrible and is only happening because I’m stubborn */ + pub fn set_ind(&self, ind: i32) { unsafe { *self.ind = ind; } } +} + +/* function signature */ +pub trait GetOpt { + fn getopt(&self, optstring: &str) -> Option; +} + +impl GetOpt for Vec { + fn getopt(&self, optstring: &str) -> Option { + let c_strings: Vec<_> = self + .iter() + .cloned() + .map(|x| CString::new(x).unwrap().into_raw()) + .collect(); + + /* god knows what this does */ + let boxed = Box::into_raw(c_strings.into_boxed_slice()); + let argv = boxed as *const *mut c_char; + + /* operations are separated out so that everything lives long enough */ + let opts = CString::new(optstring).unwrap().into_raw(); + let len = self.len() as c_int; + + unsafe { + let ret = match getopt(len, argv, opts) { + /* From getopt(3p): + * + * The getopt() function shall return the next option character + * specified on the command line. + * + * A (':') shall be returned if getopt() detects a + * missing argument and the first character of optstring was a + * (':'). + * + * A ('?') shall be returned if getopt() + * encounters an option character not in optstring or detects a + * missing argument and the first character of optstring was not + * a (':'). + * + * Otherwise, getopt() shall return -1 when all command line + * options are parsed. */ + 58 => { /* ASCII ':' */ + Some(Opt { + arg: None, + ind: std::ptr::addr_of_mut!(optind), + /* error containing option */ + opt: Err(OptError::MissingArg(optopt.to_string())), + }) + }, + 63 => { /* ASCII '?' */ + Some(Opt { + arg: None, + ind: std::ptr::addr_of_mut!(optind), + /* error containing option */ + opt: Err(OptError::UnknownOpt(optopt.to_string())), + }) + }, + /* From getopt(3p): + * + * If, when getopt() is called: + * + * argv[optind] is a null pointer + * *argv[optind] is not the character - + * argv[optind] points to the string "-" + * + * getopt() shall return -1 without changing optind. If: + * + * argv[optind] points to the string "--" + * + * getopt() shall return -1 after incrementing optind. */ + -1 => return None, + opt => { + let arg: Option; + + if optarg.is_null() { arg = None; } + else { + arg = Some(CStr::from_ptr(optarg) + .to_string_lossy() + .into_owned()); + } + + Some(Opt { + arg, + ind: std::ptr::addr_of_mut!(optind), + /* I didn’t need to cast this before; I rewrote the + * pointer logic and now I do + * + * I don’t know why this is */ + opt: Ok((opt as u8 as char).to_string()), + }) + }, + }; + + /* delloc argv (something online said I should do this) */ + let _ = Box::from_raw(boxed); + return ret; + } + } +} + +/* tests (good) */ +#[cfg(test)] +mod tests { + use GetOpt; + + #[test] + fn testing() { + let argv: Vec = ["test", "-b", "-f", "arg", "-o", "arg"] + .iter() + .map(|s| s.to_string()) + .collect(); + + while let Some(opt) = argv.getopt(":abf:o:") { + match opt.opt() { + Ok("a") => assert_eq!(opt.ind(), 1), + Ok("b") => assert_eq!(opt.ind(), 2), + Ok("f") | Ok("o") => { + assert_eq!(opt.arg(), Some("arg".into())); + }, + _ => assert!(false), + }; + } + + if let Some(opt) = argv.getopt("abc:") { + opt.clone().set_ind(1); + assert_eq!(opt.ind(), 1); + } + } +} diff --git a/src/libopenbsd.rs b/src/libopenbsd.rs new file mode 100644 index 0000000..f3e60b9 --- /dev/null +++ b/src/libopenbsd.rs @@ -0,0 +1,97 @@ +/* + * 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::{ + ffi::{ CString, c_char }, + io::Error, + ptr::null, +}; + +mod openbsd { + use std::ffi::{ c_char, c_int }; + extern "C" { + pub fn pledge(arg1: *const c_char, arg2: *const c_char) -> c_int; + + pub fn unveil(arg1: *const c_char, arg2: *const c_char) -> c_int; + + pub fn __errno() -> *mut c_int; + } +} + +pub struct Promises(*const c_char); + +impl Promises { + pub fn new(promises: &str) -> Self { + let p = CString::new(promises).unwrap(); + + Promises(p.into_raw() as *const c_char) + } +} + +pub fn pledge( + promises: Option, execpromises: Option +) -> Result<(), Error> { + /* From pledge(2): + * + * Passing NULL to promises or execpromises specifies to not change + * the current value. */ + let arg1 = promises.unwrap_or(Promises(null())).0; + let arg2 = execpromises.unwrap_or(Promises(null())).0; + + unsafe { + match openbsd::pledge(arg1, arg2) { + -1 => Err(Error::from_raw_os_error(*openbsd::__errno())), + 0 => Ok(()), + _ => panic!(), /* unreachable */ + } + } +} + +pub struct UnveilPerms(CString); + +impl UnveilPerms { + pub fn new(permissions: Vec) -> Self { + if permissions.is_empty() { + return UnveilPerms(CString::new("").unwrap()); + } + + UnveilPerms( + CString::new(permissions.iter().collect::()).unwrap() + ) + } +} + +pub fn unveil( + path: Option<&str>, + permissions: Option, +) -> Result<(), Error> { + let path_c = path.map(CString::new).map(Result::unwrap); + let arg1 = path_c.map(|p| p.into_raw() as *const c_char).unwrap_or(null()); + + let arg2 = permissions + .map(|p| p.0.into_raw() as *const c_char) + .unwrap_or(null()); + + unsafe { + match openbsd::unveil(arg1, arg2) { + -1 => Err(Error::from_raw_os_error(*openbsd::__errno())), + 0 => Ok(()), + _ => panic!(), /* unreachable */ + } + } +} diff --git a/src/strerror.rs b/src/libstrerror.rs similarity index 100% rename from src/strerror.rs rename to src/libstrerror.rs diff --git a/src/mm.c b/src/mm.c deleted file mode 100644 index dc337b7..0000000 --- a/src/mm.c +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2024 DTB - * 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 /* errno */ -#include /* signal(2), SIG_ERR, SIG_IGN, SIGINT */ -#include /* fclose(3), fopen(3), fprintf(3), getc(3), putc(3), - * setvbuf(3), size_t, _IONBF, NULL */ -#include /* free(3), realloc(3) */ -#include /* strcmp(3), strerror(3) */ -#include /* getopt(3) */ -#if !defined EX_IOERR || !defined EX_OK || !defined EX_OSERR \ - || !defined EX_USAGE -# include -#endif -extern int errno; - -/* This structure is how open files are tracked. */ -struct Files{ - size_t a; /* allocation */ - size_t s; /* used size */ - char *mode; /* file opening mode */ - char **names; /* file names */ - FILE **files; /* file pointers */ -}; - -/* How much to grow the allocation when it's saturated. */ -#ifndef ALLOC_INCREMENT -# define ALLOC_INCREMENT 1 -#endif - -/* How much to grow the allocation at program start. */ -#ifndef ALLOC_INITIAL -# define ALLOC_INITIAL 10 -#endif - -/* pre-allocated strings */ -static char *program_name = ""; -static char *stdin_name = ""; -static char *stdout_name = ""; -static char *stderr_name = ""; -static char *(fmode[]) = { (char []){"rb"}, (char []){"rb+"} }; -static char *wharsh = "wb"; - -/* Adds the open FILE pointer for the file at the path s to the files struct, - * returning the FILE if successful and NULL if not, allocating more memory in - * the files buffers as needed. */ -static FILE * -Files_append(struct Files *files, FILE *file, char *name){ - - if(file == NULL || (files->s == files->a - && ((files->files = realloc(files->files, - (files->a += (files->a == 0) - ? ALLOC_INITIAL - : ALLOC_INCREMENT) - * sizeof *(files->files))) == NULL - || (files->names = realloc(files->names, - files->a * sizeof *(files->names))) == NULL))) - return NULL; - - files->names[files->s] = name; - return files->files[files->s++] = file; -} - -/* Opens the file at the path p and puts it in the files struct, returning NULL - * if either the opening or the placement of the open FILE pointer fail. */ -#define Files_open(files, p) \ - Files_append((files), fopen((p), (files)->mode), (p)) - -/* Prints a diagnostic message based on errno and returns an exit status - * appropriate for an OS error. */ -static int -oserr(char *s, char *r){ - - fprintf(stderr, "%s: %s: %s\n", s, r, strerror(errno)); - - return EX_OSERR; -} - -/* Hijacks i and j from main and destructs the files[2] struct used by main by - * closing its files and freeing its files and names arrays, returning retval - * from main. */ -#define terminate \ - for(i = 0; i < 2; ++i){ \ - for(j = 0; j < files[i].s; ++j) \ - if(files[i].files[j] != stdin \ - && files[i].files[j] != stdout \ - && files[i].files[j] != stderr) \ - fclose(files[i].files[j]); \ - free(files[i].files); \ - free(files[i].names); \ - } \ - return retval - -/* Prints a usage text, in which s is the program being run (i.e. argv[0]), and - * returns an exit status appropriate for a usage error. */ -int usage(char *s){ - - fprintf(stderr, "Usage: %s (-aenu) (-i [input])... (-o [output])...\n", s); - - return EX_USAGE; -} - -int main(int argc, char *argv[]){ - int c; - struct Files files[2]; /* {read, write} */ - size_t i; - size_t j; - size_t k; /* loop index but also unbuffer status */ - int retval; - - /* Initializes the files structs with their default values, standard - * input and standard output. If an input or an output is specified - * these initial values will be overwritten, so to, say, use mm(1) - * equivalently to tee(1p), -o - will need to be specified before - * additional files to ensure standard output is still written. */ - for(i = 0; i < 2; ++i){ - files[i].a = 0; - files[i].s = 0; - files[i].mode = fmode[i]; - files[i].files = NULL; - files[i].names = NULL; - Files_append(&files[i], i == 0 ? stdin : stdout, - i == 0 ? stdin_name : stdout_name); - files[i].s = 0; - } - - k = 0; - - if(argc > 0) - program_name = argv[0]; - - if(argc > 1) - while((c = getopt(argc, argv, "aehi:no:u")) != -1) - switch(c){ - case 'a': /* "rb+" -> "ab" */ - files[1].mode[0] = 'a'; - files[1].mode[2] = '\0'; - break; - case 'e': - if(Files_append(&files[1], stderr, stderr_name) != NULL) - break; - retval = oserr(argv[0], "-e"); - terminate; - case 'i': - if((strcmp(optarg, "-") == 0 && Files_append(&files[0], - stdin, stdin_name) != NULL) - || Files_open(&files[0], optarg) != NULL) - break; - retval = oserr(argv[0], optarg); - terminate; - case 'o': - if((strcmp(optarg, "-") == 0 && Files_append(&files[1], - stdout, stdout_name) != NULL) - || Files_open(&files[1], optarg) != NULL) - break; - /* does not exist, so try to create it */ - if(errno == ENOENT){ - files[1].mode = wharsh; - if(Files_open(&files[1], optarg) != NULL){ - files[1].mode = fmode[1]; - break; - } - } - retval = oserr(argv[0], optarg); - terminate; - case 'n': - if(signal(SIGINT, SIG_IGN) != SIG_ERR) - break; - retval = oserr(argv[0], "-n"); - terminate; - case 'u': - k = 1; - break; - default: - retval = usage(argv[0]); - terminate; - } - - if(optind != argc){ - retval = usage(argv[0]); - terminate; - } - - files[0].s += files[0].s == 0; - files[1].s += files[1].s == 0; - - /* Unbuffer files. */ - if(k){ - for(i = 0; - i < files[0].s; - setvbuf(files[0].files[i++], NULL, _IONBF, 0)); - for(i = 0; - i < files[1].s; - setvbuf(files[1].files[i++], NULL, _IONBF, 0)); - } - - retval = EX_OK; - - /* Actual program loop. */ - for(i = 0; i < files[0].s; ++i) /* iterate ins */ - while((c = getc(files[0].files[i])) != EOF) /* iterate chars */ - for(j = 0; j < files[1].s; ++j) /* iterate outs */ - if(putc(c, files[1].files[j]) == EOF){ - /* notebook's full */ - retval = EX_IOERR; - fprintf(stderr, "%s: %s: %s\n", - program_name, files[1].names[j], strerror(errno)); - if(fclose(files[1].files[j]) == EOF) - fprintf(stderr, "%s: %s: %s\n", - program_name, files[1].names[j], strerror(errno)); - /* massage out the tense muscle */ - for(k = j--; k < files[1].s - 1; ++k){ - files[1].files[k] = files[1].files[k+1]; - files[1].names[k] = files[1].names[k+1]; - } - if(--files[1].s == 0) - terminate; - } - - terminate; -} diff --git a/src/mm.rs b/src/mm.rs new file mode 100644 index 0000000..b99bf9e --- /dev/null +++ b/src/mm.rs @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2024 Emma Tebibyte + * Copyright (c) 2024 DTB + * 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::File, + io::{ stdin, stdout, stderr, BufWriter, Read, Write }, + os::fd::{ AsRawFd, FromRawFd }, + process::{ exit, ExitCode }, +}; + +extern crate getopt; +extern crate strerror; +extern crate sysexits; + +use getopt::GetOpt; +use strerror::StrError; +use sysexits::{ EX_IOERR, EX_USAGE }; + +#[cfg(target_os="openbsd")] use sysexits::EX_OSERR; +#[cfg(target_os="openbsd")] extern crate openbsd; +#[cfg(target_os="openbsd")] +use openbsd::{ + Promises, + UnveilPerms, + pledge, + unveil, +}; + +use ArgMode::*; + +enum ArgMode { In, Out } + +fn main() -> ExitCode { + let argv = args().collect::>(); + let usage = format!("Usage: {} [-aetu] [-i input] [-o output]", argv[0]); + + if cfg!(target_os="openbsd") { + let promises = Promises::new("rpath stdio unveil"); + if let Err(e) = pledge(Some(promises), None) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + + let mut a = false; /* append to the file */ + let mut e = false; /* use stderr as an output */ + let mut t = true; /* do not truncate the file before writing */ + let mut u = false; /* unbuffer i/o */ + let mut ins = Vec::new(); /* initial input file path vector */ + let mut outs = Vec::new(); /* initial output file path vector */ + let mut mode: Option = None; /* mode set by last-used option */ + let mut optind = 0; + + while let Some(opt) = argv.getopt("aei:o:tu") { + match opt.opt() { + Ok("a") => a = true, + Ok("e") => e = true, + Ok("u") => u = true, + Ok("t") => t = false, + Ok("i") => { /* add inputs */ + let input = opt.arg().unwrap(); + + if cfg!(target_os="openbsd") { + let perms = UnveilPerms::new(vec!['r']); + if let Err(e) = unveil(Some(&input), Some(perms)) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + + ins.push(input); + mode = Some(In); /* latest argument == -i */ + }, + Ok("o") => { /* add output */ + let output = opt.arg().unwrap(); + + if cfg!(target_os="openbsd") { + let perms = UnveilPerms::new(vec!['w', 'c']); + if let Err(e) = unveil(Some(&output), Some(perms)) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + + outs.push(output); + mode = Some(Out); /* latest argument == -o */ + }, + Err(_) | Ok(_) => { + eprintln!("{}", usage); + return ExitCode::from(EX_USAGE as u8); + }, + }; + + optind = opt.ind(); + } + + let remaining = argv.iter().skip(optind); + + /* check the last flag specified */ + if let Some(m) = mode { + for arg in remaining { + /* move the subsequent arguments to the list of inputs or outputs */ + match m { + In => ins.push(arg.to_string()), + Out => outs.push(arg.to_string()), + }; + } + } + + if cfg!(target_os="openbsd") { + if let Err(e) = unveil(None, None) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + + if ins.is_empty() && outs.is_empty() && argv.len() > optind { + eprintln!("Usage: {}", usage); + return ExitCode::from(EX_USAGE as u8); + } + + /* use stdin if no inputs are specified */ + if ins.is_empty() { ins.push("-".to_string()); } + + /* use stdout if no outputs are specified */ + if outs.is_empty() && !e { outs.push("-".to_string()); } + + /* map all path strings to files */ + let inputs = ins.iter().map(|file| { + /* if a file is “-”, it is stdin */ + if *file == "-" { + /* portable way to access stdin as a file */ + return unsafe { File::from_raw_fd(stdin().as_raw_fd()) }; + } + + match File::open(file) { + Ok(f) => f, + Err(e) => { + eprintln!("{}: {}: {}", argv[0], file, e.strerror()); + exit(EX_IOERR); + }, + } + }).collect::>(); + + /* map all path strings to files */ + let mut outputs = outs.iter().map(|file| { + /* of a file is “-”, it is stdout */ + if *file == "-" { + /* portable way to access stdout as a file */ + return unsafe { File::from_raw_fd(stdout().as_raw_fd()) }; + } + + let options = File::options() + /* don’t truncate if -t is specified, append if -a is specified */ + .truncate(t) + .append(a) + /* enable the ability to create and write to files */ + .create(true) + .write(true) + /* finally, open the file! */ + .open(file); + + match options { + Ok(f) => return f, + Err(e) => { + eprintln!("{}: {}: {}", argv[0], file, e.strerror()); + exit(EX_IOERR); + }, + }; + }).collect::>(); + + /* if -e is specified, use stderr */ + if e { + /* portable way to access stderr as a file */ + outputs.push(unsafe { File::from_raw_fd(stderr().as_raw_fd()) }); + } + + let mut outputs = outputs.iter().map(|o| { + if u { + /* unbuffered writing through a buffer of capacity 0 */ + BufWriter::with_capacity(0, o) + } else { + /* theoretically buffered writing */ + BufWriter::new(o) + } + }).collect::>(); + + for file in inputs { + for byte in file.bytes().map(|b| { + b.unwrap_or_else(|e| { + eprintln!("{}: {}", argv[0], e.strerror()); + exit(EX_IOERR); + }) + }) { + for out in &mut outputs { + if let Err(e) = out.write(&[byte]) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_IOERR as u8); + } + + if u { + /* immediately flush the output for -u */ + if let Err(e) = out.flush() { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_IOERR as u8); + } + } + } + } + } + + ExitCode::SUCCESS +} diff --git a/src/npc.c b/src/npc.c index 8b97180..cd488e5 100644 --- a/src/npc.c +++ b/src/npc.c @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 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 @@ -16,46 +17,72 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -#include /* fprintf(3), fputs(3), getc(3), putc(3), stdin, stdout, - * EOF */ -#include /* EXIT_FAILURE, EXIT_SUCCESS */ -#include /* getopt(3) */ -#include +#include /* fprintf(3), fputs(3), getc(3), perror(3), putc(3), stdin, + * stdout, EOF */ +#include /* EX_IOERR, EX_OK, EX_OSERR, EX_USAGE */ +#include /* pledge(2), getopt(3) */ -int main(int argc, char *argv[]){ +char *program_name = "npc"; + +static int +ioerr(char *argv0) { + perror(argv0); + + return EX_IOERR; +} + +static int +usage(char *argv0) { + (void)fprintf(stderr, "Usage: %s [-et]\n", argv0); + + return EX_USAGE; +} + +int main(int argc, char *argv[]) { int c; - char showend; - char showtab; + char showend = 0; /* print a dollar sign before each newline */ + char showtab = 0; /* prints tab characters in caret notation */ - showend = 0; - showtab = 0; +#ifdef __OpenBSD__ + if (pledge("stdio", NULL) == -1) { + perror(argv[0] == NULL ? program_name : argv[0]); + return EX_OSERR; + } +#endif - if(argc > 0) - while((c = getopt(argc, argv, "et")) != -1) - switch(c){ - case 'e': showend = 1; break; - case 't': showtab = 1; break; - default: goto usage; + if (argc > 0) { + program_name = argv[0]; + + while ((c = getopt(argc, argv, "et")) != -1) { + switch (c){ + case 'e': showend = 1; break; + case 't': showtab = 1; break; + default: return usage(program_name); } - - if(argc > optind){ -usage: fprintf(stderr, "Usage: %s (-eht)\n", argv[0]); - return EX_USAGE; + } } - while((c = getc(stdin)) != EOF){ - if((c & 0x80) != 0) - fputs("M-", stdout); - switch(c ^ 0x80 /* 0b 1000 0000 */){ - case 0x7f: fputs("^?", stdout); + if (argc > optind) { return usage(program_name); } + + while ((c = getc(stdin)) != EOF) { + if ((c & 0x80) != 0 && fputs("M-", stdout) == EOF) { + return ioerr(argv[0]); + } + + switch (c ^ 0x80 /* 0b 1000 0000 */) { + case 0x7f: /* ASCII DEL (127d) */ + if(fputs("^?", stdout) == EOF) { return ioerr(argv[0]); } break; - case '\n': if(showend) - putc('$', stdout); - default: - if(c >= ' ' || c == '\n' || (!showtab && c == '\t')) - putc(c, stdout); - else - fprintf(stdout, "^%c", c + '@'); + case '\n': + if (showend && fputc('$', stdout) == EOF) { + return ioerr(argv[0]); + } + default: + if (c >= ' ' || c == '\n' || (!showtab && c == '\t')) { + if (fputc(c, stdout) == EOF) { return ioerr(argv[0]); } + } else if (fprintf(stdout, "^%c", c + '@') < 0) { + return ioerr(argv[0]); + } } } diff --git a/src/rpn.rs b/src/rpn.rs index 2bfbbf5..e4f725d 100644 --- a/src/rpn.rs +++ b/src/rpn.rs @@ -56,8 +56,14 @@ extern crate sysexits; use sysexits::EX_DATAERR; +#[cfg(target_os="openbsd")] use sysexits::EX_OSERR; +#[cfg(target_os="openbsd")] extern crate strerror; +#[cfg(target_os="openbsd")] extern crate openbsd; +#[cfg(target_os="openbsd")] use strerror::StrError; +#[cfg(target_os="openbsd")] use openbsd::{ Promises, pledge }; + #[derive(Clone, PartialEq, PartialOrd, Debug)] -// enum CalcType is a type containing operations used in the calculator +/* enum CalcType is a type containing operations used in the calculator */ enum CalcType { Add, Subtract, @@ -117,8 +123,8 @@ struct EvaluationError { code: i32, } -// I’m no math nerd but I want the highest possible approximation of 0.9 -// repeating and it seems this can give it to me +/* I’m no math nerd but I want the highest possible approximation of 0.9 + * repeating and it seems this can give it to me */ const PRECISION_MOD: f64 = 0.9 + f64::EPSILON * 100.0; fn eval( @@ -133,7 +139,7 @@ fn eval( return Ok((stack, oper)); } - // Split the input into tokens. + /* Split the input into tokens. */ let mut toks: VecDeque = input .split_whitespace() .rev() @@ -183,7 +189,7 @@ fn eval( Ok((stack, oper)) } -// Round a float to the given precision level +/* Round a float to the given precision level */ fn round_precise(value: &f64, precision: usize) -> f64 { let multiplier = 10_f64.powi(precision as i32); (value * multiplier).round() / multiplier @@ -191,13 +197,22 @@ fn round_precise(value: &f64, precision: usize) -> f64 { fn main() -> ExitCode { let argv = args().collect::>(); + + if cfg!(target_os="openbsd") { + let promises = Promises::new("stdio"); + if let Err(e) = pledge(Some(promises), None) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + let mut stack = VecDeque::new(); let mut buf = String::new(); - // Set floating-point precision for correcting rounding errors based on - // machine epsilon + /* Set floating-point precision for correcting rounding errors based on + * machine epsilon */ let precision = (-f64::EPSILON.log10() * PRECISION_MOD).ceil() as usize; - if argv.get(1).is_none() { + if argv.get(1).is_none() { /* read from stdin */ while let Ok(_) = stdin().read_line(&mut buf) { match eval(&buf.trim(), stack) { Ok(s) => { @@ -219,12 +234,13 @@ fn main() -> ExitCode { }, }; } - } else { + } else { /* read from argv */ + /* join argv into an owned String joined by spaces minus argv[0] */ let input = argv .iter() .skip(1) .map(|x| x.to_owned()) - .collect::>() + .collect::>() .join(" "); match eval(&input, stack) { @@ -233,7 +249,7 @@ fn main() -> ExitCode { let val = match stack.iter().last() { Some(v) => v, - None => return ExitCode::from(0), + None => return ExitCode::SUCCESS, }; println!("{}", round_precise(val, precision).to_string()) @@ -244,5 +260,5 @@ fn main() -> ExitCode { }, }; } - ExitCode::from(0) + ExitCode::SUCCESS } diff --git a/src/scrut.c b/src/scrut.c index c5b675f..cfdd923 100644 --- a/src/scrut.c +++ b/src/scrut.c @@ -17,91 +17,100 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +#include /* assert(3) */ #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 /* EX_OSERR, EX_USAGE */ +#include /* access(3), getopt(3), pledge(2), unveil(2), 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"; +char *program_name = "scrut"; +#define OPTS "bcdefgkprsuwxLS" +/* this is an array so main:sel's size can be known at compile time */ +static char opts[] = OPTS; -int main(int argc, char *argv[]){ - struct stat buf; - int c; - size_t i; - char *p; +static int +usage(char *argv0) { + (void)fprintf(stderr, "Usage: %s [-" OPTS "] file...\n", argv0); - if(argc < 2) - goto usage; + return EX_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'; +int main(int argc, char *argv[]) { + char sel[(sizeof opts) / (sizeof *opts)]; + + program_name = argv[0] == NULL ? program_name : argv[0]; + +#ifdef __OpenBSD__ + if (pledge("rpath stdio unveil", NULL) == -1) { + perror(program_name); + return EX_OSERR; + } +#endif + + if (argc < 2) { return usage(program_name); } + + { /* option parsing */ + char *p; + + memset(sel, '\0', sizeof sel); + for (int c; (c = getopt(argc, argv, opts)) != -1;) { + if ((p = strchr(opts, c)) == NULL) { return usage(argv[0]); } + else { + assert(p - opts < sizeof sel / sizeof *sel); /* bounds check */ + sel[p - opts] = c; + } } - if(optind == argc) - goto usage; + /* straighten out selections; permute out nulls */ + p = sel; + for (size_t i = 0; i < (sizeof sel) / (sizeof *sel); ++i) { + if (sel[i] != '\0') { + *p = sel[i]; + if (&sel[i] != p++) { sel[i] = '\0'; } + } + } + } - argv += optind; - do{ if(access(*argv, F_OK) != 0 || lstat(*argv, &buf) == -1) + if (optind == argc) { return usage(argv[0]); } + + for (argv += optind ; *argv != NULL; argv = &argv[1]) { + struct stat buf; + +#ifdef __OpenBSD__ + if (unveil(*argv, "r") == -1) { + perror(program_name); + return EX_OSERR; + } +#endif + + 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); + for (size_t i = 0; sel[i] != '\0'; ++i) { + if ( + (sel[i] == 'b' && !S_ISBLK(buf.st_mode)) + || (sel[i] == 'c' && !S_ISCHR(buf.st_mode)) + || (sel[i] == 'd' && !S_ISDIR(buf.st_mode)) + || (sel[i] == 'e' && 0) + || (sel[i] == 'f' && !S_ISREG(buf.st_mode)) + || (sel[i] == 'g' && !(buf.st_mode & S_ISGID)) + || (sel[i] == 'k' && !(buf.st_mode & S_ISVTX)) + || (sel[i] == 'p' && !S_ISFIFO(buf.st_mode)) + || (sel[i] == 'r' && access(*argv, R_OK) != 0) + || (sel[i] == 'u' && !(buf.st_mode & S_ISUID)) + || (sel[i] == 'w' && access(*argv, W_OK) != 0) + || (sel[i] == 'x' && access(*argv, X_OK) != 0) + || (sel[i] == 'L' && !S_ISLNK(buf.st_mode)) + || (sel[i] == 'S' && !S_ISSOCK(buf.st_mode)) + ) { return EXIT_FAILURE; } + } + } return EXIT_SUCCESS; } diff --git a/src/strcmp.c b/src/strcmp.c index acb4946..6d930d5 100644 --- a/src/strcmp.c +++ b/src/strcmp.c @@ -1,24 +1,62 @@ -#include /* fprintf(3), stderr */ -#include /* EXIT_FAILURE */ -#include +/* + * Copyright (c) 2023 DTB + * Copyright (c) 2023–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), perror(3), stderr */ +#include /* EX_OK, EX_OSERR, EX_USAGE */ -static char *program_name = "strcmp"; +#ifdef __OpenBSD__ +# include /* pledge(2) */ +#endif -int main(int argc, char *argv[]){ - int i; +char *program_name = "strcmp"; + +int main(int argc, char *argv[]) { + unsigned int i; + +#ifdef __OpenBSD__ + if (pledge("stdio", NULL) == -1) { + perror(argv[0] == NULL ? program_name : argv[0]); + + return EX_OSERR; + } +#endif + + if (argc < 3) { + (void)fprintf( + stderr, + "Usage: %s string string...\n", + argv[0] == NULL ? program_name : argv[0] + ); - if(argc < 3){ - fprintf(stderr, "Usage: %s [string] [string...]\n", - argv[0] == NULL ? program_name : argv[0]); return EX_USAGE; } - for(; *argv[1] != '\0'; ++argv[1]) - for(i = 2; i < argc; ++i) - if(*argv[i-1] > *argv[i]) + for (; *argv[1] != '\0'; ++argv[1]) { + for (i = 2; i < argc; ++i) { + /* a former string has a greater byte value */ + if (*argv[i-1] > *argv[i]) { return 1; - else if(*argv[i-1] < *argv[i]++) + /* a latter string has a greater byte value */ + } else if (*argv[i-1] < *argv[i]++) { return -1; /* actually 255 */ + } + } + } - return 0; + return EX_OK; } diff --git a/src/swab.rs b/src/swab.rs index ca944d9..27ce9a5 100644 --- a/src/swab.rs +++ b/src/swab.rs @@ -1,5 +1,6 @@ /* * 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 @@ -18,72 +19,90 @@ use std::{ env::args, - io::{ stdin, stdout, Error, ErrorKind, Read, Write }, + io::{ stdin, stdout, Error, Read, Write }, process::ExitCode, vec::Vec }; extern crate getopt; -use getopt::{ Opt, Parser }; - extern crate sysexits; -use sysexits::{ EX_OK, EX_OSERR, EX_USAGE }; +extern crate strerror; -fn oserr(s: &str, e: Error) -> ExitCode { - eprintln!("{}: {}", s, e); +use getopt::GetOpt; +use sysexits::{ EX_IOERR, EX_OK, EX_OSERR, EX_USAGE }; +use strerror::StrError; + +#[cfg(target_os="openbsd")] extern crate openbsd; +#[cfg(target_os="openbsd")] use openbsd::{ Promises, pledge }; + + +fn oserr(argv0: &str, e: Error) -> ExitCode { + eprintln!("{}: {}", argv0, e.strerror()); ExitCode::from(EX_OSERR as u8) } +fn ioerr(argv0: &str, e: Error) -> ExitCode { + eprintln!("{}: {}", argv0, e.strerror()); + ExitCode::from(EX_IOERR as u8) +} + fn usage(s: &str) -> ExitCode { - eprintln!("Usage: {} (-f) (-w [wordsize])", s); + eprintln!("Usage: {} [-w word_size]", s); ExitCode::from(EX_USAGE as u8) } fn main() -> ExitCode { let argv = args().collect::>(); - let mut buf: Vec = Vec::new(); + + if cfg!(target_os="openbsd") { + let promises = Promises::new("stdio"); + if let Err(e) = pledge(Some(promises), None) { + return oserr(&argv[0], e); + } + } + + let mut buf: Vec = Vec::new(); // holds the sequence getting swabbed let mut input = stdin(); let mut output = stdout().lock(); + let mut optind: usize = 1; // argv[0] + let mut wordsize: usize = 2; // default; mimics dd(1p) conv=swab - let mut opts = Parser::new(&argv, "fw:"); - let mut force = false; - let mut wordsize: usize = 2; - - loop { - match opts.next() { - None => break, - Some(opt) => - match opt { - Ok(Opt('f', None)) => force = true, - Ok(Opt('w', Some(arg))) => { - match arg.parse::() { - Ok(w) if w % 2 == 0 => { wordsize = w; () }, - _ => { return usage(&argv[0]); }, - } - }, - _ => { return usage(&argv[0]); } - } + while let Some(opt) = argv.getopt("w:") { + match opt.opt() { + Ok("w") => { + match opt.arg().unwrap().parse::() { + Ok(w) if w % 2 == 0 => { wordsize = w; }, + _ => { return usage(&argv[0]); }, + } + optind = opt.ind(); + }, + _ => { return usage(&argv[0]); } } } + if optind < argv.len() { + return usage(&argv[0]); + } + buf.resize(wordsize, 0); loop { match input.read(&mut buf) { - Ok(0) => break ExitCode::from(EX_OK as u8), - Ok(v) if v == wordsize => { + Ok(0) => break ExitCode::from(EX_OK as u8), // read nothing; bye + Ok(v) if v == wordsize => { // read full block; swab let (left, right) = buf.split_at(v/2); + if let Err(e) = output.write(&right) .and_then(|_| output.write(&left)) { - break oserr(&argv[0], e) + break ioerr(&argv[0], e); } + }, - Ok(v) => { + Ok(v) => { // partial read; partially write if let Err(e) = output.write(&buf[..v]) { - break oserr(&argv[0], e) + break ioerr(&argv[0], e); } }, - Err(e) if e.kind() == ErrorKind::Interrupted && force => continue, Err(e) => break oserr(&argv[0], e) } } diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index 4a602b1..0000000 --- a/src/test.rs +++ /dev/null @@ -1,10 +0,0 @@ -extern crate strerror; - -use strerror::raw_message; - -fn main() { - stdout.write_all(b"meow\n").unwrap_or_else(|e| { - eprintln!("{}", raw_message(e)); - std::process::exit(1); - }); -} diff --git a/src/true.c b/src/true.c index ab8da96..eb16efd 100644 --- a/src/true.c +++ b/src/true.c @@ -1,9 +1,17 @@ /* - * Copyright (c) 2023 Emma Tebibyte + * Copyright (c) 2023–2024 Emma Tebibyte * SPDX-License-Identifier: CC0 * * This work is marked with CC0 1.0. To view a copy of this license, visit * . */ -int main() {} +#ifdef __OpenBSD__ +# include /* pledge(2) */ +#endif + +int main() { +#ifdef __OpenBSD__ + pledge(NULL, NULL); +#endif +} diff --git a/tests/README b/tests/README new file mode 100644 index 0000000..7e7508c --- /dev/null +++ b/tests/README @@ -0,0 +1,36 @@ +The testing suite contains two trees: the Bonsai tree and the POSIX tree: + +. +├── README +├── bonsai/ +│   ├── dj.mk +│   ├── false.mk +│   ├── fop.mk +│   └── ... +├── posix/ +└── tests.mk + +The Bonsai tree tests the functionality of Harakit utilities for regressions and +other issues relating to compliance to our standards of practice. + +The POSIX tests are currently a work-in-progress. Their status in this +repository is uncertain. + +Both sets of tests also inherit the environment set by the top-level Makefile, +which sets the BIN variable to the build/bin directory at the root of the +project; therefore, each binary is located at $(BIN)/tool for idiomatic access. + +Each test contains a set of PHONY targets which are prefixed with the name of +the tool being tested and an underscore. The first target is tests, which +depends on all the other targets in the test file. These test files are each +included in the top Makefile, so they can be called from the root of the +repository. This also means that BIN can be set manually so that tests can be +run using make(1) inside of the tests directory: + + $ make -f tests.mk BIN=../build/bin dj_tests + +-- +Copyright © 2024 Emma Tebibyte + +This work is licensed under CC BY-SA 4.0. To view a copy of this license, visit +. diff --git a/tests/bonsai/dj.mk b/tests/bonsai/dj.mk new file mode 100755 index 0000000..bf8adf3 --- /dev/null +++ b/tests/bonsai/dj.mk @@ -0,0 +1,48 @@ +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PRAGMA: command_comment + +/dev/full: +/dev/null: + +.PHONY: dj_tests +dj_tests: dj_help dj_full dj_null # dj_skip_stdin + +.PHONY: dj_full +# Linux has a /dev/full pseudodevice useful for testing errors. +dj_full: $(BIN)/dj /dev/full + case "$$(uname)" in \ + Linux) \ + $(BIN)/dj -Hi /dev/zero -o /dev/full 2>&1 \ + | tee /dev/stderr \ + | xargs -I out test '1+0 > 0+0; 1024 > 0' = out \ + ;; \ + esac + +.PHONY: dj_help +dj_help: $(BIN)/dj + ! $(BIN)/dj -h + +.PHONY: dj_null +# Read nothing from /dev/null, write nothing to /dev/null. +dj_null: $(BIN)/dj /dev/null + $(BIN)/dj -Hi /dev/null -o /dev/null 2>&1 \ + | tee /dev/stderr \ + | xargs -I out test '0+0 > 0+0; 0 > 0' = out + +# This test currently fails. This is probably due to dj(1) being stale relative +# to the main harakit branch. TODO: Reassess once the testing branch is merged. +# .PHONY: dj_skip_stdin +# # Test skipping stdin. +# dj_skip_stdin: $(BIN)/dj +# # Pipe 1024B of '\0' into dj(1); skip the first 24B; expect 1000B written. +# dd count=1 bs=1024 /dev/null \ +# | $(BIN)/dj -H -s 24 -o /dev/null 2>&1 \ +# | tee /dev/stderr \ +# | xargs -I out test '1+0 > 1+0; 1024 > 1000' = out diff --git a/tests/bonsai/false.mk b/tests/bonsai/false.mk new file mode 100755 index 0000000..e3d19ae --- /dev/null +++ b/tests/bonsai/false.mk @@ -0,0 +1,18 @@ +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PHONY: false_tests +false_tests: false_test false_help + +.PHONY: false +false_test: $(BIN)/false + ! $(BIN)/false + +.PHONY: false_help +false_help: $(BIN)/false + ! $(BIN)/false -h diff --git a/tests/bonsai/fop.mk b/tests/bonsai/fop.mk new file mode 100755 index 0000000..b538031 --- /dev/null +++ b/tests/bonsai/fop.mk @@ -0,0 +1,31 @@ +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PHONY: fop_tests +fop_tests: fop_functionality fop_delimiter fop_help fop_fail + +.PHONY: fop_help +fop_help: $(BIN)/fop + ! $(BIN)/fop -h + +.PHONY: fop_delimiter +fop_delimiter: $(BIN)/fop + test "$$(printf 'test1 test1 test1\n' | $(BIN)/fop -d' ' 2 sed 's/1/4/g')" \ + = 'test1 test1 test4' + test "$$(printf 'meowsetwoofsetribbit\n' \ + | $(BIN)/fop -d 'set' 1 sed 's/woof/meow/g')" = 'meowsetmeowsetribbit' + +.PHONY: fop_fail +fop_fail: $(BIN)/fop + ! printf 'test\n' | $(BIN)/fop 1 cat + ! printf 'test\n' | $(BIN)/fop 'test' cat + ! printf 'test\n' | $(BIN)/fop -d'test' cat + +.PHONY: fop_functionality +fop_functionality: $(BIN)/fop + test "$$(printf 'test1\036test1\036test1\n' | $(BIN)/fop 1 sed 's/1/4/g')" \ + = "$$(printf 'test1\036test4\036test1\n')" diff --git a/tests/bonsai/hru.mk b/tests/bonsai/hru.mk new file mode 100755 index 0000000..49d30cd --- /dev/null +++ b/tests/bonsai/hru.mk @@ -0,0 +1,32 @@ +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PHONY: hru_tests +hru_tests: hru_help hru_functionality hru_negative hru_regressions + +.PHONY: hru_help +hru_help: $(BIN)/hru + ! $(BIN)/hru -h + +.PHONY: hru_functionality +hru_functionality: $(BIN)/hru + test "$$(printf '1234\n' | $(BIN)/hru)" = '1.2 kB' + test "$$(printf '0\n' | $(BIN)/hru)" = '0 B' + +.PHONY: hru_negative +hru_negative: $(BIN)/hru + ! printf '%s\n' '-1' | $(BIN)/hru + +.PHONY: hru_regressions +hru_regressions: $(BIN)/hru + n=1; \ + while true; \ + do \ + printf '%s\n' "$$n" | $(BIN)/hru || break; \ + n="$$(($$n * 10))"; \ + done; \ + printf 'Max float: %s\n' "$$n" diff --git a/tests/bonsai/intcmp.mk b/tests/bonsai/intcmp.mk new file mode 100755 index 0000000..abcff03 --- /dev/null +++ b/tests/bonsai/intcmp.mk @@ -0,0 +1,67 @@ +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PHONY: intcmp_tests +intcmp_tests: intcmp_help intcmp_none intcmp_e intcmp_g intcmp_l intcmp_combined + +.PHONY: intcmp_help +intcmp_help: $(BIN)/intcmp + ! $(BIN)/intcmp -h + +# These test that integer comparisons are working as they should. For the sake +# of readability (to facilitate faster skimming) these recipes follow a +# columned format: +# $binary -flags d d d d # op +# For flag meanings reference intcmp(1) (though they are somewhat self +# explanatory). d here refers to a decimal number; a mixture of 1s, 2s, and 3s +# (a particularly lovely number) arranged to demonstrate easily the operation +# under scrutiny. The commented op is the operation that is true for the given +# numbers. For example: +# $(BIN)/intcmp -e 3 3 3 3 # == +# op here is ==; 3 == 3 == 3 == 3. The flag being used is -e, to test for +# equality, so this test should succeed. +# ! $(BIN)/intcmp -l 3 2 1 # > +# op here is >; 3 > 2 > 1. The flag being used is -l, to test for each integer +# being less than the next, so intcmp should fail - hence the ! at the start of +# the invocation. If this test failed, intcmp(1) would be confusing -l for -g, +# so that would be a good place to start looking for bugs. + +.PHONY: intcmp_none +intcmp_none: $(BIN)/intcmp + ! $(BIN)/intcmp 1 2 + +.PHONY: intcmp_e +intcmp_e: $(BIN)/intcmp + $(BIN)/intcmp -e 3 3 3 # == + ! $(BIN)/intcmp -e 1 2 3 # < + ! $(BIN)/intcmp -e 3 2 1 # > + +.PHONY: intcmp_g +intcmp_g: $(BIN)/intcmp + $(BIN)/intcmp -g 3 2 1 # > + ! $(BIN)/intcmp -g 3 3 3 # == + ! $(BIN)/intcmp -g 1 2 3 # < + $(BIN)/intcmp -ge 3 3 1 # >= + ! $(BIN)/intcmp -ge 1 2 3 # < + +.PHONY: intcmp_l +intcmp_l: $(BIN)/intcmp + $(BIN)/intcmp -l 1 2 3 # < + ! $(BIN)/intcmp -l 3 3 3 # == + ! $(BIN)/intcmp -l 3 2 1 # > + $(BIN)/intcmp -le 1 3 3 # <= + ! $(BIN)/intcmp -le 3 2 1 # > + +.PHONY: intcmp_combined +intcmp_combined: $(BIN)/intcmp + $(BIN)/intcmp -gl 1 2 3 # < + $(BIN)/intcmp -gl 3 2 1 # > + $(BIN)/intcmp -gl 1 3 1 # != + ! $(BIN)/intcmp -gl 3 3 3 # == + $(BIN)/intcmp -egl 3 1 1 3 # >, ==, < + ! $(BIN)/intcmp -egl foo # huh? diff --git a/tests/bonsai/mm.mk b/tests/bonsai/mm.mk new file mode 100755 index 0000000..2800460 --- /dev/null +++ b/tests/bonsai/mm.mk @@ -0,0 +1,27 @@ +# Copyright (c) 2024 E$(NAME)a Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PHONY: mm_tests +mm_tests: mm_args mm_help mm_stderr + +.PHONY: mm_none +mm_none: $(BIN)/mm + test "$$(printf 'meow\n' | $(BIN)/mm)" = meow + +.PHONY: mm_args +# mm(1) will error if positional arguments are given without -i or -o +mm_args: $(BIN)/mm + ! $(BIN)/mm argument + +.PHONY: mm_help +mm_help: $(BIN)/mm + ! $(BIN)/mm -h + +.PHONY: mm_stderr +# check if stderr is empty upon specifying -e +mm_stderr: $(BIN)/mm + test "$$(printf 'test\n' | $(BIN)/mm -e 2>&1 >/dev/null )" = "test" diff --git a/tests/bonsai/npc.mk b/tests/bonsai/npc.mk new file mode 100755 index 0000000..8b10fdf --- /dev/null +++ b/tests/bonsai/npc.mk @@ -0,0 +1,72 @@ +#!/bin/sh +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PRAGMA: command_comment + +.PHONY: npc_tests +npc_tests: npc_help npc_args npc_ascii + +.PHONY: npc_help +npc_help: $(BIN)/npc + ! $(BIN)/npc -h + +.PHONY: npc_args +# arg parsing +npc_args: + $(BIN)/npc -e ?" = out + +.PHONY: npc_ascii_uppers +# ASCII 0x40 to 0x5f (uppercases) +npc_ascii_uppers: + awk 'BEGIN{ for (i = 64; i < 96; ++i) printf("%c", i); print }' \ + | $(BIN)/npc \ + | sed 's/\\/\\\\/' \ + | xargs -I out test @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ = out + +# This test is broken and will need closer inspection along with the npc(1) +# source. +# .PHONY: npc_ascii_lowers +# # ASCII 0x60 to 0x7f (lowercases) +# npc_ascii_lowers: +# awk 'BEGIN{ for (i = 96; i < 128; ++i) printf("%c", i); print }' \ +# | $(BIN)/npc \ +# | xargs -I out test "\`abcdefghijklmnopqrstuvwxyz{|}~^?" = out diff --git a/tests/bonsai/rpn.mk b/tests/bonsai/rpn.mk new file mode 100755 index 0000000..3619692 --- /dev/null +++ b/tests/bonsai/rpn.mk @@ -0,0 +1,43 @@ +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PHONY: rpn_tests +rpn_tests: rpn_help rpn_add rpn_sub rpn_mul rpn_div rpn_mod rpn_flr + +.PHONY: rpn_help +rpn_help: $(BIN)/rpn + ! $(BIN)/rpn -h + +.PHONY: rpn_add +rpn_add: $(BIN)/rpn + test "$$($(BIN)/rpn 1 2 +)" -eq 3 + test "$$($(BIN)/rpn 0.2 0.1 +)" = 0.3 + +.PHONY: rpn_sub +rpn_sub: $(BIN)/rpn + test "$$($(BIN)/rpn 23 5 -)" -eq 18 + test "$$($(BIN)/rpn 0.3 0.1 -)" = 0.2 + +.PHONY: rpn_mul +rpn_mul: $(BIN)/rpn + test "$$($(BIN)/rpn 1.2 3 '*')" = 3.6 + test "$$($(BIN)/rpn 0 3 '*')" -eq 0 + +.PHONY: rpn_div +rpn_div: $(BIN)/rpn + test "$$($(BIN)/rpn 12 5 /)" = 2.4 + test "$$($(BIN)/rpn 3 0 /)" -eq inf + +.PHONY: rpn_mod +rpn_mod: $(BIN)/rpn + test "$$($(BIN)/rpn 12 5 %)" -eq 2 + test "$$($(BIN)/rpn 9 4 %)" -eq 1 + +.PHONY: rpn_flr +rpn_flr: $(BIN)/rpn + test "$$($(BIN)/rpn 12 5 //)" -eq 2 + test "$$($(BIN)/rpn 9 4 //)" -eq 2 diff --git a/tests/bonsai/scrut.mk b/tests/bonsai/scrut.mk new file mode 100755 index 0000000..e7f421e --- /dev/null +++ b/tests/bonsai/scrut.mk @@ -0,0 +1,46 @@ +#!/bin/sh +# Copyright (c) 2024 DTB +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PRAGMA: command_comment + +.PHONY: scrut_tests +scrut_tests: scrut_help scrut_options + +.PHONY: scrut_help +scrut_help: $(BIN)/scrut + ! $(BIN)/scrut -h + +.PHONY: scrut_options +# scrut tests file attributes, but files of a certain attribute aren't +# guaranteed to be present on a system. This test checks all of the files in +# harakit and, if test(1p) says a file matches a certain attribute, then checks +# scrut. +# opts are space-delimited (for command splitting), sel is not +scrut_options: $(BIN)/scrut + set -e; \ + opts="b c d e f g k p r s u w x L S"; \ + sel=; \ + find . -name .git -prune -o -print \ + | while read -r f; do \ + for opt in $$opts; \ + do if ! printf "%s\n" $$sel | grep $$opt >/dev/null; then \ + if test -$$opt "$$f"; then \ + if ! $(BIN)/scrut -$$opt "$$f"; \ + then printf "[!!] scrut -%s failed on %s.\n" \ + $$opt "$$f"; \ + fi; \ + sel="$$sel$$opt"; \ + printf "[OK] Tested scrut -%s using %s\n" \ + $$opt "$$f"; \ + fi; \ + fi; \ + done; \ + if printf "%s\n" "$$opts" | sed 's/ //g' | xargs test "$$sel" =; \ + then break; \ + fi; \ + done diff --git a/tests/bonsai/str.mk b/tests/bonsai/str.mk new file mode 100755 index 0000000..38ad9ca --- /dev/null +++ b/tests/bonsai/str.mk @@ -0,0 +1,20 @@ +# Copyright (c) 2024 DTB +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PRAGMA: command_comment + +.PHONY: str_tests +str_tests: str_help str_isalpha + +.PHONY: str_help +str_help: $(BIN)/str + ! $(BIN)/str -h + +.PHONY: str_isalpha +str_isalpha: $(BIN)/str + $(BIN)/str isalpha c + ! $(BIN)/str isalpha 3 diff --git a/tests/bonsai/strcmp.mk b/tests/bonsai/strcmp.mk new file mode 100755 index 0000000..c395ca3 --- /dev/null +++ b/tests/bonsai/strcmp.mk @@ -0,0 +1,31 @@ +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +NAME = strcmp +TARGET = $(NAME)_tests +BINARY = $(BIN)/$(NAME) + +.PHONY: strcmp_tests +strcmp_tests: strcmp_equals strcmp_help strcmp_nocmp strcmp_unequals + +.PHONY: strcmp_equals +strcmp_equals: $(BIN)/strcmp + $(BIN)/strcmp equals equals + $(BIN)/strcmp - - + +.PHONY: strcmp_help +strcmp_help: $(BIN)/strcmp + ! $(BIN)/strcmp -h + +.PHONY: strcmp_nocmp +strcmp_nocmp: $(BIN)/strcmp + ! $(BIN)/strcmp nocmp + +.PHONY: strcmp_unequals +strcmp_unequals: $(BIN)/strcmp + ! $(BIN)/strcmp unequals equals diff --git a/tests/bonsai/swab.mk b/tests/bonsai/swab.mk new file mode 100755 index 0000000..ff64c17 --- /dev/null +++ b/tests/bonsai/swab.mk @@ -0,0 +1,22 @@ +# Copyright (c) 2024 DTB +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PRAGMA: command_comment + +.PHONY: swab_tests +swab_tests: swab_help swab_examples + +.PHONY: swab_help +swab_help: $(BIN)/swab + ! $(BIN)/swab -h + +.PHONY: swab_examples +# These are the examples present in the man page. +swab_examples: $(BIN)/swab + printf 'hello world!\n' \ + | $(BIN)/swab \ + | xargs -I out test 'ehll oowlr!d' = out diff --git a/tests/bonsai/true.mk b/tests/bonsai/true.mk new file mode 100755 index 0000000..fb6e82c --- /dev/null +++ b/tests/bonsai/true.mk @@ -0,0 +1,19 @@ +#!/bin/sh +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +.PHONY: true_tests +true_tests: true_test + +.PHONY: true_help +true_help: $(BIN)/true + $(BIN)/true -h + +.PHONY: true_test +true_test: $(BIN)/true + $(BIN)/true diff --git a/tests/posix-compat.sh b/tests/posix-compat.sh deleted file mode 100755 index 1e98d7b..0000000 --- a/tests/posix-compat.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -# Copyright (c) 2023–2024 Emma Tebibyte -# SPDX-License-Identifier: FSFAP -# -# Copying and distribution of this file, with or without modification, are -# permitted in any medium without royalty provided the copyright notice and this -# notice are preserved. This file is offered as-is, without any warranty. - -set -e - -if ! ls Makefile >/dev/null 2>&1 -then - printf '%s: Run this script in the root of the project.\n' "$0" 1>&2 - exit 1 -fi - -printf "Starting POSIX compatibility testing.\n" - -for utility in tests/posix/*; do - printf '%s: %s: Testing utility.\n' "$0" "$utility" - "$utility" - printf '\n' -done diff --git a/tests/posix/bin/cat b/tests/posix/bin/cat new file mode 100755 index 0000000..e123d7c --- /dev/null +++ b/tests/posix/bin/cat @@ -0,0 +1,22 @@ +#!/bin/sh +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +# Strictly POSIX-compliant cat(1) implementation. See cat(1p) + +for arg in "$@"; do + case "$arg" in + -u) args="$(printf '%s %s\n' "$args" "$arg")" ;; + *) args="$(printf -- '%s -i %s\n' "$args" "$arg")" ;; + esac +done + +# See IEEE Std 1003.1-2017 3.282 +# https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282 +IFS=' ' + +mm $args diff --git a/tests/posix/bin/false b/tests/posix/bin/false new file mode 100755 index 0000000..62a10c9 --- /dev/null +++ b/tests/posix/bin/false @@ -0,0 +1,12 @@ +#!/bin/sh +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +# Strictly POSIX-compliant false(1) implementation. See false(1p) + +false "$@" diff --git a/tests/posix/bin/true b/tests/posix/bin/true new file mode 100755 index 0000000..aed1b86 --- /dev/null +++ b/tests/posix/bin/true @@ -0,0 +1,11 @@ +#!/bin/sh +# Copyright (c) 2024 DTB +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +# Strictly POSIX-compliant true(1) implementation. See true(1p) +true "$@" diff --git a/tests/posix/posix_env b/tests/posix/posix_env new file mode 100644 index 0000000..b436601 --- /dev/null +++ b/tests/posix/posix_env @@ -0,0 +1,5 @@ +#!/bin/sh + +set -ex + +PATH="$PWD/bin:$PATH" diff --git a/tests/tests.mk b/tests/tests.mk new file mode 100644 index 0000000..04f3680 --- /dev/null +++ b/tests/tests.mk @@ -0,0 +1,15 @@ +# Copyright (c) 2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + + +#TESTFILES != for file in tests/bonsai/*.mk tests/posix/*.mk; do printf '%s ' "$$file"; done; +TESTFILES != for file in tests/bonsai/*.mk; do printf '%s ' "$$file"; done; + +TESTS != printf '%s\n' "$(TESTFILES)" | xargs -n1 basename \ + | sed 's/\.mk/_tests/g' + +include $(TESTFILES)