diff --git a/CONTRIBUTING b/CONTRIBUTING index 74e821e..267918b 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -99,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: @@ -130,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 024815f..b86db1e 100644 --- a/Makefile +++ b/Makefile @@ -16,21 +16,31 @@ DESTDIR ?= dist PREFIX ?= /usr/local +# for conditionally compiling OS features +OS != uname +OS_INCLUDE != test -e include/$(OS).mk && printf 'include/$(OS).mk\n' \ + || printf '/dev/null\n' + # normalized prefix -PREFIX_N != (test -d $(PREFIX) && [ '-' != $(PREFIX) ] \ - && CDPATH= cd -P -- $(PREFIX) && pwd -P) -MANDIR != [ $(PREFIX_N) = / ] && printf '/usr/share/man\n' \ +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: docs dj false fop hru intcmp mm npc peek rpn scrut str strcmp swab true @@ -52,10 +62,12 @@ dist: all docs install: dist cp -r $(DESTDIR)/* / +include tests/tests.mk + .PHONY: test -test: build /tmp/getopt +test: all $(TESTS) /tmp/getopt + @echo $(TESTS) /tmp/getopt - tests/posix-compat.sh /tmp/getopt: src/libgetopt.rs $(RUSTC) --test -o /tmp/getopt src/libgetopt.rs @@ -67,9 +79,12 @@ docs: docs/ build "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/libgetopt.rs $(RUSTC) $(RUSTFLAGS) --crate-type=lib --crate-name=getopt \ @@ -100,22 +115,22 @@ 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.rs build rustlibs - $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/intcmp.rs + $(RUSTC) $(RUSTFLAGS) -o $@ src/intcmp.rs .PHONY: mm mm: build/bin/mm build/bin/mm: src/mm.rs build rustlibs - $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/mm.rs + $(RUSTC) $(RUSTFLAGS) -o $@ src/mm.rs .PHONY: npc npc: build/bin/npc @@ -130,7 +145,7 @@ build/bin/peek: src/peek.c build .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 @@ -150,7 +165,7 @@ build/bin/strcmp: src/strcmp.c build .PHONY: swab swab: build/bin/swab build/bin/swab: src/swab.rs build rustlibs - $(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/swab.rs + $(RUSTC) $(RUSTFLAGS) -o $@ src/swab.rs .PHONY: true true: build/bin/true 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 d358e3f..79096d5 100644 --- a/docs/dj.1 +++ b/docs/dj.1 @@ -4,7 +4,7 @@ .\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, .\" visit . .\" -.TH DJ 1 2024-07-03 "Harakit X.X.X" +.TH DJ 1 2024-07-14 "Harakit X.X.X" .SH NAME dj \(en disk jockey .\" @@ -56,9 +56,9 @@ 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 -Does the same as -.B -b -but for the output buffer. +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, @@ -68,8 +68,8 @@ 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 reads to make. The default is 0, in which case the -input is read until a partial or empty read is made. +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. @@ -181,15 +181,22 @@ option is specified, this could make written data nonsensical. Existing files are not truncated on ouput and are instead overwritten. -The options -.B -b -and +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 -could be confused for each other, and so could -.B -s -and -.BR -S . -The lowercase option affects input and the capitalized option affects output. +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 @@ -216,3 +223,4 @@ Copyright \(co 2023 DTB. License AGPLv3+: GNU AGPL version 3 or later .SH SEE ALSO .BR dd (1p) .BR lseek (3p) +.BR mm (1) diff --git a/docs/swab.1 b/docs/swab.1 index 7c50b15..c364814 100644 --- a/docs/swab.1 +++ b/docs/swab.1 @@ -33,6 +33,7 @@ line: .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: 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/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 3b5bc5f..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 @@ -19,26 +20,24 @@ #include /* assert(3) */ #include /* errno */ #include /* open(2) */ +#include /* bool */ #include /* fprintf(3), stderr */ #include /* malloc(3), strtol(3), size_t */ #include /* memcpy(3), memmove(3), memset(3) */ -#if !defined EX_OK || !defined EX_OSERR || !defined EX_USAGE -# include -#endif +#include /* EX_OK, EX_OSERR, EX_USAGE */ #include /* close(2), getopt(3), lseek(2), read(2), write(2), - * optarg, optind, STDIN_FILENO, STDOUT_FILENO */ -#include /* S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, - S_IWUSR */ -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{ - char *buf; /* buffer */ - char *fn; /* file name (-io) */ +struct Io { + char *buf; /* buffer */ + char *fn; /* file name (-io) */ size_t bs; /* buffer size (-bB) */ size_t bufuse; /* buffer usage */ size_t bytes; /* bytes processed */ @@ -46,14 +45,10 @@ struct Io{ 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 */ + int fd; /* file descriptor */ + int fl; /* file opening flags */ }; -/* To be assigned to main:fmt and used with printio(). */ -static char *fmt_asv = "%d\037%d\036%d\037%d\035%d\036%d\034"; -static char *fmt_human = "%d+%d > %d+%d; %d > %d\n"; - static char *stdin_name = ""; static char *stdout_name = ""; @@ -67,14 +62,15 @@ static int write_flags = O_WRONLY | O_CREAT; /* Macro to check if fd is stdin or stdout */ #define fdisstd(fd) ((fd) == STDIN_FILENO || (fd) == STDOUT_FILENO) +/* Completes one Io block read */ static struct Io * -Io_read(struct Io *io){ +Io_read(struct Io *io) { int t; assert(io->bs > 0); assert(io->bufuse < io->bs); - if((t = read(io->fd, &(io->buf)[io->bufuse], io->bs - io->bufuse)) < 0){ + if ((t = read(io->fd, &(io->buf)[io->bufuse], io->bs - io->bufuse)) < 0) { io->error = errno; t = 0; } @@ -89,18 +85,20 @@ Io_read(struct Io *io){ return io; } +/* Completes one Io block write */ static struct Io * -Io_write(struct Io *io){ +Io_write(struct Io *io) { int t; assert(io->bufuse > 0); assert(io->bufuse <= io->bs); - if((t = write(io->fd, io->buf, io->bufuse)) < 0){ + 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)); + } else if (t > 0) { + (void)memmove(io->buf, &(io->buf)[t], (io->bufuse -= t)); + } io->bytes += t; io->prec += (t > 0 && io->bufuse > 0); @@ -110,62 +108,87 @@ Io_write(struct Io *io){ } static int -oserr(char *e, int n){ - fprintf(stderr, "%s: %s: %s\n", program_name, e, strerror(n)); +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. */ -static void -fprintio(FILE *stream, char *fmt, struct Io io[2]){ - - fprintf(stream, fmt, - io[0].rec, io[0].prec, io[1].rec, io[1].prec, - io[0].bytes, io[1].bytes); - - return; +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(char *s){ - - fprintf(stderr, "Usage: %s [-Hn] [-a byte] [-c count]\n" +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", - program_name); + argv0 + ); return EX_USAGE; } -int main(int argc, char *argv[]){ - int align; /* low 8b used, negative if no alignment is being done */ - int count; /* 0 if dj(1) runs until no more reads are possible */ - char *fmt; /* == fmt_asv (default) or fmt_human (-H) */ - size_t i; /* side of io being modified */ - char noerror; /* 0=exits (default) 1=retries on partial reads or writes */ +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 } */]; +#ifdef __OpenBSD__ + if (pledge("cpath rpath stdio unveil wpath", NULL) == -1) { + return oserr(NULL, errno); + } +#endif + /* Set defaults. */ align = -1; - count = 0; + count = -1; fmt = fmt_asv; - noerror = 0; - for(i = 0; i < (sizeof io) / (sizeof *io); ++i){ + 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; @@ -178,88 +201,123 @@ int main(int argc, char *argv[]){ io[i].seek = 0; } - if(argc > 0){ + if (argc > 0) { int c; program_name = argv[0]; - while((c = getopt(argc, argv, ":a:b:B:c:i:hHns:S:o:")) != -1) - switch(c){ - case 'i': case 'o': i = (c == 'o'); - if(optarg[0] == '-' && optarg[1] == '\0'){ /* optarg == "-" */ - io[i].fd = i == 0 ? STDIN_FILENO : STDOUT_FILENO; - io[i].fn = i == 0 ? stdin_name : stdout_name; - break; - }else{ - int fd; + 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'); - 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; + /* 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; } - } - return oserr(optarg, errno); - case 'n': noerror = 1; break; - case 'H': fmt = fmt_human; 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': /* numbers */ - if(c == 'c' && (count = parse(optarg)) >= 0) - break; - i = (c >= 'A' && c <= 'Z'); - c |= 0x20 /* 0b 0010 0000 */; /* (ASCII) make lowercase */ - if((c == 'b' && (io[i].bs = parse(optarg)) > 0) - || (c == 's' && (io[i].seek = parse(optarg)) >= 0)) - break; - /* FALLTHROUGH */ - default: - return usage(program_name); + /* 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); } + } } +#ifdef __OpenBSD__ + if (unveil(NULL, NULL) == -1) { return oserr(NULL, errno); } +#endif + assert(io->fd != STDIN_FILENO || io->fl == read_flags); assert(io->fd != STDOUT_FILENO || io->fl == write_flags); - if(argc > optind) - return usage(program_name); + if (argc > optind) { return usage(program_name); } - for(i = 0; i < (sizeof io) / (sizeof *io); ++i){ + for (i = 0; i < (sizeof io) / (sizeof *io); ++i) { /* buffer allocation */ - if((io[i].buf = malloc(io[i].bs * (sizeof *(io[i].buf)))) == NULL){ - fprintf(stderr, "%s: Failed to allocate %zd bytes\n", - program_name, io[i].bs); + 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; } + /* easy seeking */ - if(!fdisstd(io[i].fd) && lseek(io[i].fd, io[i].seek, SEEK_SET) != -1) + if (!fdisstd(io[i].fd) && lseek(io[i].fd, io[i].seek, SEEK_SET) != -1) { io[i].seek = 0; + } } - /* hard seeking */ - if(io[1].seek > 0){ - size_t t; - do{ - memset(io[1].buf, '\0', - (t = io[1].bufuse = MIN(io[1].bs, io[1].seek))); - if(Io_write(&io[1])->bufuse == t && !noerror && io[1].error == 0) - Io_write(&io[1]); /* second chance */ - if(io[1].error != 0) - return oserr(io[1].fn, io[1].error); - }while((io[1].seek -= (t - io[1].bufuse)) > 0 && io[1].bufuse != t); - io[1].bufuse = 0; + 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! */ } - if(io[1].seek > 0){ - fprintio(stderr, fmt, io); + 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); } - do{ + for ( ; + count == -1 || count > 0; + count -= (count != -1) /* decrement if counting */ + ) { assert(io[0].bufuse == 0); { /* read */ @@ -267,87 +325,114 @@ int main(int argc, char *argv[]){ size_t t; /* hack to intentionally get a partial read from Io_read */ - if((skipping = MIN(io[0].seek, io[0].bs)) > 0) + if ((skipping = MIN(io[0].seek, io[0].bs)) > 0) { io[0].bufuse = io[0].bs - (size_t)skipping; + } t = io[0].bufuse; - if(Io_read(&io[0])->bufuse == t && !noerror && io[0].error == 0) - Io_read(&io[0]); /* second chance */ - assert(io[0].bufuse >= t); - if(io[0].bufuse == t) /* that's all she wrote */ - break; + if (Io_read(&io[0])->bufuse == t && !retry && io[0].error == 0) { + (void)Io_read(&io[0]); /* second chance */ + } - if(/* t < io[0].bufuse && */ io[0].bufuse < io[0].bs){ - fprintf(stderr, "%s: Partial read:\n\t", program_name); - fprintio(stderr, fmt, io); - if(!noerror) - count = 1; - if(align >= 0){ + 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 */ - memset(&(io[0].buf)[io[0].bufuse], align, - io[0].bs - io[0].bufuse); + (void)memset( + &(io[0].buf)[io[0].bufuse], + align, + io[0].bs - io[0].bufuse + ); + io->bufuse = io->bs; } } - if(skipping > 0){ + if (skipping > 0) { io[0].seek -= skipping; io[0].bufuse = 0; - count += (count != 0); + count += (count != -1); /* increment if counting */ continue; } } - /* write */ - do{ - int t; + assert(io[0].bufuse > 0); - if(io[0].bs <= io[1].bs){ + while (io[0].bufuse > 0) { /* write */ + if (io[0].bs <= io[1].bs) { int n; - /* saturate obuf */ - memcpy(io[1].buf, io[0].buf, - (io[1].bufuse = (n = MIN(io[0].bufuse, io[1].bs)))); + (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 */ - memmove(io[0].buf, &(io[0].buf)[n], (io[0].bufuse -= n)); - }else /* if(io[0].bs < io[1].bs) */ { + (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 */ - memcpy(&(io[1].buf)[io[1].bufuse], io[0].buf, - (n = MIN(io[0].bufuse, io[1].bs - io[1].bufuse))); + (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 */ - memmove(io[0].buf, &(io[0].buf)[n], io[0].bs - n); + (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) + if(io[0].bs + io[1].bufuse <= io[1].bs && count != 1) { continue; /* obuf not saturated - we could write more */ + } } - t = io[1].bufuse; - if(Io_write(&io[1])->bufuse == t && !noerror && io[1].error == 0) - Io_write(&io[1]); /* second chance */ - assert(io[1].bufuse <= t); - if(io[1].bufuse == t){ /* no more love */ - count = 1; - break; - } + { /* writes actually happen, or die */ + size_t t; - if(0 < io[1].bufuse /* && io[1].bufuse < t */){ - fprintf(stderr, "%s: Partial write:\n\t", program_name); - fprintio(stderr, fmt, io); - if(!noerror) + 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; + } } - }while(io[0].bufuse > 0); - }while(count == 0 || --count > 0); - fprintio(stderr, fmt, io); + if (0 < io[1].bufuse /* && io[1].bufuse < t */) { + (void)fprintf(stderr, "%s: Partial write:\n\t", program_name); + (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); + 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..555a170 100644 --- a/src/false.c +++ b/src/false.c @@ -1,9 +1,19 @@ /* - * Copyright (c) 2023 Emma Tebibyte + * Copyright (c) 2023–2024 Emma Tebibyte + * Copyright (c) 2024 DTB * 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(void) { +#ifdef __OpenBSD__ + pledge(NULL, NULL); +#endif + return 1; +} diff --git a/src/fop.rs b/src/fop.rs index 91c8a72..7c8424e 100644 --- a/src/fop.rs +++ b/src/fop.rs @@ -30,10 +30,22 @@ 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 = '\u{1E}'.to_string(); - let mut index_arg = 0; + let mut d = '\u{1E}'.to_string(); /* ASCII record separator */ + let mut optind = 1; + + #[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...]", @@ -42,51 +54,62 @@ fn main() { while let Some(opt) = argv.getopt("d:") { match opt.opt() { - Ok(_) => { - /* unwrap because Err(OptError::MissingArg) will be returned if - * opt.arg() is None */ + Ok("d") => { + /* delimiter */ d = opt.arg().unwrap(); - index_arg = opt.ind(); + optind = opt.ind(); }, - Err(_) => { + _ => { eprintln!("{}", usage); exit(EX_USAGE); } }; } - let command_arg = index_arg as usize + 1; - - argv.get(command_arg).unwrap_or_else(|| { - eprintln!("{}", usage); - 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); }); + /* index of the argv[0] for the operator command */ + let command_arg = optind as usize + 1; + + /* 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(); - let _ = stdin().read_to_string(&mut buf); + 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::>(); - let opts = argv + /* 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", @@ -96,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| { @@ -106,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/hru.rs b/src/hru.rs index b7937f7..e07ec39 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); + } + + #[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.rs b/src/intcmp.rs index f504b50..456f268 100644 --- a/src/intcmp.rs +++ b/src/intcmp.rs @@ -1,5 +1,6 @@ /* * 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 @@ -22,11 +23,17 @@ use std::{ }; extern crate getopt; -use getopt::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) @@ -34,6 +41,15 @@ fn usage(s: &str) -> ExitCode { fn main() -> ExitCode { let argv = args().collect::>(); + + #[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 < */ @@ -46,11 +62,13 @@ fn main() -> ExitCode { Ok("e") => e = true, Ok("g") => g = true, Ok("l") => l = true, - _ => { return usage(&argv[0]); }, + _ => 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 */ @@ -59,8 +77,8 @@ fn main() -> ExitCode { for arg in argv.iter().skip(optind) { /* iterate operands */ match arg.parse::() { /* parse current operand */ Ok(n) => currn = n, - _ => { - eprintln!("{}: {}: Invalid integer", &argv[0], arg); + Err(e) => { + eprintln!("{}: {}: {}", &argv[0], arg, e); return ExitCode::from(EX_USAGE as u8); } } 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/mm.rs b/src/mm.rs index 41b7857..10c6ef2 100644 --- a/src/mm.rs +++ b/src/mm.rs @@ -33,6 +33,16 @@ 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 } @@ -41,6 +51,14 @@ fn main() -> ExitCode { let argv = args().collect::>(); let usage = format!("Usage: {} [-aetu] [-i input] [-o output]", argv[0]); + #[cfg(target_os="openbsd")] { + let promises = Promises::new("cpath rpath stdio unveil wpath"); + 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 */ @@ -62,7 +80,7 @@ fn main() -> ExitCode { mode = Some(In); /* latest argument == -i */ }, Ok("o") => { /* add output */ - let output = opt.arg().unwrap(); + let output = opt.arg().unwrap(); outs.push(output); mode = Some(Out); /* latest argument == -o */ }, @@ -86,8 +104,35 @@ fn main() -> ExitCode { Out => outs.push(arg.to_string()), }; } - } else { - eprintln!("{}", usage); + } + + #[cfg(target_os="openbsd")] { + for input in &ins { + 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); + } + } + + for output in &outs { + let perms = UnveilPerms::new(vec!['c', 'w']); + + if let Err(e) = unveil(Some(&output), Some(perms)) { + eprintln!("{}: {}", argv[0], e.strerror()); + return ExitCode::from(EX_OSERR as u8); + } + } + + 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); } diff --git a/src/npc.c b/src/npc.c index 1f96668..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 [-et]\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..7669efb 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::>(); + + #[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 d85d243..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/str.c b/src/str.c index b4725eb..9ed27c2 100644 --- a/src/str.c +++ b/src/str.c @@ -19,17 +19,21 @@ #include #include /* NULL */ -#include /* fprintf(3) */ -#include /* EXIT_FAILURE */ +#include /* fprintf(3), perror(3) */ +#include /* size_t, EXIT_FAILURE */ #include /* strcmp(3) */ -#include +#include /* EX_OSERR, EX_USAGE */ -static char *program_name = "str"; +#ifdef __OpenBSD__ +# include /* pledge(2) */ +#endif + +char *program_name = "str"; static struct { char *name; int (*f)(int); -}ctypes[] = { +} ctypes[] = { { "isalnum", isalnum }, { "isalpha", isalpha }, { "isblank", isblank }, @@ -41,35 +45,52 @@ static struct { { "isprint", isprint }, { "ispunct", ispunct }, { "isspace", isspace }, - { "isupper", isupper } + { "isupper", isupper }, + { NULL, NULL } /* marks end */ }; -int main(int argc, char *argv[]){ - int ctype; - int i; - int r; - - if(argc >= 3){ - for(ctype = 0; ctype < (sizeof ctypes) / (sizeof *ctypes); - ++ctype) - if(strcmp(argv[1], ctypes[ctype].name) == 0) - goto pass; - } - - fprintf(stderr, "Usage: %s type string...\n", - argv[0] == NULL ? program_name : argv[0]); +static int +usage(char *argv0) { + (void)fprintf(stderr, "Usage: %s type string...\n", argv0); return EX_USAGE; - -pass: for(argv += 2, r = 1; *argv != NULL; ++argv) - for(i = 0; argv[0][i] != '\0'; ++i) - /* First checks if argv[0][i] is valid ASCII; ctypes(3) - * don't handle non-ASCII. - * This is bad. */ - if((unsigned char)argv[0][i] < 0x80 && !ctypes[ctype].f(argv[0][i])) - return 1; - else - r = 0; - - return r; +} + +int main(int argc, char *argv[]) { + size_t ctype; // selected from ctypes.h; index of ctype + int retval; // initially fail but becomes success on the first valid char + program_name = argv[0] == NULL ? program_name : argv[0]; + +#ifdef __OpenBSD__ + if (pledge("stdio", NULL) == -1) { + perror(program_name); + return EX_OSERR; + } +#endif + + if (argc < 3) { return usage(program_name); } + + for ( /* iterate ctypes */ + ctype = 0; + ctypes[ctype].f != NULL /* break at the end of ctypes */ + && strcmp(argv[1], ctypes[ctype].name) != 0; /* break at match */ + ++ctype + ); + + if (ctypes[ctype].f == NULL) { return usage(argv[0]); } + + /* iterate args */ + for (argv += 2, retval = EXIT_FAILURE; *argv != NULL; ++argv) { + for (size_t i = 0; argv[0][i] != '\0'; ++i) { /* iterate arg bytes */ + /* First checks if argv[0][i] is valid ASCII; ctypes(3) don't + * handle non-ASCII. This is bad. */ + if( + (unsigned char)argv[0][i] < 0x80 // argv[0][i] is ASCII, + && !ctypes[ctype].f(argv[0][i]) // so use ctypes(3) + ) { return EXIT_FAILURE; } + else { retval = EXIT_SUCCESS; } + } + } + + return retval; } diff --git a/src/strcmp.c b/src/strcmp.c index 33eab10..6d930d5 100644 --- a/src/strcmp.c +++ b/src/strcmp.c @@ -1,5 +1,6 @@ /* - * Copyright (c) 2022–2024 DTB + * 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 @@ -15,29 +16,47 @@ * 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 */ -#include /* fprintf(3), stderr */ -#include /* size_t */ -#include /* EX_USAGE */ +#ifdef __OpenBSD__ +# include /* pledge(2) */ +#endif char *program_name = "strcmp"; -int main(int argc, char *argv[]){ +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) { - fprintf(stderr, "Usage: %s string string...\n", + (void)fprintf( + stderr, + "Usage: %s string string...\n", argv[0] == NULL ? program_name : argv[0] ); + return EX_USAGE; } - /* This compares the Nth character of arg[2] onward with argv[1]'s Nth - * character, rather than comparing each arg with argv[1] sequentially. */ - for (; *argv[1] != '\0'; ++argv[1]) { /* iterate chars in argv[1] */ - for (size_t i = 2; i < argc; ++argv[i], ++i) { /* iterate &argv[2] */ - /* this never overruns because of nul termination */ - if (*argv[i-1] != *argv[i]) { return *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; + /* 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 dd5b464..4dc86de 100644 --- a/src/swab.rs +++ b/src/swab.rs @@ -25,19 +25,27 @@ use std::{ }; extern crate getopt; -use getopt::GetOpt; - extern crate sysexits; -use sysexits::{ EX_OK, EX_OSERR, EX_USAGE }; - extern crate strerror; + +use getopt::GetOpt; +use sysexits::{ EX_IOERR, EX_OK, EX_OSERR, EX_USAGE }; use strerror::StrError; -fn oserr(s: &str, e: Error) -> ExitCode { - eprintln!("{}: {}", s, e.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: {} [-w word_size]", s); ExitCode::from(EX_USAGE as u8) @@ -45,12 +53,19 @@ fn usage(s: &str) -> ExitCode { fn main() -> ExitCode { let argv = args().collect::>(); - let mut buf: Vec = Vec::new(); + + #[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; // Equivalent to dd(1p). + let mut wordsize: usize = 2; // default; mimics dd(1p) conv=swab while let Some(opt) = argv.getopt("w:") { match opt.opt() { @@ -73,17 +88,19 @@ fn main() -> ExitCode { 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) => break oserr(&argv[0], e) diff --git a/src/true.c b/src/true.c index ab8da96..0e2f91d 100644 --- a/src/true.c +++ b/src/true.c @@ -1,9 +1,18 @@ /* - * Copyright (c) 2023 Emma Tebibyte + * Copyright (c) 2023–2024 Emma Tebibyte + * Copyright (c) 2024 DTB * 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(void) { +#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..2b4ef6b --- /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..124d284 --- /dev/null +++ b/tests/bonsai/mm.mk @@ -0,0 +1,34 @@ +# 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: mm_tests +mm_tests: mm_args mm_help mm_stderr mm_remaining + +.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" + +.PHONY: mm_remaining +# check to make sure remaining arguments are used +mm_remaining: $(BIN)/mm + test "$$($(BIN)/mm -i README COPYING)" = "$$(cat README COPYING)" + $(BIN)/mm -i README -o /tmp/mm_test0 /tmp/mm_test1 + diff /tmp/mm_test0 /tmp/mm_test1 diff --git a/tests/bonsai/npc.mk b/tests/bonsai/npc.mk new file mode 100755 index 0000000..b028c4d --- /dev/null +++ b/tests/bonsai/npc.mk @@ -0,0 +1,90 @@ +#!/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..6fecab9 --- /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 /)" = 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)