diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:55 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:55 +0300 |
| commit | 22343fc2bb8db94066d3e314c5a20a9b9278f4c9 (patch) | |
| tree | d2a731892b60e63970796b567a6a2dd7eb209427 /corebinutils | |
| parent | 4cf86e37e42f9f04ec4a41a28ffe466d2510438a (diff) | |
| parent | ceefe27a76f3b2075abbf01b0c44375363967af6 (diff) | |
| download | Project-Tick-22343fc2bb8db94066d3e314c5a20a9b9278f4c9.tar.gz Project-Tick-22343fc2bb8db94066d3e314c5a20a9b9278f4c9.zip | |
Add 'corebinutils/test/' from commit 'ceefe27a76f3b2075abbf01b0c44375363967af6'
git-subtree-dir: corebinutils/test
git-subtree-mainline: 4cf86e37e42f9f04ec4a41a28ffe466d2510438a
git-subtree-split: ceefe27a76f3b2075abbf01b0c44375363967af6
Diffstat (limited to 'corebinutils')
| -rw-r--r-- | corebinutils/test/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/test/GNUmakefile | 46 | ||||
| -rw-r--r-- | corebinutils/test/LICENSE | 26 | ||||
| -rw-r--r-- | corebinutils/test/README.md | 47 | ||||
| -rw-r--r-- | corebinutils/test/test.1 | 392 | ||||
| -rw-r--r-- | corebinutils/test/test.c | 679 | ||||
| -rw-r--r-- | corebinutils/test/tests/fd_helper.c | 111 | ||||
| -rw-r--r-- | corebinutils/test/tests/legacy_test.sh | 209 | ||||
| -rw-r--r-- | corebinutils/test/tests/test.sh | 479 |
9 files changed, 2014 insertions, 0 deletions
diff --git a/corebinutils/test/.gitignore b/corebinutils/test/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/test/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/corebinutils/test/GNUmakefile b/corebinutils/test/GNUmakefile new file mode 100644 index 0000000000..828204bb77 --- /dev/null +++ b/corebinutils/test/GNUmakefile @@ -0,0 +1,46 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS ?= +CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror -Wpedantic +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/test +BRACKET_TARGET := $(OUTDIR)/[ +FD_HELPER := $(OBJDIR)/fd_helper +OBJS := $(OBJDIR)/test.o + +.PHONY: all clean dirs status test + +all: $(TARGET) $(BRACKET_TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(BRACKET_TARGET): $(TARGET) | dirs + ln -sf "test" "$@" + +$(OBJDIR)/test.o: $(CURDIR)/test.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/test.c" -o "$@" + +$(FD_HELPER): $(CURDIR)/tests/fd_helper.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) "$(CURDIR)/tests/fd_helper.c" -o "$@" $(LDFLAGS) $(LDLIBS) + +test: $(TARGET) $(BRACKET_TARGET) $(FD_HELPER) + TEST_BIN="$(TARGET)" BRACKET_BIN="$(BRACKET_TARGET)" \ + FD_HELPER_BIN="$(FD_HELPER)" \ + sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/test/LICENSE b/corebinutils/test/LICENSE new file mode 100644 index 0000000000..c735a9a4d6 --- /dev/null +++ b/corebinutils/test/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2026 + Project Tick. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. 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. +3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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. diff --git a/corebinutils/test/README.md b/corebinutils/test/README.md new file mode 100644 index 0000000000..cca3353d99 --- /dev/null +++ b/corebinutils/test/README.md @@ -0,0 +1,47 @@ +# test + +Standalone Linux-native port of FreeBSD `test(1)` / `[(1)` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port Strategy + +- The standalone layout follows sibling ports such as `bin/expr` and `bin/pwd`: local `GNUmakefile`, short technical `README.md`, and shell regression tests under `tests/`. +- The FreeBSD shared-source shape (`/bin/test` plus `/bin/sh` builtin hooks) was removed. This port is a standalone utility and keeps the expression parser in one Linux-focused source file. +- The original recursive-descent parser and POSIX ambiguity handling are preserved, but the parser state is moved into an explicit `struct parser` instead of global cursor variables. +- BSD libc-only diagnostics (`err(3)`, `__dead2`, `__printf0like`, `__nonstring`) are replaced with plain `fprintf(3)` / `vfprintf(3)` error paths so the code builds cleanly on glibc and musl. + +## Linux API Mapping + +| FreeBSD / BSD mechanism | Linux-native replacement | Reasoning | +|---|---|---| +| `eaccess(2)` for `-r`, `-w`, `-x` | `faccessat(AT_FDCWD, ..., AT_EACCESS)` | Linux exposes effective-ID permission checks through `faccessat` without a BSD shim layer | +| `-e` existence check | `stat(2)` / `lstat(2)` result | existence does not need a separate access probe on Linux | +| `err(3)` / `verrx(3)` | local `fprintf(3)` / `vfprintf(3)` diagnostics | removes BSD libc dependency and keeps exact exit status control | +| shared `/bin/sh` builtin glue (`#ifdef SHELL`, `bltin.h`) | removed | target deliverable here is the standalone userland tool, not a shell builtin | +| implicit parser globals (`nargc`, `t_wp`, `parenlevel`) | `struct parser` state object | reduces global state and makes ambiguity handling easier to audit | +| `stat(2)` / `lstat(2)` timestamp compare with BSD struct access | Linux `st_mtim` comparison | POSIX.1-2008 / Linux-compatible nanosecond mtime comparison for `-nt` and `-ot` | + +## Supported Semantics + +- All primaries documented in [`test.1`](/home/samet/freebsd/bin/test/test.1), including `-L`, `-h`, `-S`, `-O`, `-G`, `-nt`, `-ot`, `-ef`, and the compatibility `==` operator. +- Ambiguous POSIX grammar cases handled the same way as the historical FreeBSD parser, including the non-short-circuit `-a` / `-o` behaviour documented in `BUGS`. +- `[` is built as a symlink to `test`; missing closing `]` is a hard error. +- Numeric parsing is strict: trailing garbage is rejected, and values outside `intmax_t` or file-descriptor `int` range fail explicitly. + +## Intentionally Unsupported Semantics + +- The shared-source `/bin/sh` builtin integration is not carried into this standalone Linux port. That is a shell-porting concern, not a `test(1)` binary concern. +- If the running Linux libc/kernel combination cannot provide `faccessat(..., AT_EACCESS)`, the port exits with an explicit error instead of silently approximating effective-ID permission checks. diff --git a/corebinutils/test/test.1 b/corebinutils/test/test.1 new file mode 100644 index 0000000000..04a39a3c7e --- /dev/null +++ b/corebinutils/test/test.1 @@ -0,0 +1,392 @@ +.\"- +.\" Copyright (c) 1991, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" the Institute of Electrical and Electronics Engineers, Inc. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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. +.\" +.Dd October 5, 2016 +.Dt TEST 1 +.Os +.Sh NAME +.Nm test , +.Nm \&[ +.Nd condition evaluation utility +.Sh SYNOPSIS +.Nm +.Ar expression +.Nm \&[ +.Ar expression Cm \&] +.Sh DESCRIPTION +The +.Nm +utility evaluates the expression and, if it evaluates +to true, returns a zero (true) exit status; otherwise +it returns 1 (false). +If there is no expression, +.Nm +also +returns 1 (false). +.Pp +All operators and flags are separate arguments to the +.Nm +utility. +.Pp +The following primaries are used to construct expression: +.Bl -tag -width Ar +.It Fl b Ar file +True if +.Ar file +exists and is a block special +file. +.It Fl c Ar file +True if +.Ar file +exists and is a character +special file. +.It Fl d Ar file +True if +.Ar file +exists and is a directory. +.It Fl e Ar file +True if +.Ar file +exists (regardless of type). +.It Fl f Ar file +True if +.Ar file +exists and is a regular file. +.It Fl g Ar file +True if +.Ar file +exists and its set group ID flag +is set. +.It Fl h Ar file +True if +.Ar file +exists and is a symbolic link. +This operator is retained for compatibility with previous versions of +this program. +Do not rely on its existence; use +.Fl L +instead. +.It Fl k Ar file +True if +.Ar file +exists and its sticky bit is set. +.It Fl n Ar string +True if the length of +.Ar string +is nonzero. +.It Fl p Ar file +True if +.Ar file +is a named pipe +.Pq Tn FIFO . +.It Fl r Ar file +True if +.Ar file +exists and is readable. +.It Fl s Ar file +True if +.Ar file +exists and has a size greater +than zero. +.It Fl t Ar file_descriptor +True if the file whose file descriptor number +is +.Ar file_descriptor +is open and is associated with a terminal. +.It Fl u Ar file +True if +.Ar file +exists and its set user ID flag +is set. +.It Fl w Ar file +True if +.Ar file +exists and is writable. +True +indicates only that the write flag is on. +The file is not writable on a read-only file +system even if this test indicates true. +.It Fl x Ar file +True if +.Ar file +exists and is executable. +True +indicates only that the execute flag is on. +If +.Ar file +is a directory, true indicates that +.Ar file +can be searched. +.It Fl z Ar string +True if the length of +.Ar string +is zero. +.It Fl L Ar file +True if +.Ar file +exists and is a symbolic link. +.It Fl O Ar file +True if +.Ar file +exists and its owner matches the effective user id of this process. +.It Fl G Ar file +True if +.Ar file +exists and its group matches the effective group id of this process. +.It Fl S Ar file +True if +.Ar file +exists and is a socket. +.It Ar file1 Fl nt Ar file2 +True if +.Ar file1 +exists and is newer than +.Ar file2 . +.It Ar file1 Fl ot Ar file2 +True if +.Ar file1 +exists and is older than +.Ar file2 . +.It Ar file1 Fl ef Ar file2 +True if +.Ar file1 +and +.Ar file2 +exist and refer to the same file. +.It Ar string +True if +.Ar string +is not the null +string. +.It Ar s1 Cm = Ar s2 +True if the strings +.Ar s1 +and +.Ar s2 +are identical. +.It Ar s1 Cm != Ar s2 +True if the strings +.Ar s1 +and +.Ar s2 +are not identical. +.It Ar s1 Cm < Ar s2 +True if string +.Ar s1 +comes before +.Ar s2 +based on the binary value of their characters. +.It Ar s1 Cm > Ar s2 +True if string +.Ar s1 +comes after +.Ar s2 +based on the binary value of their characters. +.It Ar n1 Fl eq Ar n2 +True if the integers +.Ar n1 +and +.Ar n2 +are algebraically +equal. +.It Ar n1 Fl ne Ar n2 +True if the integers +.Ar n1 +and +.Ar n2 +are not +algebraically equal. +.It Ar n1 Fl gt Ar n2 +True if the integer +.Ar n1 +is algebraically +greater than the integer +.Ar n2 . +.It Ar n1 Fl ge Ar n2 +True if the integer +.Ar n1 +is algebraically +greater than or equal to the integer +.Ar n2 . +.It Ar n1 Fl lt Ar n2 +True if the integer +.Ar n1 +is algebraically less +than the integer +.Ar n2 . +.It Ar n1 Fl le Ar n2 +True if the integer +.Ar n1 +is algebraically less +than or equal to the integer +.Ar n2 . +.El +.Pp +If +.Ar file +is a symbolic link, +.Nm +will fully dereference it and then evaluate the expression +against the file referenced, except for the +.Fl h +and +.Fl L +primaries. +.Pp +These primaries can be combined with the following operators: +.Bl -tag -width Ar +.It Cm \&! Ar expression +True if +.Ar expression +is false. +.It Ar expression1 Fl a Ar expression2 +True if both +.Ar expression1 +and +.Ar expression2 +are true. +.It Ar expression1 Fl o Ar expression2 +True if either +.Ar expression1 +or +.Ar expression2 +are true. +.It Cm \&( Ar expression Cm \&) +True if expression is true. +.El +.Pp +The +.Fl a +operator has higher precedence than the +.Fl o +operator. +.Pp +Some shells may provide a builtin +.Nm +command which is similar or identical to this utility. +Consult the +.Xr builtin 1 +manual page. +.Sh GRAMMAR AMBIGUITY +The +.Nm +grammar is inherently ambiguous. +In order to assure a degree of consistency, +the cases described in the +.St -p1003.2 , +section D11.2/4.62.4, standard +are evaluated consistently according to the rules specified in the +standards document. +All other cases are subject to the ambiguity in the +command semantics. +.Pp +In particular, only expressions containing +.Fl a , +.Fl o , +.Cm \&( +or +.Cm \&) +can be ambiguous. +.Sh EXIT STATUS +The +.Nm +utility exits with one of the following values: +.Bl -tag -width indent +.It 0 +expression evaluated to true. +.It 1 +expression evaluated to false or expression was +missing. +.It >1 +An error occurred. +.El +.Sh EXAMPLES +Implement +.Li test FILE1 -nt FILE2 +using only +.Tn POSIX +functionality: +.Pp +.Dl test -n \&"$(find -L -- FILE1 -prune -newer FILE2 2>/dev/null)\&" +.Pp +This can be modified using non-standard +.Xr find 1 +primaries like +.Cm -newerca +to compare other timestamps. +.Sh COMPATIBILITY +For compatibility with some other implementations, +the +.Cm = +primary can be substituted with +.Cm == +with the same meaning. +.Sh SEE ALSO +.Xr builtin 1 , +.Xr expr 1 , +.Xr find 1 , +.Xr sh 1 , +.Xr stat 1 , +.Xr symlink 7 +.Sh STANDARDS +The +.Nm +utility implements a superset of the +.St -p1003.2 +specification. +The primaries +.Cm < , +.Cm == , +.Cm > , +.Fl ef , +.Fl nt , +.Fl ot , +.Fl G , +and +.Fl O +are extensions. +.Sh HISTORY +A +.Nm +utility appeared in +.At v7 . +.Sh BUGS +Both sides are always evaluated in +.Fl a +and +.Fl o . +For instance, the writable status of +.Pa file +will be tested by the following command even though the former expression +indicated false, which results in a gratuitous access to the file system: +.Dl "[ -z abc -a -w file ]" +To avoid this, write +.Dl "[ -z abc ] && [ -w file ]" diff --git a/corebinutils/test/test.c b/corebinutils/test/test.c new file mode 100644 index 0000000000..8159a9f0fd --- /dev/null +++ b/corebinutils/test/test.c @@ -0,0 +1,679 @@ +/* + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (c) 2026 + Project Tick. All rights reserved. + +This code is derived from software contributed to Berkeley by +the Institute of Electrical and Electronics Engineers, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. 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. +3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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. + +*/ + +#include <sys/stat.h> +#include <sys/types.h> + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <limits.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +enum token_type { + TOKEN_UNARY = 0x100, + TOKEN_BINARY = 0x200, + TOKEN_BOOLEAN_UNARY = 0x300, + TOKEN_BOOLEAN_BINARY = 0x400, + TOKEN_PAREN = 0x500 +}; + +enum token { + TOKEN_EOI, + TOKEN_OPERAND, + TOKEN_FILRD = TOKEN_UNARY + 1, + TOKEN_FILWR, + TOKEN_FILEX, + TOKEN_FILEXIST, + TOKEN_FILREG, + TOKEN_FILDIR, + TOKEN_FILCDEV, + TOKEN_FILBDEV, + TOKEN_FILFIFO, + TOKEN_FILSOCK, + TOKEN_FILSYM, + TOKEN_FILGZ, + TOKEN_FILTT, + TOKEN_FILSUID, + TOKEN_FILSGID, + TOKEN_FILSTCK, + TOKEN_STREZ, + TOKEN_STRNZ, + TOKEN_FILUID, + TOKEN_FILGID, + TOKEN_FILNT = TOKEN_BINARY + 1, + TOKEN_FILOT, + TOKEN_FILEQ, + TOKEN_STREQ, + TOKEN_STRNE, + TOKEN_STRLT, + TOKEN_STRGT, + TOKEN_INTEQ, + TOKEN_INTNE, + TOKEN_INTGE, + TOKEN_INTGT, + TOKEN_INTLE, + TOKEN_INTLT, + TOKEN_UNOT = TOKEN_BOOLEAN_UNARY + 1, + TOKEN_BAND = TOKEN_BOOLEAN_BINARY + 1, + TOKEN_BOR, + TOKEN_LPAREN = TOKEN_PAREN + 1, + TOKEN_RPAREN +}; + +#define TOKEN_FAMILY(token) ((token) & 0xff00) + +struct operator { + const char *text; + enum token token; +}; + +struct parser { + char **argv; + int pos; + int remaining; + int paren_level; +}; + +static const struct operator ops_single[] = { + {"=", TOKEN_STREQ}, + {"<", TOKEN_STRLT}, + {">", TOKEN_STRGT}, + {"!", TOKEN_UNOT}, + {"(", TOKEN_LPAREN}, + {")", TOKEN_RPAREN}, +}; + +static const struct operator ops_dash_single[] = { + {"r", TOKEN_FILRD}, + {"w", TOKEN_FILWR}, + {"x", TOKEN_FILEX}, + {"e", TOKEN_FILEXIST}, + {"f", TOKEN_FILREG}, + {"d", TOKEN_FILDIR}, + {"c", TOKEN_FILCDEV}, + {"b", TOKEN_FILBDEV}, + {"p", TOKEN_FILFIFO}, + {"u", TOKEN_FILSUID}, + {"g", TOKEN_FILSGID}, + {"k", TOKEN_FILSTCK}, + {"s", TOKEN_FILGZ}, + {"t", TOKEN_FILTT}, + {"z", TOKEN_STREZ}, + {"n", TOKEN_STRNZ}, + {"h", TOKEN_FILSYM}, + {"O", TOKEN_FILUID}, + {"G", TOKEN_FILGID}, + {"L", TOKEN_FILSYM}, + {"S", TOKEN_FILSOCK}, + {"a", TOKEN_BAND}, + {"o", TOKEN_BOR}, +}; + +static const struct operator ops_double[] = { + {"==", TOKEN_STREQ}, + {"!=", TOKEN_STRNE}, +}; + +static const struct operator ops_dash_double[] = { + {"eq", TOKEN_INTEQ}, + {"ne", TOKEN_INTNE}, + {"ge", TOKEN_INTGE}, + {"gt", TOKEN_INTGT}, + {"le", TOKEN_INTLE}, + {"lt", TOKEN_INTLT}, + {"nt", TOKEN_FILNT}, + {"ot", TOKEN_FILOT}, + {"ef", TOKEN_FILEQ}, +}; + +static const char *program_name = "test"; + +__attribute__((format(printf, 1, 2), noreturn)) +static void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + fprintf(stderr, "%s: ", program_name); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); + va_end(ap); + exit(2); +} + +static void +syntax_error(const char *op, const char *msg) +{ + if (op != NULL && *op != '\0') + die("%s: %s", op, msg); + die("%s", msg); +} + +static const char * +base_name(const char *path) +{ + const char *slash; + + slash = strrchr(path, '/'); + return slash == NULL ? path : slash + 1; +} + +static const char * +current_arg(const struct parser *parser) +{ + if (parser->remaining <= 0) + return NULL; + return parser->argv[parser->pos]; +} + +static const char * +peek_arg(const struct parser *parser, int offset) +{ + if (offset < 0 || offset >= parser->remaining) + return NULL; + return parser->argv[parser->pos + offset]; +} + +static const char * +advance_arg(struct parser *parser) +{ + if (parser->remaining > 0) { + parser->pos++; + parser->remaining--; + } + return current_arg(parser); +} + +static void +rewind_arg(struct parser *parser) +{ + if (parser->pos > 0) { + parser->pos--; + parser->remaining++; + } +} + +static enum token +lookup_operator(const struct operator *ops, size_t count, const char *text) +{ + size_t i; + + for (i = 0; i < count; i++) { + if (strcmp(text, ops[i].text) == 0) + return ops[i].token; + } + return TOKEN_OPERAND; +} + +static enum token +find_operator(const char *text) +{ + size_t length; + + if (text == NULL || text[0] == '\0') + return TOKEN_OPERAND; + + length = strlen(text); + if (length == 1) + return lookup_operator(ops_single, + sizeof(ops_single) / sizeof(ops_single[0]), text); + if (length == 2) { + if (text[0] == '-') + return lookup_operator(ops_dash_single, + sizeof(ops_dash_single) / sizeof(ops_dash_single[0]), + text + 1); + return lookup_operator(ops_double, + sizeof(ops_double) / sizeof(ops_double[0]), text); + } + if (length == 3 && text[0] == '-') { + return lookup_operator(ops_dash_double, + sizeof(ops_dash_double) / sizeof(ops_dash_double[0]), + text + 1); + } + return TOKEN_OPERAND; +} + +static bool +is_unary_operand(const struct parser *parser) +{ + const char *next; + const char *after_next; + enum token next_token; + + if (parser->remaining == 1) + return true; + + next = peek_arg(parser, 1); + if (parser->remaining == 2) + return parser->paren_level == 1 && strcmp(next, ")") == 0; + + after_next = peek_arg(parser, 2); + next_token = find_operator(next); + return TOKEN_FAMILY(next_token) == TOKEN_BINARY && + (parser->paren_level == 0 || strcmp(after_next, ")") != 0); +} + +static bool +is_left_paren_operand(const struct parser *parser) +{ + const char *next; + enum token next_token; + + if (parser->remaining == 1) + return true; + + next = peek_arg(parser, 1); + if (parser->remaining == 2) + return parser->paren_level == 1 && strcmp(next, ")") == 0; + if (parser->remaining != 3) + return false; + + next_token = find_operator(next); + return TOKEN_FAMILY(next_token) == TOKEN_BINARY; +} + +static bool +is_right_paren_operand(const struct parser *parser) +{ + const char *next; + + if (parser->remaining == 1) + return false; + + next = peek_arg(parser, 1); + if (parser->remaining == 2) + return parser->paren_level == 1 && strcmp(next, ")") == 0; + return false; +} + +static enum token +lex_token(struct parser *parser, const char *text) +{ + enum token token; + + if (text == NULL) + return TOKEN_EOI; + + token = find_operator(text); + if (((TOKEN_FAMILY(token) == TOKEN_UNARY || + TOKEN_FAMILY(token) == TOKEN_BOOLEAN_UNARY) && + is_unary_operand(parser)) || + (token == TOKEN_LPAREN && is_left_paren_operand(parser)) || + (token == TOKEN_RPAREN && is_right_paren_operand(parser))) { + return TOKEN_OPERAND; + } + return token; +} + +static int +parse_int(const char *text) +{ + char *end; + intmax_t value; + + errno = 0; + value = strtoimax(text, &end, 10); + if (end == text) + die("%s: bad number", text); + if (errno == ERANGE || value < INT_MIN || value > INT_MAX) + die("%s: out of range", text); + + while (*end != '\0' && isspace((unsigned char)*end)) + end++; + if (*end != '\0') + die("%s: bad number", text); + + return (int)value; +} + +static intmax_t +parse_intmax(const char *text) +{ + char *end; + intmax_t value; + + errno = 0; + value = strtoimax(text, &end, 10); + if (end == text) + die("%s: bad number", text); + if (errno == ERANGE) + die("%s: out of range", text); + + while (*end != '\0' && isspace((unsigned char)*end)) + end++; + if (*end != '\0') + die("%s: bad number", text); + + return value; +} + +static int +effective_access(const char *path, int mode) +{ + if (faccessat(AT_FDCWD, path, mode, AT_EACCESS) == 0) + return 0; + if (errno == EINVAL || errno == ENOSYS) + die("Linux effective access checks require faccessat(AT_EACCESS)"); + return -1; +} + +static int +compare_mtime(const struct stat *lhs, const struct stat *rhs) +{ + if (lhs->st_mtim.tv_sec > rhs->st_mtim.tv_sec) + return 1; + if (lhs->st_mtim.tv_sec < rhs->st_mtim.tv_sec) + return -1; + if (lhs->st_mtim.tv_nsec > rhs->st_mtim.tv_nsec) + return 1; + if (lhs->st_mtim.tv_nsec < rhs->st_mtim.tv_nsec) + return -1; + return 0; +} + +static int +newer_file(const char *lhs, const char *rhs) +{ + struct stat lhs_stat; + struct stat rhs_stat; + + if (stat(lhs, &lhs_stat) != 0 || stat(rhs, &rhs_stat) != 0) + return 0; + return compare_mtime(&lhs_stat, &rhs_stat) > 0; +} + +static int +older_file(const char *lhs, const char *rhs) +{ + return newer_file(rhs, lhs); +} + +static int +same_file(const char *lhs, const char *rhs) +{ + struct stat lhs_stat; + struct stat rhs_stat; + + return stat(lhs, &lhs_stat) == 0 && + stat(rhs, &rhs_stat) == 0 && + lhs_stat.st_dev == rhs_stat.st_dev && + lhs_stat.st_ino == rhs_stat.st_ino; +} + +static int +evaluate_file_test(const char *path, enum token token) +{ + struct stat st; + int stat_result; + + stat_result = token == TOKEN_FILSYM ? lstat(path, &st) : stat(path, &st); + if (stat_result != 0) + return 0; + + switch (token) { + case TOKEN_FILRD: + return effective_access(path, R_OK) == 0; + case TOKEN_FILWR: + return effective_access(path, W_OK) == 0; + case TOKEN_FILEX: + if (effective_access(path, X_OK) != 0) + return 0; + if (S_ISDIR(st.st_mode) || geteuid() != 0) + return 1; + return (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0; + case TOKEN_FILEXIST: + return 1; + case TOKEN_FILREG: + return S_ISREG(st.st_mode); + case TOKEN_FILDIR: + return S_ISDIR(st.st_mode); + case TOKEN_FILCDEV: + return S_ISCHR(st.st_mode); + case TOKEN_FILBDEV: + return S_ISBLK(st.st_mode); + case TOKEN_FILFIFO: + return S_ISFIFO(st.st_mode); + case TOKEN_FILSOCK: + return S_ISSOCK(st.st_mode); + case TOKEN_FILSYM: + return S_ISLNK(st.st_mode); + case TOKEN_FILSUID: + return (st.st_mode & S_ISUID) != 0; + case TOKEN_FILSGID: + return (st.st_mode & S_ISGID) != 0; + case TOKEN_FILSTCK: + return (st.st_mode & S_ISVTX) != 0; + case TOKEN_FILGZ: + return st.st_size > 0; + case TOKEN_FILUID: + return st.st_uid == geteuid(); + case TOKEN_FILGID: + return st.st_gid == getegid(); + default: + return 0; + } +} + +static int +compare_integers(const char *lhs, const char *rhs) +{ + intmax_t lhs_value; + intmax_t rhs_value; + + lhs_value = parse_intmax(lhs); + rhs_value = parse_intmax(rhs); + if (lhs_value > rhs_value) + return 1; + if (lhs_value < rhs_value) + return -1; + return 0; +} + +static int parse_oexpr(struct parser *parser, enum token token); + +static int +parse_binop(struct parser *parser, enum token token) +{ + const char *lhs; + const char *op; + const char *rhs; + + lhs = current_arg(parser); + advance_arg(parser); + op = current_arg(parser); + advance_arg(parser); + rhs = current_arg(parser); + if (rhs == NULL) + syntax_error(op, "argument expected"); + + switch (token) { + case TOKEN_STREQ: + return strcmp(lhs, rhs) == 0; + case TOKEN_STRNE: + return strcmp(lhs, rhs) != 0; + case TOKEN_STRLT: + return strcmp(lhs, rhs) < 0; + case TOKEN_STRGT: + return strcmp(lhs, rhs) > 0; + case TOKEN_INTEQ: + return compare_integers(lhs, rhs) == 0; + case TOKEN_INTNE: + return compare_integers(lhs, rhs) != 0; + case TOKEN_INTGE: + return compare_integers(lhs, rhs) >= 0; + case TOKEN_INTGT: + return compare_integers(lhs, rhs) > 0; + case TOKEN_INTLE: + return compare_integers(lhs, rhs) <= 0; + case TOKEN_INTLT: + return compare_integers(lhs, rhs) < 0; + case TOKEN_FILNT: + return newer_file(lhs, rhs); + case TOKEN_FILOT: + return older_file(lhs, rhs); + case TOKEN_FILEQ: + return same_file(lhs, rhs); + default: + abort(); + } +} + +static int +parse_primary(struct parser *parser, enum token token) +{ + enum token next_token; + const char *operand; + int result; + + if (token == TOKEN_EOI) + return 0; + + if (token == TOKEN_LPAREN) { + parser->paren_level++; + next_token = lex_token(parser, advance_arg(parser)); + if (next_token == TOKEN_RPAREN) { + parser->paren_level--; + return 0; + } + result = parse_oexpr(parser, next_token); + if (lex_token(parser, advance_arg(parser)) != TOKEN_RPAREN) + syntax_error(NULL, "closing paren expected"); + parser->paren_level--; + return result; + } + + if (TOKEN_FAMILY(token) == TOKEN_UNARY) { + if (parser->remaining <= 1) + syntax_error(NULL, "argument expected"); + operand = advance_arg(parser); + switch (token) { + case TOKEN_STREZ: + return operand[0] == '\0'; + case TOKEN_STRNZ: + return operand[0] != '\0'; + case TOKEN_FILTT: + return isatty(parse_int(operand)); + default: + return evaluate_file_test(operand, token); + } + } + + next_token = lex_token(parser, peek_arg(parser, 1)); + if (TOKEN_FAMILY(next_token) == TOKEN_BINARY) + return parse_binop(parser, next_token); + + return current_arg(parser)[0] != '\0'; +} + +static int +parse_nexpr(struct parser *parser, enum token token) +{ + if (token == TOKEN_UNOT) + return !parse_nexpr(parser, lex_token(parser, advance_arg(parser))); + return parse_primary(parser, token); +} + +static int +parse_aexpr(struct parser *parser, enum token token) +{ + int result; + + result = parse_nexpr(parser, token); + if (lex_token(parser, advance_arg(parser)) == TOKEN_BAND) + return parse_aexpr(parser, lex_token(parser, advance_arg(parser))) && + result; + rewind_arg(parser); + return result; +} + +static int +parse_oexpr(struct parser *parser, enum token token) +{ + int result; + + result = parse_aexpr(parser, token); + if (lex_token(parser, advance_arg(parser)) == TOKEN_BOR) + return parse_oexpr(parser, lex_token(parser, advance_arg(parser))) || + result; + rewind_arg(parser); + return result; +} + +int +main(int argc, char **argv) +{ + struct parser parser; + int result; + + program_name = base_name(argv[0]); + if (strcmp(program_name, "[") == 0) { + if (argc == 1 || strcmp(argv[argc - 1], "]") != 0) + die("missing ']'"); + argc--; + argv[argc] = NULL; + } + + if (argc <= 1) + return 1; + + parser.argv = argv + 1; + parser.pos = 0; + parser.remaining = argc - 1; + parser.paren_level = 0; + + if (parser.remaining == 4 && strcmp(current_arg(&parser), "!") == 0) { + advance_arg(&parser); + result = parse_oexpr(&parser, + lex_token(&parser, current_arg(&parser))); + } else { + result = !parse_oexpr(&parser, + lex_token(&parser, current_arg(&parser))); + } + + advance_arg(&parser); + if (parser.remaining > 0) + syntax_error(current_arg(&parser), "unexpected operator"); + + return result; +} diff --git a/corebinutils/test/tests/fd_helper.c b/corebinutils/test/tests/fd_helper.c new file mode 100644 index 0000000000..27d81f7164 --- /dev/null +++ b/corebinutils/test/tests/fd_helper.c @@ -0,0 +1,111 @@ +/* + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (c) 2026 + Project Tick. All rights reserved. + +This code is derived from software contributed to Berkeley by +the Institute of Electrical and Electronics Engineers, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. 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. +3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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. + +*/ + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static void +die_errno(const char *what) +{ + fprintf(stderr, "fd_helper: %s: %s\n", what, strerror(errno)); + exit(126); +} + +static int +parse_fd(const char *text) +{ + char *end; + long value; + + errno = 0; + value = strtol(text, &end, 10); + if (end == text || *end != '\0' || errno == ERANGE || + value < 0 || value > INT_MAX) { + fprintf(stderr, "fd_helper: invalid file descriptor: %s\n", text); + exit(126); + } + return (int)value; +} + +int +main(int argc, char **argv) +{ + int fd; + int master_fd; + int slave_fd; + char *slave_name; + + if (argc < 4) { + fprintf(stderr, "usage: fd_helper fd program arg ...\n"); + return 126; + } + + fd = parse_fd(argv[1]); + + master_fd = posix_openpt(O_RDWR | O_NOCTTY); + if (master_fd < 0) + die_errno("posix_openpt"); + if (grantpt(master_fd) != 0) + die_errno("grantpt"); + if (unlockpt(master_fd) != 0) + die_errno("unlockpt"); + + slave_name = ptsname(master_fd); + if (slave_name == NULL) + die_errno("ptsname"); + + slave_fd = open(slave_name, O_RDWR | O_NOCTTY); + if (slave_fd < 0) + die_errno("open slave pty"); + + if (dup2(slave_fd, fd) < 0) + die_errno("dup2"); + + if (!isatty(fd)) + die_errno("isatty"); + + if (slave_fd != fd) + close(slave_fd); + close(master_fd); + + execv(argv[2], &argv[2]); + die_errno("execv"); +} diff --git a/corebinutils/test/tests/legacy_test.sh b/corebinutils/test/tests/legacy_test.sh new file mode 100644 index 0000000000..94f51aefba --- /dev/null +++ b/corebinutils/test/tests/legacy_test.sh @@ -0,0 +1,209 @@ +#!/bin/sh + +set -eu + +#- +# Copyright (c) June 1996 Wolfram Schneider <wosch@FreeBSD.org>. Berlin. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +# +# TEST.sh - check if test(1) or builtin test works +# + +# force a specified test program, e.g. `env TEST_BIN=/bin/test sh legacy_test.sh' +TEST_BIN=${TEST_BIN:-test} +FAILED=0 + +t () +{ + # $1 -> exit code + # $2 -> $test expression + + count=$((count+1)) + # check for syntax errors + if syntax=$(eval "\"$TEST_BIN\" $2" 2>&1); then + ret=0 + else + ret=$? + fi + if test -n "$syntax"; then + printf "not ok %s - (syntax error)\n" "$count $2" + FAILED=1 + elif [ "$ret" != "$1" ]; then + printf "not ok %s - (got $ret, expected $1)\n" "$count $2" + FAILED=1 + else + printf "ok %s\n" "$count $2" + fi +} + +count=0 +echo "1..130" + +t 0 'b = b' +t 0 'b == b' +t 1 'b != b' +t 0 '\( b = b \)' +t 0 '\( b == b \)' +t 1 '! \( b = b \)' +t 1 '! \( b == b \)' +t 1 '! -f /etc/passwd' + +t 0 '-h = -h' +t 0 '-o = -o' +t 1 '-f = h' +t 1 '-h = f' +t 1 '-o = f' +t 1 'f = -o' +t 0 '\( -h = -h \)' +t 1 '\( a = -h \)' +t 1 '\( -f = h \)' +t 0 '-h = -h -o a' +t 0 '\( -h = -h \) -o 1' +t 0 '-h = -h -o -h = -h' +t 0 '\( -h = -h \) -o \( -h = -h \)' +t 0 'roedelheim = roedelheim' +t 1 'potsdam = berlin-dahlem' + +t 0 '-d /' +t 0 '-d / -a a != b' +t 1 '-z "-z"' +t 0 '-n -n' + +t 0 '0' +t 0 '\( 0 \)' +t 0 '-E' +t 0 '-X -a -X' +t 0 '-XXX' +t 0 '\( -E \)' +t 0 'true -o X' +t 0 'true -o -X' +t 0 '\( \( \( a = a \) -o 1 \) -a 1 \) -a true' +t 1 '-h /' +t 0 '-r /' +t 1 '-w /' +t 0 '-x /bin/sh' +t 0 '-c /dev/null' +t 0 '-f /etc/passwd' +t 0 '-s /etc/passwd' + +t 1 '! \( 700 -le 1000 -a -n "1" -a "20" = "20" \)' +t 0 '100 -eq 100' +t 0 '100 -lt 200' +t 1 '1000 -lt 200' +t 0 '1000 -gt 200' +t 0 '1000 -ge 200' +t 0 '1000 -ge 1000' +t 1 '2 -ne 2' +t 0 '0 -eq 0' +t 1 '-5 -eq 5' +t 0 '\( 0 -eq 0 \)' +t 1 '1 -eq 0 -o a = a -a 1 -eq 0 -o a = aa' + +t 1 '"" -o ""' +t 1 '"" -a ""' +t 1 '"a" -a ""' +t 0 '"a" -a ! ""' +t 1 '""' +t 0 '! ""' + +t 0 '!' +t 0 '\(' +t 0 '\)' + +t 1 '\( = \)' +t 0 '\( != \)' +t 0 '\( ! \)' +t 0 '\( \( \)' +t 0 '\( \) \)' +t 0 '! = !' +t 1 '! != !' +t 1 '-n = \)' +t 0 '! != \)' +t 1 '! = a' +t 0 '! != -n' +t 0 '! -c /etc/passwd' + +t 1 '! = = =' +t 0 '! = = \)' +t 0 '! "" -o ""' +t 1 '! "x" -o ""' +t 1 '! "" -o "x"' +t 1 '! "x" -o "x"' +t 0 '\( -f /etc/passwd \)' +t 0 '\( ! "" \)' +t 1 '\( ! -e \)' + +t 0 '0 -eq 0 -a -d /' +t 0 '-s = "" -o "" = ""' +t 0 '"" = "" -o -s = ""' +t 1 '-s = "" -o -s = ""' +t 0 '-z x -o x = "#" -o x = x' +t 1 '-z y -o y = "#" -o y = x' +t 0 '0 -ne 0 -o ! -f /' +t 0 '1 -ne 0 -o ! -f /etc/passwd' +t 1 '0 -ne 0 -o ! -f /etc/passwd' + +t 0 '-n =' +t 1 '-z =' +t 1 '! =' +t 0 '-n -eq' +t 1 '-z -eq' +t 1 '! -eq' +t 0 '-n -a' +t 1 '-z -a' +t 1 '! -a' +t 0 '-n -o' +t 1 '-z -o' +t 1 '! -o' +t 1 '! -n =' +t 0 '! -z =' +t 0 '! ! =' +t 1 '! -n -eq' +t 0 '! -z -eq' +t 0 '! ! -eq' +t 1 '! -n -a' +t 0 '! -z -a' +t 0 '! ! -a' +t 1 '! -n -o' +t 0 '! -z -o' +t 0 '! ! -o' +t 0 '\( -n = \)' +t 1 '\( -z = \)' +t 1 '\( ! = \)' +t 0 '\( -n -eq \)' +t 1 '\( -z -eq \)' +t 1 '\( ! -eq \)' +t 0 '\( -n -a \)' +t 1 '\( -z -a \)' +t 1 '\( ! -a \)' +t 0 '\( -n -o \)' +t 1 '\( -z -o \)' +t 1 '\( ! -o \)' + +if [ "$FAILED" -ne 0 ]; then + exit 1 +fi + +printf '%s\n' "PASS" diff --git a/corebinutils/test/tests/test.sh b/corebinutils/test/tests/test.sh new file mode 100644 index 0000000000..debbbf81a1 --- /dev/null +++ b/corebinutils/test/tests/test.sh @@ -0,0 +1,479 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +TEST_BIN=${TEST_BIN:-"$ROOT/out/test"} +BRACKET_BIN=${BRACKET_BIN:-"$ROOT/out/["} +FD_HELPER_BIN=${FD_HELPER_BIN:-"$ROOT/build/fd_helper"} +SHELL_BIN=$(command -v sh) + +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/test-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +LAST_STATUS=0 +LAST_STDOUT= +LAST_STDERR= + +export LC_ALL=C +export TZ=UTC + +cleanup() { + rm -rf "$WORKDIR" +} + +trap cleanup EXIT INT TERM HUP + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_status() { + name=$1 + expected=$2 + actual=$3 + + if [ "$expected" -ne "$actual" ]; then + printf 'FAIL: %s\n' "$name" >&2 + printf 'expected status: %s\n' "$expected" >&2 + printf 'actual status: %s\n' "$actual" >&2 + exit 1 + fi +} + +assert_eq() { + name=$1 + expected=$2 + actual=$3 + + if [ "$expected" != "$actual" ]; then + printf 'FAIL: %s\n' "$name" >&2 + printf '%s\n' '--- expected ---' >&2 + printf '%s\n' "$expected" >&2 + printf '%s\n' '--- actual ---' >&2 + printf '%s\n' "$actual" >&2 + exit 1 + fi +} + +assert_empty() { + name=$1 + value=$2 + + if [ -n "$value" ]; then + printf 'FAIL: %s\n' "$name" >&2 + printf '%s\n' '--- expected empty ---' >&2 + printf '%s\n' '--- actual ---' >&2 + printf '%s\n' "$value" >&2 + exit 1 + fi +} + +assert_contains() { + name=$1 + value=$2 + pattern=$3 + + case $value in + *"$pattern"*) ;; + *) fail "$name" ;; + esac +} + +run_capture() { + if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +run_in_shell() { + if sh -c "$1" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +find_socket_path() { + for candidate in \ + /run/* /run/*/* /run/*/*/* \ + /var/run/* /var/run/*/* /var/run/*/*/* \ + /tmp/* /tmp/*/* /tmp/*/*/*; do + if [ -S "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + return 1 +} + +skip() { + printf 'SKIP: %s\n' "$1" +} + +[ -x "$TEST_BIN" ] || fail "missing binary: $TEST_BIN" +[ -x "$BRACKET_BIN" ] || fail "missing bracket binary: $BRACKET_BIN" +[ -x "$FD_HELPER_BIN" ] || fail "missing fd helper: $FD_HELPER_BIN" + +run_capture "$TEST_BIN" +assert_status "no expression status" 1 "$LAST_STATUS" +assert_empty "no expression stdout" "$LAST_STDOUT" +assert_empty "no expression stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" "" +assert_status "empty operand status" 1 "$LAST_STATUS" +assert_empty "empty operand stdout" "$LAST_STDOUT" +assert_empty "empty operand stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" value +assert_status "single operand status" 0 "$LAST_STATUS" +assert_empty "single operand stdout" "$LAST_STDOUT" +assert_empty "single operand stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -n "" +assert_status "string -n status" 1 "$LAST_STATUS" +assert_empty "string -n stdout" "$LAST_STDOUT" +assert_empty "string -n stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -z "" +assert_status "string -z status" 0 "$LAST_STATUS" +assert_empty "string -z stdout" "$LAST_STDOUT" +assert_empty "string -z stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -h = -h +assert_status "operator-like operand status" 0 "$LAST_STATUS" +assert_empty "operator-like operand stdout" "$LAST_STDOUT" +assert_empty "operator-like operand stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" alpha = alpha +assert_status "string equality status" 0 "$LAST_STATUS" +assert_empty "string equality stdout" "$LAST_STDOUT" +assert_empty "string equality stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" alpha == alpha +assert_status "string double equals status" 0 "$LAST_STATUS" +assert_empty "string double equals stdout" "$LAST_STDOUT" +assert_empty "string double equals stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" alpha '!=' beta +assert_status "string inequality status" 0 "$LAST_STATUS" +assert_empty "string inequality stdout" "$LAST_STDOUT" +assert_empty "string inequality stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" alpha '<' beta +assert_status "string less-than status" 0 "$LAST_STATUS" +assert_empty "string less-than stdout" "$LAST_STDOUT" +assert_empty "string less-than stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" beta '>' alpha +assert_status "string greater-than status" 0 "$LAST_STATUS" +assert_empty "string greater-than stdout" "$LAST_STDOUT" +assert_empty "string greater-than stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" 100 -eq 100 +assert_status "numeric eq status" 0 "$LAST_STATUS" +assert_empty "numeric eq stdout" "$LAST_STDOUT" +assert_empty "numeric eq stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -5 -lt 5 +assert_status "numeric lt status" 0 "$LAST_STATUS" +assert_empty "numeric lt stdout" "$LAST_STDOUT" +assert_empty "numeric lt stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" 7 -ge 8 +assert_status "numeric false status" 1 "$LAST_STATUS" +assert_empty "numeric false stdout" "$LAST_STDOUT" +assert_empty "numeric false stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" abc -eq 1 +assert_status "bad number status" 2 "$LAST_STATUS" +assert_empty "bad number stdout" "$LAST_STDOUT" +assert_eq "bad number stderr" "test: abc: bad number" "$LAST_STDERR" + +run_capture "$TEST_BIN" 999999999999999999999999999999 -eq 1 +assert_status "out of range status" 2 "$LAST_STATUS" +assert_empty "out of range stdout" "$LAST_STDOUT" +assert_eq "out of range stderr" "test: 999999999999999999999999999999: out of range" "$LAST_STDERR" + +run_capture "$TEST_BIN" '!' "" +assert_status "bang empty status" 0 "$LAST_STATUS" +assert_empty "bang empty stdout" "$LAST_STDOUT" +assert_empty "bang empty stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" '!' +assert_status "bare bang status" 0 "$LAST_STATUS" +assert_empty "bare bang stdout" "$LAST_STDOUT" +assert_empty "bare bang stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" '(' 0 -eq 0 ')' -a '(' 2 -gt 1 ')' +assert_status "paren and status" 0 "$LAST_STATUS" +assert_empty "paren and stdout" "$LAST_STDOUT" +assert_empty "paren and stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" 1 -eq 0 -o a = a -a 1 -eq 0 -o a = aa +assert_status "precedence status" 1 "$LAST_STATUS" +assert_empty "precedence stdout" "$LAST_STDOUT" +assert_empty "precedence stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" '(' 1 = 1 +assert_status "closing paren error status" 2 "$LAST_STATUS" +assert_empty "closing paren error stdout" "$LAST_STDOUT" +assert_eq "closing paren error stderr" "test: closing paren expected" "$LAST_STDERR" + +run_capture "$TEST_BIN" 1 -eq +assert_status "argument expected status" 2 "$LAST_STATUS" +assert_empty "argument expected stdout" "$LAST_STDOUT" +assert_eq "argument expected stderr" "test: -eq: argument expected" "$LAST_STDERR" + +run_capture "$TEST_BIN" one two +assert_status "unexpected operator status" 2 "$LAST_STATUS" +assert_empty "unexpected operator stdout" "$LAST_STDOUT" +assert_eq "unexpected operator stderr" "test: two: unexpected operator" "$LAST_STDERR" + +REGULAR_FILE="$WORKDIR/regular" +EMPTY_FILE="$WORKDIR/empty" +EXECUTABLE_FILE="$WORKDIR/executable" +PERM_FILE="$WORKDIR/no-perm" +FIFO_PATH="$WORKDIR/fifo" +LINK_PATH="$WORKDIR/link" +HARDLINK_PATH="$WORKDIR/hardlink" +DIR_PATH="$WORKDIR/dir" +STICKY_DIR="$WORKDIR/sticky" +OLDER_FILE="$WORKDIR/older" +NEWER_FILE="$WORKDIR/newer" +MODE_FILE="$WORKDIR/mode" + +printf 'payload\n' >"$REGULAR_FILE" +: >"$EMPTY_FILE" +printf '#!/bin/sh\nexit 0\n' >"$EXECUTABLE_FILE" +chmod 0755 "$EXECUTABLE_FILE" +: >"$PERM_FILE" +chmod 0000 "$PERM_FILE" +mkfifo "$FIFO_PATH" +ln -s "$REGULAR_FILE" "$LINK_PATH" +ln "$REGULAR_FILE" "$HARDLINK_PATH" +mkdir "$DIR_PATH" +mkdir "$STICKY_DIR" +chmod 1777 "$STICKY_DIR" +: >"$MODE_FILE" +chmod 6755 "$MODE_FILE" +: >"$OLDER_FILE" +sleep 1 +: >"$NEWER_FILE" + +run_capture "$TEST_BIN" -e "$REGULAR_FILE" +assert_status "file exists status" 0 "$LAST_STATUS" +assert_empty "file exists stdout" "$LAST_STDOUT" +assert_empty "file exists stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -f "$REGULAR_FILE" +assert_status "regular file status" 0 "$LAST_STATUS" +assert_empty "regular file stdout" "$LAST_STDOUT" +assert_empty "regular file stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -d "$DIR_PATH" +assert_status "directory status" 0 "$LAST_STATUS" +assert_empty "directory stdout" "$LAST_STDOUT" +assert_empty "directory stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -s "$REGULAR_FILE" +assert_status "size greater than zero status" 0 "$LAST_STATUS" +assert_empty "size greater than zero stdout" "$LAST_STDOUT" +assert_empty "size greater than zero stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -s "$EMPTY_FILE" +assert_status "size zero status" 1 "$LAST_STATUS" +assert_empty "size zero stdout" "$LAST_STDOUT" +assert_empty "size zero stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -p "$FIFO_PATH" +assert_status "fifo status" 0 "$LAST_STATUS" +assert_empty "fifo stdout" "$LAST_STDOUT" +assert_empty "fifo stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -L "$LINK_PATH" +assert_status "symlink L status" 0 "$LAST_STATUS" +assert_empty "symlink L stdout" "$LAST_STDOUT" +assert_empty "symlink L stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -h "$LINK_PATH" +assert_status "symlink h status" 0 "$LAST_STATUS" +assert_empty "symlink h stdout" "$LAST_STDOUT" +assert_empty "symlink h stderr" "$LAST_STDERR" + +SOCKET_PATH=$(find_socket_path || true) +if [ -n "$SOCKET_PATH" ]; then + run_capture "$TEST_BIN" -S "$SOCKET_PATH" + assert_status "socket status" 0 "$LAST_STATUS" + assert_empty "socket stdout" "$LAST_STDOUT" + assert_empty "socket stderr" "$LAST_STDERR" +else + skip "socket positive test skipped because no UNIX socket path is visible" +fi + +run_capture "$TEST_BIN" -O "$REGULAR_FILE" +assert_status "owner matches euid status" 0 "$LAST_STATUS" +assert_empty "owner matches euid stdout" "$LAST_STDOUT" +assert_empty "owner matches euid stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -G "$REGULAR_FILE" +assert_status "group matches egid status" 0 "$LAST_STATUS" +assert_empty "group matches egid stdout" "$LAST_STDOUT" +assert_empty "group matches egid stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -u "$MODE_FILE" +assert_status "setuid bit status" 0 "$LAST_STATUS" +assert_empty "setuid bit stdout" "$LAST_STDOUT" +assert_empty "setuid bit stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -g "$MODE_FILE" +assert_status "setgid bit status" 0 "$LAST_STATUS" +assert_empty "setgid bit stdout" "$LAST_STDOUT" +assert_empty "setgid bit stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -k "$STICKY_DIR" +assert_status "sticky bit status" 0 "$LAST_STATUS" +assert_empty "sticky bit stdout" "$LAST_STDOUT" +assert_empty "sticky bit stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -r "$REGULAR_FILE" +assert_status "readable status" 0 "$LAST_STATUS" +assert_empty "readable stdout" "$LAST_STDOUT" +assert_empty "readable stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -w "$REGULAR_FILE" +assert_status "writable status" 0 "$LAST_STATUS" +assert_empty "writable stdout" "$LAST_STDOUT" +assert_empty "writable stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -x "$EXECUTABLE_FILE" +assert_status "executable status" 0 "$LAST_STATUS" +assert_empty "executable stdout" "$LAST_STDOUT" +assert_empty "executable stderr" "$LAST_STDERR" + +if [ "$(id -u)" -ne 0 ]; then + run_capture "$TEST_BIN" -r "$PERM_FILE" + assert_status "unreadable status" 1 "$LAST_STATUS" + assert_empty "unreadable stdout" "$LAST_STDOUT" + assert_empty "unreadable stderr" "$LAST_STDERR" + + run_capture "$TEST_BIN" -w "$PERM_FILE" + assert_status "unwritable status" 1 "$LAST_STATUS" + assert_empty "unwritable stdout" "$LAST_STDOUT" + assert_empty "unwritable stderr" "$LAST_STDERR" +else + skip "permission-negative tests skipped for euid 0" +fi + +run_capture "$TEST_BIN" "$NEWER_FILE" -nt "$OLDER_FILE" +assert_status "newer-than status" 0 "$LAST_STATUS" +assert_empty "newer-than stdout" "$LAST_STDOUT" +assert_empty "newer-than stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" "$OLDER_FILE" -ot "$NEWER_FILE" +assert_status "older-than status" 0 "$LAST_STATUS" +assert_empty "older-than stdout" "$LAST_STDOUT" +assert_empty "older-than stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" "$REGULAR_FILE" -ef "$HARDLINK_PATH" +assert_status "same file status" 0 "$LAST_STATUS" +assert_empty "same file stdout" "$LAST_STDOUT" +assert_empty "same file stderr" "$LAST_STDERR" + +run_capture "$TEST_BIN" -c /dev/null +assert_status "character device status" 0 "$LAST_STATUS" +assert_empty "character device stdout" "$LAST_STDOUT" +assert_empty "character device stderr" "$LAST_STDERR" + +BLOCK_DEVICE_FOUND= +for candidate in /dev/* /dev/*/*; do + if [ -b "$candidate" ]; then + BLOCK_DEVICE_FOUND=$candidate + break + fi +done +if [ -n "${BLOCK_DEVICE_FOUND:-}" ]; then + run_capture "$TEST_BIN" -b "$BLOCK_DEVICE_FOUND" + assert_status "block device status" 0 "$LAST_STATUS" + assert_empty "block device stdout" "$LAST_STDOUT" + assert_empty "block device stderr" "$LAST_STDERR" +else + skip "block-device positive test skipped because no block device is visible" +fi + +run_capture "$TEST_BIN" -t 99 +assert_status "closed fd tty status" 1 "$LAST_STATUS" +assert_empty "closed fd tty stdout" "$LAST_STDOUT" +assert_empty "closed fd tty stderr" "$LAST_STDERR" + +run_capture "$FD_HELPER_BIN" 9 "$TEST_BIN" -t 9 +case $LAST_STATUS in + 0) + assert_empty "pty fd tty stdout" "$LAST_STDOUT" + assert_empty "pty fd tty stderr" "$LAST_STDERR" + ;; + 1) + run_capture "$FD_HELPER_BIN" 9 "$SHELL_BIN" -c 'test -t 9' + case $LAST_STATUS in + 1) + skip "pty-backed -t positive test skipped because shell test also reports non-tty" + ;; + 126) + case $LAST_STDERR in + *"posix_openpt"*|*"grantpt"*|*"unlockpt"*|*"ptsname"*|*"open slave pty"*|*"isatty"*) + skip "pty-backed -t positive test skipped because PTY checks are blocked" + ;; + *) + fail "pty helper unexpected failure: $LAST_STDERR" + ;; + esac + ;; + *) + fail "pty fd tty status mismatch: test returned 1 but shell test -t returned $LAST_STATUS" + ;; + esac + ;; + 126) + case $LAST_STDERR in + *"posix_openpt"*|*"grantpt"*|*"unlockpt"*|*"ptsname"*|*"open slave pty"*|*"isatty"*) + skip "pty-backed -t positive test skipped because PTY checks are blocked" + ;; + *) + fail "pty helper unexpected failure: $LAST_STDERR" + ;; + esac + ;; + *) + fail "pty helper unexpected status: $LAST_STATUS" + ;; +esac + +run_capture "$BRACKET_BIN" alpha = alpha ']' +assert_status "bracket true status" 0 "$LAST_STATUS" +assert_empty "bracket true stdout" "$LAST_STDOUT" +assert_empty "bracket true stderr" "$LAST_STDERR" + +run_capture "$BRACKET_BIN" ']' +assert_status "bracket empty expression status" 1 "$LAST_STATUS" +assert_empty "bracket empty expression stdout" "$LAST_STDOUT" +assert_empty "bracket empty expression stderr" "$LAST_STDERR" + +run_capture "$BRACKET_BIN" alpha = alpha +assert_status "missing closing bracket status" 2 "$LAST_STATUS" +assert_empty "missing closing bracket stdout" "$LAST_STDOUT" +assert_eq "missing closing bracket stderr" "[: missing ']'" "$LAST_STDERR" + +run_in_shell "TEST_BIN='$TEST_BIN' sh '$ROOT/tests/legacy_test.sh'" +assert_status "legacy suite status" 0 "$LAST_STATUS" +assert_contains "legacy suite stdout" "$LAST_STDOUT" "1..130" +assert_contains "legacy suite stdout" "$LAST_STDOUT" "PASS" +assert_empty "legacy suite stderr" "$LAST_STDERR" + +printf '%s\n' "PASS" |
