commit: a49b8b759cad50d7b325cd9c66bd5d0b0473bf2f
parent 955e8d080f4a2b518fac93bd437fae1882788138
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Fri, 19 Apr 2024 07:45:43 +0200
cmd/pathchk: new
Diffstat:
6 files changed, 263 insertions(+), 3 deletions(-)
diff --git a/cmd/pathchk.1 b/cmd/pathchk.1
@@ -0,0 +1,68 @@
+.\" utils-std: Collection of commonly available Unix tools
+.\" Copyright 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+.\" SPDX-License-Identifier: MPL-2.0
+.Dd 2024-04-19
+.Dt PATHCHK 1
+.Os
+.Sh NAME
+.Nm pathchk
+.Nd check pathname validity
+.Sh SYNOPSIS
+.Nm
+.Op Fl pP
+.Ar pathname ...
+.Sh DESCRIPTION
+.Nm
+checks each
+.Ar pathname
+for their validity and portability.
+.Pp
+By default,
+.Nm
+does the following checks, based on the underlying filesystem:
+.Bl -bullet
+.It
+Isn't longer than PATH_MAX
+.It
+Doesn't have any path component longer than NAME_MAX
+.It
+Is reachable to the current user
+.It
+Doesn't contains invalid bytes
+.El
+.Pp
+Note that non-existing path components aren't seen as an error by
+.Nm .
+.Sh OPTIONS
+.Bl -tag -width ee
+.It Fl p
+Do not perform checks against the underlying filesystem, meaning:
+.Bl -bullet -compact
+.It
+_POSIX_PATH_MAX is used instead of PATH_MAX
+.It
+_POSIX_NAME_MAX is used instead of NAME_MAX
+.It
+Directories aren't checked for user reachability
+.It
+POSIX Portable Character Set is the only reference for invalid byte sequences
+.El
+.It Fl P
+Perform the following additional checks:
+.Bl -bullet -compact
+.It
+Doesn't contains a path component whose first character is the <hyphen-minus> character
+.It
+Isn't empty
+.El
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr test 1
+.Sh STANDARDS
+.Nm
+should be compliant with
+IEEE P1003.1-202x/D4 (“POSIX.1”).
+.Sh AUTHORS
+.An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me
diff --git a/cmd/pathchk.c b/cmd/pathchk.c
@@ -0,0 +1,153 @@
+// utils-std: Collection of commonly available Unix tools
+// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+// SPDX-License-Identifier: MPL-2.0
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h> // PATH_MAX
+#include <stdbool.h>
+#include <stdio.h> // fprintf
+#include <string.h> // strerror
+#include <sys/stat.h> // lstat
+#include <unistd.h> // getopt
+
+// POSIX Portable Character Set
+// Returns 0 on success, or the first invalid character found
+static char
+str_pcs(char *str)
+{
+ for(size_t i = 0; i < strlen(str); i++)
+ {
+ char c = str[i];
+ if(c >= 0x07 && c <= 0x0D) continue;
+ if(c >= 0x20 && c <= 0x7E) continue;
+
+ return c;
+ }
+
+ return 0;
+}
+
+static void
+usage()
+{
+ fprintf(stderr, "Usage: pathchk [-p] [-P] pathname...\n");
+}
+
+int
+main(int argc, char *argv[])
+{
+ bool opt_P = false, opt_p = false;
+ size_t path_max = PATH_MAX;
+ size_t name_max = NAME_MAX;
+
+ int c = -1;
+ while((c = getopt(argc, argv, ":pP")) != -1)
+ {
+ switch(c)
+ {
+ case 'P':
+ opt_P = true;
+ break;
+ case 'p':
+ opt_p = true;
+ path_max = _POSIX_PATH_MAX;
+ name_max = _POSIX_NAME_MAX;
+ break;
+ case ':':
+ fprintf(stderr, "realpath: Error: Missing operand for option: '-%c'\n", optopt);
+ usage();
+ return 1;
+ case '?':
+ fprintf(stderr, "realpath: Error: Unrecognised option: '-%c'\n", optopt);
+ usage();
+ return 1;
+ }
+ }
+
+ argv += optind;
+ argc -= optind;
+
+ if(argc != 1)
+ {
+ usage();
+ return 1;
+ }
+
+ int err = 0;
+
+ for(int i = 0; i < argc; i++)
+ {
+ char *path = argv[i];
+ size_t len = strlen(path);
+
+ if(opt_P && len == 0)
+ {
+ fprintf(stderr, "pathchk: Operand number %d is empty\n", i);
+ err = 1;
+ }
+
+ if(len > path_max)
+ {
+ fprintf(stderr,
+ "pathchk: Path (%zd octets) is over the maximum size (%zd octets): %s\n",
+ len,
+ path_max,
+ path);
+ err = 1;
+ }
+
+ char *p = strtok(path, "/");
+ do
+ {
+ if(p == NULL || p[0] == 0) break;
+ if(p[0] == '-' && opt_P)
+ {
+ fprintf(stderr, "pathchk: Path component starts with an hyphen: %s\n", p);
+ err = 1;
+ }
+ size_t name_len = strlen(p);
+ if(name_len > name_max)
+ {
+ fprintf(stderr,
+ "pathchk: Path component (%zd octets) is over the maximum size (%zd octets): %s\n",
+ name_len,
+ name_max,
+ p);
+ err = 1;
+ }
+ } while((p = strtok(NULL, "/")) != NULL);
+
+ if(!opt_p)
+ {
+ assert(errno == 0);
+ /* flawfinder: ignore, doesn't do any other filesystem interaction afterwards */
+ if(access(path, F_OK) < 0 && errno != ENOENT)
+ {
+ fprintf(stderr,
+ "pathchk: Error checking while checking '%s' against filesystem: %s\n",
+ path,
+ strerror(errno));
+ errno = 0;
+ err = 1;
+ }
+ }
+ else
+ {
+ char c_pcs = str_pcs(path);
+ if(c_pcs != 0)
+ {
+ fprintf(stderr,
+ "pathchk: Error non-portable character '%c' (0x%02x) found in: %s\n",
+ c_pcs,
+ c_pcs,
+ path);
+ err = 1;
+ }
+ }
+ }
+
+ return err;
+}
diff --git a/coreutils.txt b/coreutils.txt
@@ -55,7 +55,7 @@ nproc: ?
numfmt: Maybe, consider humanize(1)
od: Todo
paste: Todo
-pathchk: ?
+pathchk: Done
pinky: No
pr: No. Considered obsolete
printenv: No, the output sucks
diff --git a/lsb_commands.txt b/lsb_commands.txt
@@ -93,7 +93,7 @@ od: Todo
passwd: out of scope
paste: Todo
patch: out of scope
-pathchk: ?
+pathchk: Done
pax: out of scope
pidof: ?
pr: No. Considered obsolete
diff --git a/posix_utilities.txt b/posix_utilities.txt
@@ -92,7 +92,7 @@ nohup: done
od
paste
patch: no
-pathchk
+pathchk: done
pax: no
pr: no
printf
diff --git a/test-cmd/pathchk.t b/test-cmd/pathchk.t
@@ -0,0 +1,39 @@
+#!/usr/bin/env cram
+# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+# SPDX-License-Identifier: MPL-2.0
+
+ $ export PATH="$TESTDIR/../cmd:$PATH"
+
+ $ test "$(command -v pathchk)" = "$TESTDIR/../cmd/pathchk"
+
+ $ pathchk "$(printf "foo/%*s/bar" "$(getconf NAME_MAX "$PWD")" | tr ' ' _)"
+ $ pathchk "$(printf "foo/s%*s/bar" "$(getconf NAME_MAX "$PWD")" | tr ' ' _)"
+ pathchk: Path component \([0-9]+ octets\) is over the maximum size \([0-9]+ octets\): s_+ (re)
+ [1]
+
+ $ pathchk -p "$(printf "foo/%*s/bar" "$(getconf _POSIX_NAME_MAX)" | tr ' ' _)"
+ $ pathchk -p "$(printf "foo/s%*s/bar" "$(getconf _POSIX_NAME_MAX)" | tr ' ' _)"
+ pathchk: Path component \([0-9]+ octets\) is over the maximum size \([0-9]+ octets\): s_+ (re)
+ [1]
+
+ $ pathchk "$(printf "s%*s" "$(getconf PATH_MAX "$PWD")" | tr ' ' /)"
+ pathchk: Path \([0-9]+ octets\) is over the maximum size \([0-9]+ octets\): s/+ (re)
+ [1]
+
+ $ pathchk -p "$(printf "%*s" "$(getconf _POSIX_PATH_MAX)" | tr ' ' /)"
+ $ pathchk -p "$(printf "s%*s" "$(getconf _POSIX_PATH_MAX)" | tr ' ' /)"
+ pathchk: Path \([0-9]+ octets\) is over the maximum size \([0-9]+ octets\): s/+ (re)
+ [1]
+
+ $ pathchk /dev/null/foo
+ $ pathchk /../foo
+ $ pathchk -P ./-foo
+ pathchk: Path component starts with an hyphen: -foo
+ [1]
+ $ pathchk -P ./bar/-foo
+ pathchk: Path component starts with an hyphen: -foo
+ [1]
+
+ $ pathchk -p "foo$(printf '\01')bar"
+ pathchk: Error non-portable character '\x01' (0x01) found in: foo\x01bar (esc)
+ [1]