summaryrefslogtreecommitdiff
path: root/corebinutils/mkdir
diff options
context:
space:
mode:
Diffstat (limited to 'corebinutils/mkdir')
-rw-r--r--corebinutils/mkdir/.gitignore25
-rw-r--r--corebinutils/mkdir/GNUmakefile39
-rw-r--r--corebinutils/mkdir/LICENSE32
-rw-r--r--corebinutils/mkdir/LICENSES/BSD-3-Clause.txt11
-rw-r--r--corebinutils/mkdir/README.md33
-rw-r--r--corebinutils/mkdir/mkdir.1120
-rw-r--r--corebinutils/mkdir/mkdir.c362
-rw-r--r--corebinutils/mkdir/mode.c357
-rw-r--r--corebinutils/mkdir/mode.h7
-rw-r--r--corebinutils/mkdir/tests/test.sh206
10 files changed, 1192 insertions, 0 deletions
diff --git a/corebinutils/mkdir/.gitignore b/corebinutils/mkdir/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/mkdir/.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/mkdir/GNUmakefile b/corebinutils/mkdir/GNUmakefile
new file mode 100644
index 0000000000..b4d90bbb2e
--- /dev/null
+++ b/corebinutils/mkdir/GNUmakefile
@@ -0,0 +1,39 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS ?=
+CPPFLAGS += -D_POSIX_C_SOURCE=200809L
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/mkdir
+OBJS := $(OBJDIR)/mkdir.o $(OBJDIR)/mode.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/mkdir.o: $(CURDIR)/mkdir.c $(CURDIR)/mode.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/mkdir.c" -o "$@"
+
+$(OBJDIR)/mode.o: $(CURDIR)/mode.c $(CURDIR)/mode.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/mode.c" -o "$@"
+
+test: $(TARGET)
+ MKDIR_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/mkdir/LICENSE b/corebinutils/mkdir/LICENSE
new file mode 100644
index 0000000000..06b62f1772
--- /dev/null
+++ b/corebinutils/mkdir/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 1983, 1992, 1993
+ The Regents of the University of California. All rights reserved.
+
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+This code is derived from software contributed to Berkeley by
+Michael Fischbein.
+
+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/mkdir/LICENSES/BSD-3-Clause.txt b/corebinutils/mkdir/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000000..ea890afbc7
--- /dev/null
+++ b/corebinutils/mkdir/LICENSES/BSD-3-Clause.txt
@@ -0,0 +1,11 @@
+Copyright (c) <year> <owner>.
+
+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 copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/corebinutils/mkdir/README.md b/corebinutils/mkdir/README.md
new file mode 100644
index 0000000000..4c703616fe
--- /dev/null
+++ b/corebinutils/mkdir/README.md
@@ -0,0 +1,33 @@
+# mkdir
+
+Standalone Linux-native port of FreeBSD `mkdir` 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 FreeBSD utility was rewritten into a standalone Linux-native build instead of preserving BSD build glue, `err(3)`, or `setmode(3)` dependencies.
+- Final directory creation maps to Linux `mkdir(2)`. When `-m` is present, the port follows FreeBSD semantics by applying the requested mode with `chmod(2)` after successful creation so the result is not truncated by `umask`.
+- For symbolic `-m` values, `+` and `-` are interpreted relative to an initial `a=rwx` mode, matching `mkdir.1` rather than inheriting `chmod(1)`'s default-`umask` behavior.
+- `-p` walks the path a component at a time with repeated `mkdir(2)` and `stat(2)` calls. Intermediate directories use a temporary `umask(2)` adjustment so Linux still grants owner write/search bits as required by POSIX and FreeBSD behavior.
+- Mode parsing is local project code; no shared BSD compatibility shim is required.
+- Path handling is dynamic and does not depend on `PATH_MAX`, `dirname(3)`, or in-place mutation of caller-owned argument strings.
+
+## Supported / Unsupported Semantics
+
+- Supported: FreeBSD/POSIX option surface `-m`, `-p`, and `-v`, including octal and symbolic modes.
+- Supported: existing-directory success for `-p`, verbose output for newly created path components only, and correct failure when an existing path component is not a directory.
+- Unsupported by design: GNU long options and GNU-specific parsing extensions. This port keeps BSD/POSIX command-line parsing strict.
+- Linux-native limitation: kernel/filesystem specific permission inheritance such as SGID propagation follows the underlying Linux filesystem semantics.
diff --git a/corebinutils/mkdir/mkdir.1 b/corebinutils/mkdir/mkdir.1
new file mode 100644
index 0000000000..599ac7e4a7
--- /dev/null
+++ b/corebinutils/mkdir/mkdir.1
@@ -0,0 +1,120 @@
+.\"-
+.\" Copyright (c) 1989, 1990, 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 March 15, 2013
+.Dt MKDIR 1
+.Os
+.Sh NAME
+.Nm mkdir
+.Nd make directories
+.Sh SYNOPSIS
+.Nm
+.Op Fl pv
+.Op Fl m Ar mode
+.Ar directory_name ...
+.Sh DESCRIPTION
+The
+.Nm
+utility creates the directories named as operands, in the order specified,
+using mode
+.Dq Li rwxrwxrwx
+(0777)
+as modified by the current
+.Xr umask 2 .
+.Pp
+The options are as follows:
+.Bl -tag -width ".Fl m Ar mode"
+.It Fl m Ar mode
+Set the file permission bits of the final created directory to
+the specified mode.
+The
+.Ar mode
+argument can be in any of the formats specified to the
+.Xr chmod 1
+command.
+If a symbolic mode is specified, the operation characters
+.Ql +
+and
+.Ql -
+are interpreted relative to an initial mode of
+.Dq Li a=rwx .
+.It Fl p
+Create intermediate directories as required.
+If this option is not specified, the full path prefix of each
+operand must already exist.
+On the other hand, with this option specified, no error will
+be reported if a directory given as an operand already exists.
+Intermediate directories are created with permission bits of
+.Dq Li rwxrwxrwx
+(0777)
+as modified by the current umask, plus write and search
+permission for the owner.
+.It Fl v
+Be verbose when creating directories, listing them as they are created.
+.El
+.Pp
+The user must have write permission in the parent directory.
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+Create a directory named
+.Pa foobar :
+.Pp
+.Dl $ mkdir foobar
+.Pp
+Create a directory named
+.Pa foobar
+and set its file mode to 700:
+.Pp
+.Dl $ mkdir -m 700 foobar
+.Pp
+Create a directory named
+.Pa cow/horse/monkey ,
+creating any non-existent intermediate directories as necessary:
+.Pp
+.Dl $ mkdir -p cow/horse/monkey
+.Sh COMPATIBILITY
+The
+.Fl v
+option is non-standard and its use in scripts is not recommended.
+.Sh SEE ALSO
+.Xr rmdir 1
+.Sh STANDARDS
+The
+.Nm
+utility is expected to be
+.St -p1003.2
+compatible.
+.Sh HISTORY
+A
+.Nm
+command appeared in
+.At v1 .
diff --git a/corebinutils/mkdir/mkdir.c b/corebinutils/mkdir/mkdir.c
new file mode 100644
index 0000000000..daea0f0097
--- /dev/null
+++ b/corebinutils/mkdir/mkdir.c
@@ -0,0 +1,362 @@
+#define _POSIX_C_SOURCE 200809L
+
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1983, 1992, 1993
+ * The Regents of the University of California. All rights reserved.
+ * 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.
+ */
+
+#include <errno.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "mode.h"
+
+struct mkdir_options {
+ bool parents;
+ bool verbose;
+ bool explicit_mode;
+ mode_t final_mode;
+};
+
+static const char *progname;
+
+static int create_component(const char *path,
+ const struct mkdir_options *options, bool last_component,
+ bool allow_existing_dir, mode_t intermediate_umask);
+static int create_parents_path(const char *path,
+ const struct mkdir_options *options);
+static int create_single_path(const char *path,
+ const struct mkdir_options *options);
+static mode_t current_umask(void);
+static void error_errno(const char *fmt, ...);
+static void error_msg(const char *fmt, ...);
+static int existing_directory(const char *path);
+static int mkdir_with_umask(const char *path, mode_t mode, mode_t mask);
+static int print_verbose_path(const char *path);
+static const char *program_name(const char *argv0);
+static char *xstrdup(const char *text);
+static void usage(void) __attribute__((noreturn));
+
+static const char *
+program_name(const char *argv0)
+{
+ const char *name;
+
+ if (argv0 == NULL || argv0[0] == '\0')
+ return ("mkdir");
+ name = strrchr(argv0, '/');
+ return (name == NULL ? argv0 : name + 1);
+}
+
+static void
+verror_message(bool with_errno, const char *fmt, va_list ap)
+{
+ int saved_errno;
+
+ saved_errno = errno;
+ (void)fprintf(stderr, "%s: ", progname);
+ (void)vfprintf(stderr, fmt, ap);
+ if (with_errno)
+ (void)fprintf(stderr, ": %s", strerror(saved_errno));
+ (void)fputc('\n', stderr);
+}
+
+static void
+error_errno(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ verror_message(true, fmt, ap);
+ va_end(ap);
+}
+
+static void
+error_msg(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ verror_message(false, fmt, ap);
+ va_end(ap);
+}
+
+static char *
+xstrdup(const char *text)
+{
+ size_t len;
+ char *copy;
+
+ len = strlen(text) + 1;
+ copy = malloc(len);
+ if (copy == NULL) {
+ error_msg("out of memory");
+ exit(1);
+ }
+ memcpy(copy, text, len);
+ return (copy);
+}
+
+static mode_t
+current_umask(void)
+{
+ sigset_t all_signals, original_signals;
+ mode_t mask;
+
+ sigfillset(&all_signals);
+ (void)sigprocmask(SIG_BLOCK, &all_signals, &original_signals);
+ mask = umask(0);
+ (void)umask(mask);
+ (void)sigprocmask(SIG_SETMASK, &original_signals, NULL);
+ return (mask);
+}
+
+static int
+mkdir_with_umask(const char *path, mode_t mode, mode_t mask)
+{
+ mode_t original_mask;
+ int saved_errno;
+ int rc;
+
+ original_mask = umask(mask);
+ rc = mkdir(path, mode);
+ saved_errno = errno;
+ (void)umask(original_mask);
+ errno = saved_errno;
+ return (rc);
+}
+
+static int
+existing_directory(const char *path)
+{
+ struct stat st;
+
+ if (stat(path, &st) == -1)
+ return (-1);
+ return (S_ISDIR(st.st_mode) ? 1 : 0);
+}
+
+static int
+print_verbose_path(const char *path)
+{
+ if (printf("%s\n", path) < 0) {
+ error_errno("stdout");
+ return (1);
+ }
+ return (0);
+}
+
+static int
+create_component(const char *path, const struct mkdir_options *options,
+ bool last_component, bool allow_existing_dir, mode_t intermediate_umask)
+{
+ int rc;
+
+ if (last_component) {
+ mode_t mode;
+
+ mode = options->explicit_mode ? options->final_mode :
+ (S_IRWXU | S_IRWXG | S_IRWXO);
+ rc = mkdir(path, mode);
+ } else {
+ rc = mkdir_with_umask(path, S_IRWXU | S_IRWXG | S_IRWXO,
+ intermediate_umask);
+ }
+
+ if (rc == -1) {
+ int dir_status;
+
+ if (errno == EEXIST || errno == EISDIR) {
+ dir_status = existing_directory(path);
+ if (dir_status == -1) {
+ error_errno("%s", path);
+ return (1);
+ }
+ if (allow_existing_dir && dir_status == 1)
+ return (0);
+ errno = last_component ? EEXIST : ENOTDIR;
+ }
+ error_errno("%s", path);
+ return (1);
+ }
+
+ if (last_component && options->explicit_mode &&
+ chmod(path, options->final_mode) == -1) {
+ error_errno("%s", path);
+ return (1);
+ }
+
+ if (options->verbose)
+ return (print_verbose_path(path));
+ return (0);
+}
+
+static int
+create_single_path(const char *path, const struct mkdir_options *options)
+{
+ return (create_component(path, options, true, false, 0));
+}
+
+static int
+create_parents_path(const char *path, const struct mkdir_options *options)
+{
+ char *cursor, *path_copy, *segment_end;
+ mode_t intermediate_umask;
+ int dir_status;
+ int status;
+
+ if (path[0] == '\0')
+ return (create_single_path(path, options));
+
+ path_copy = xstrdup(path);
+ while (path_copy[0] != '\0' && strcmp(path_copy, "/") != 0) {
+ size_t len;
+
+ len = strlen(path_copy);
+ if (len <= 1 || path_copy[len - 1] != '/')
+ break;
+ path_copy[len - 1] = '\0';
+ }
+
+ dir_status = existing_directory(path_copy);
+ if (dir_status == 1) {
+ free(path_copy);
+ return (0);
+ }
+ if (dir_status == -1 && errno != ENOENT) {
+ error_errno("%s", path_copy);
+ free(path_copy);
+ return (1);
+ }
+
+ intermediate_umask = current_umask() & ~(S_IWUSR | S_IXUSR);
+ status = 0;
+ cursor = path_copy;
+ for (;;) {
+ while (*cursor == '/')
+ cursor++;
+ if (*cursor == '\0')
+ break;
+
+ segment_end = cursor;
+ while (*segment_end != '\0' && *segment_end != '/')
+ segment_end++;
+
+ if (*segment_end == '\0') {
+ status = create_component(path_copy, options, true, true,
+ intermediate_umask);
+ break;
+ }
+
+ *segment_end = '\0';
+ status = create_component(path_copy, options, false, true,
+ intermediate_umask);
+ *segment_end = '/';
+ if (status != 0)
+ break;
+ cursor = segment_end + 1;
+ }
+
+ free(path_copy);
+ return (status);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct mkdir_options options;
+ void *compiled_mode;
+ int ch, exit_status;
+
+ progname = program_name(argv[0]);
+ memset(&options, 0, sizeof(options));
+ options.final_mode = S_IRWXU | S_IRWXG | S_IRWXO;
+ compiled_mode = NULL;
+
+ while ((ch = getopt(argc, argv, "m:pv")) != -1) {
+ switch (ch) {
+ case 'm':
+ mode_free(compiled_mode);
+ compiled_mode = mode_compile(optarg);
+ if (compiled_mode == NULL)
+ error_msg("invalid file mode: %s", optarg);
+ if (compiled_mode == NULL)
+ return (1);
+ options.explicit_mode = true;
+ options.final_mode = mode_apply(compiled_mode,
+ S_IRWXU | S_IRWXG | S_IRWXO);
+ break;
+ case 'p':
+ options.parents = true;
+ break;
+ case 'v':
+ options.verbose = true;
+ break;
+ case '?':
+ default:
+ usage();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+ if (argc == 0)
+ usage();
+
+ exit_status = 0;
+ for (int i = 0; i < argc; i++) {
+ int status;
+
+ if (options.parents)
+ status = create_parents_path(argv[i], &options);
+ else
+ status = create_single_path(argv[i], &options);
+ if (status != 0)
+ exit_status = 1;
+ }
+
+ mode_free(compiled_mode);
+ return (exit_status);
+}
+
+static void
+usage(void)
+{
+ (void)fprintf(stderr,
+ "usage: mkdir [-pv] [-m mode] directory_name ...\n");
+ exit(2);
+}
diff --git a/corebinutils/mkdir/mode.c b/corebinutils/mkdir/mode.c
new file mode 100644
index 0000000000..1f3d2af9b8
--- /dev/null
+++ b/corebinutils/mkdir/mode.c
@@ -0,0 +1,357 @@
+#include "mode.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+typedef struct bitcmd {
+ char cmd;
+ char cmd2;
+ mode_t bits;
+} bitcmd_t;
+
+#define SET_LEN 6
+#define SET_LEN_INCR 4
+
+#define CMD2_CLR 0x01
+#define CMD2_SET 0x02
+#define CMD2_GBITS 0x04
+#define CMD2_OBITS 0x08
+#define CMD2_UBITS 0x10
+
+#define STANDARD_BITS (S_ISUID | S_ISGID | S_IRWXU | S_IRWXG | S_IRWXO)
+
+#ifndef S_ISVTX
+#define S_ISVTX 01000
+#endif
+
+static bitcmd_t *addcmd(bitcmd_t *set, mode_t op, mode_t who, mode_t oparg,
+ mode_t mask);
+static void compress_mode(bitcmd_t *set);
+
+mode_t
+mode_apply(const void *compiled, mode_t old_mode)
+{
+ const bitcmd_t *set;
+ mode_t clear_value, new_mode, value;
+
+ set = compiled;
+ new_mode = old_mode;
+ for (value = 0;; set++) {
+ switch (set->cmd) {
+ case 'u':
+ value = (new_mode & S_IRWXU) >> 6;
+ goto common;
+ case 'g':
+ value = (new_mode & S_IRWXG) >> 3;
+ goto common;
+ case 'o':
+ value = new_mode & S_IRWXO;
+common:
+ if (set->cmd2 & CMD2_CLR) {
+ clear_value = (set->cmd2 & CMD2_SET) ? S_IRWXO :
+ value;
+ if (set->cmd2 & CMD2_UBITS)
+ new_mode &= ~((clear_value << 6) &
+ set->bits);
+ if (set->cmd2 & CMD2_GBITS)
+ new_mode &= ~((clear_value << 3) &
+ set->bits);
+ if (set->cmd2 & CMD2_OBITS)
+ new_mode &= ~(clear_value & set->bits);
+ }
+ if (set->cmd2 & CMD2_SET) {
+ if (set->cmd2 & CMD2_UBITS)
+ new_mode |= (value << 6) & set->bits;
+ if (set->cmd2 & CMD2_GBITS)
+ new_mode |= (value << 3) & set->bits;
+ if (set->cmd2 & CMD2_OBITS)
+ new_mode |= value & set->bits;
+ }
+ break;
+ case '+':
+ new_mode |= set->bits;
+ break;
+ case '-':
+ new_mode &= ~set->bits;
+ break;
+ case 'X':
+ if (old_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
+ new_mode |= set->bits;
+ break;
+ case '\0':
+ default:
+ return (new_mode);
+ }
+ }
+}
+
+void *
+mode_compile(const char *mode_string)
+{
+ char op, *endp;
+ bitcmd_t *endset, *saveset, *set;
+ mode_t mask, perm, perm_xbits, who;
+ long perm_value;
+ unsigned int setlen;
+ int saved_errno;
+ int equal_done;
+
+ if (*mode_string == '\0') {
+ errno = EINVAL;
+ return (NULL);
+ }
+
+ mask = S_IRWXU | S_IRWXG | S_IRWXO;
+ setlen = SET_LEN + 2;
+ set = malloc(setlen * sizeof(*set));
+ if (set == NULL)
+ return (NULL);
+ saveset = set;
+ endset = set + (setlen - 2);
+
+ if (isdigit((unsigned char)*mode_string)) {
+ errno = 0;
+ perm_value = strtol(mode_string, &endp, 8);
+ if (*endp != '\0') {
+ errno = EINVAL;
+ goto fail;
+ }
+ if (errno == ERANGE &&
+ (perm_value == LONG_MAX || perm_value == LONG_MIN)) {
+ goto fail;
+ }
+ if (perm_value & ~(STANDARD_BITS | S_ISVTX)) {
+ errno = EINVAL;
+ goto fail;
+ }
+ perm = (mode_t)perm_value;
+ set = addcmd(set, '=', STANDARD_BITS | S_ISVTX, perm, mask);
+ set->cmd = '\0';
+ return (saveset);
+ }
+
+ equal_done = 0;
+ for (;;) {
+ for (who = 0;; mode_string++) {
+ switch (*mode_string) {
+ case 'a':
+ who |= STANDARD_BITS;
+ break;
+ case 'u':
+ who |= S_ISUID | S_IRWXU;
+ break;
+ case 'g':
+ who |= S_ISGID | S_IRWXG;
+ break;
+ case 'o':
+ who |= S_IRWXO;
+ break;
+ default:
+ goto getop;
+ }
+ }
+getop:
+ op = *mode_string++;
+ if (op != '+' && op != '-' && op != '=') {
+ errno = EINVAL;
+ goto fail;
+ }
+ if (op == '=')
+ equal_done = 0;
+
+ who &= ~S_ISVTX;
+ for (perm = 0, perm_xbits = 0;; mode_string++) {
+ switch (*mode_string) {
+ case 'r':
+ perm |= S_IRUSR | S_IRGRP | S_IROTH;
+ break;
+ case 'w':
+ perm |= S_IWUSR | S_IWGRP | S_IWOTH;
+ break;
+ case 'x':
+ perm |= S_IXUSR | S_IXGRP | S_IXOTH;
+ break;
+ case 'X':
+ perm_xbits = S_IXUSR | S_IXGRP | S_IXOTH;
+ break;
+ case 's':
+ if (who == 0 || (who & ~S_IRWXO) != 0)
+ perm |= S_ISUID | S_ISGID;
+ break;
+ case 't':
+ if (who == 0 || (who & ~S_IRWXO) != 0) {
+ who |= S_ISVTX;
+ perm |= S_ISVTX;
+ }
+ break;
+ case 'u':
+ case 'g':
+ case 'o':
+ if (perm != 0) {
+ set = addcmd(set, op, who, perm, mask);
+ perm = 0;
+ }
+ if (op == '=')
+ equal_done = 1;
+ if (op == '+' && perm_xbits != 0) {
+ set = addcmd(set, 'X', who, perm_xbits,
+ mask);
+ perm_xbits = 0;
+ }
+ set = addcmd(set, *mode_string, who, op, mask);
+ break;
+ default:
+ if (perm != 0 || (op == '=' && !equal_done)) {
+ if (op == '=')
+ equal_done = 1;
+ set = addcmd(set, op, who, perm, mask);
+ perm = 0;
+ }
+ if (perm_xbits != 0) {
+ set = addcmd(set, 'X', who, perm_xbits,
+ mask);
+ perm_xbits = 0;
+ }
+ goto apply;
+ }
+
+ if (set >= endset) {
+ ptrdiff_t offset;
+ bitcmd_t *newset;
+
+ offset = set - saveset;
+ setlen += SET_LEN_INCR;
+ newset = realloc(saveset,
+ setlen * sizeof(*newset));
+ if (newset == NULL)
+ goto fail;
+ saveset = newset;
+ set = newset + offset;
+ endset = newset + (setlen - 2);
+ }
+ }
+apply:
+ if (*mode_string == '\0')
+ break;
+ if (*mode_string != ',') {
+ errno = EINVAL;
+ goto fail;
+ }
+ mode_string++;
+ }
+
+ set->cmd = '\0';
+ compress_mode(saveset);
+ return (saveset);
+
+fail:
+ saved_errno = errno;
+ free(saveset);
+ errno = saved_errno;
+ return (NULL);
+}
+
+void
+mode_free(void *compiled)
+{
+ free(compiled);
+}
+
+static bitcmd_t *
+addcmd(bitcmd_t *set, mode_t op, mode_t who, mode_t oparg, mode_t mask)
+{
+ switch (op) {
+ case '=':
+ set->cmd = '-';
+ set->bits = who ? who : STANDARD_BITS;
+ set++;
+ op = '+';
+ /* FALLTHROUGH */
+ case '+':
+ case '-':
+ case 'X':
+ set->cmd = op;
+ set->cmd2 = 0;
+ set->bits = (who ? who : mask) & oparg;
+ break;
+ case 'u':
+ case 'g':
+ case 'o':
+ set->cmd = op;
+ if (who != 0) {
+ set->cmd2 = ((who & S_IRUSR) ? CMD2_UBITS : 0) |
+ ((who & S_IRGRP) ? CMD2_GBITS : 0) |
+ ((who & S_IROTH) ? CMD2_OBITS : 0);
+ set->bits = (mode_t)~0;
+ } else {
+ set->cmd2 = CMD2_UBITS | CMD2_GBITS | CMD2_OBITS;
+ set->bits = mask;
+ }
+
+ if (oparg == '+')
+ set->cmd2 |= CMD2_SET;
+ else if (oparg == '-')
+ set->cmd2 |= CMD2_CLR;
+ else if (oparg == '=')
+ set->cmd2 |= CMD2_SET | CMD2_CLR;
+ break;
+ }
+
+ return (set + 1);
+}
+
+static void
+compress_mode(bitcmd_t *set)
+{
+ bitcmd_t *next_set;
+ int clear_bits, op, set_bits, x_bits;
+
+ for (next_set = set;;) {
+ while ((op = next_set->cmd) != '+' && op != '-' && op != 'X') {
+ *set++ = *next_set++;
+ if (op == '\0')
+ return;
+ }
+
+ for (set_bits = clear_bits = x_bits = 0;; next_set++) {
+ op = next_set->cmd;
+ if (op == '-') {
+ clear_bits |= next_set->bits;
+ set_bits &= ~next_set->bits;
+ x_bits &= ~next_set->bits;
+ } else if (op == '+') {
+ set_bits |= next_set->bits;
+ clear_bits &= ~next_set->bits;
+ x_bits &= ~next_set->bits;
+ } else if (op == 'X') {
+ x_bits |= next_set->bits & ~set_bits;
+ } else {
+ break;
+ }
+ }
+
+ if (clear_bits != 0) {
+ set->cmd = '-';
+ set->cmd2 = 0;
+ set->bits = clear_bits;
+ set++;
+ }
+ if (set_bits != 0) {
+ set->cmd = '+';
+ set->cmd2 = 0;
+ set->bits = set_bits;
+ set++;
+ }
+ if (x_bits != 0) {
+ set->cmd = 'X';
+ set->cmd2 = 0;
+ set->bits = x_bits;
+ set++;
+ }
+ }
+}
diff --git a/corebinutils/mkdir/mode.h b/corebinutils/mkdir/mode.h
new file mode 100644
index 0000000000..ff1f3d791b
--- /dev/null
+++ b/corebinutils/mkdir/mode.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include <sys/types.h>
+
+void *mode_compile(const char *mode_string);
+mode_t mode_apply(const void *compiled, mode_t old_mode);
+void mode_free(void *compiled);
diff --git a/corebinutils/mkdir/tests/test.sh b/corebinutils/mkdir/tests/test.sh
new file mode 100644
index 0000000000..dd42ba2362
--- /dev/null
+++ b/corebinutils/mkdir/tests/test.sh
@@ -0,0 +1,206 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
+MKDIR_BIN=${MKDIR_BIN:-"$ROOT/out/mkdir"}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/mkdir-test.XXXXXX")
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+LAST_STATUS=0
+LAST_STDOUT=
+LAST_STDERR=
+trap 'chmod -R u+rwx "$WORKDIR" 2>/dev/null || true; rm -rf "$WORKDIR"' EXIT INT TERM
+
+export LC_ALL=C
+
+USAGE_TEXT='usage: mkdir [-pv] [-m mode] directory_name ...'
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected ---" >&2
+ printf '%s' "$expected" >&2
+ printf '\n%s\n' "--- actual ---" >&2
+ printf '%s' "$actual" >&2
+ printf '\n' >&2
+ exit 1
+ fi
+}
+
+assert_contains() {
+ name=$1
+ text=$2
+ pattern=$3
+ case $text in
+ *"$pattern"*) ;;
+ *) fail "$name" ;;
+ esac
+}
+
+assert_empty() {
+ name=$1
+ text=$2
+ if [ -n "$text" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected empty ---" >&2
+ printf '%s\n' "--- actual ---" >&2
+ printf '%s' "$text" >&2
+ printf '\n' >&2
+ exit 1
+ fi
+}
+
+assert_status() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" -ne "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "expected status: $expected" >&2
+ printf '%s\n' "actual status: $actual" >&2
+ exit 1
+ fi
+}
+
+assert_mode() {
+ expected=$1
+ path=$2
+ actual=$(stat -c '%a' "$path")
+ [ "$actual" = "$expected" ] || fail "$path mode expected $expected got $actual"
+}
+
+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")
+}
+
+[ -x "$MKDIR_BIN" ] || fail "missing binary: $MKDIR_BIN"
+
+run_capture "$MKDIR_BIN"
+assert_status "usage status" 2 "$LAST_STATUS"
+assert_empty "usage stdout" "$LAST_STDOUT"
+assert_eq "usage stderr" "$USAGE_TEXT" "$LAST_STDERR"
+
+run_capture "$MKDIR_BIN" -m
+assert_status "missing mode argument status" 2 "$LAST_STATUS"
+assert_empty "missing mode argument stdout" "$LAST_STDOUT"
+assert_contains "missing mode argument stderr has usage" "$LAST_STDERR" "$USAGE_TEXT"
+
+run_capture "$MKDIR_BIN" -z
+assert_status "invalid option status" 2 "$LAST_STATUS"
+assert_empty "invalid option stdout" "$LAST_STDOUT"
+assert_contains "invalid option stderr has usage" "$LAST_STDERR" "$USAGE_TEXT"
+
+run_capture "$MKDIR_BIN" -m invalid "$WORKDIR/invalid"
+assert_status "invalid mode status" 1 "$LAST_STATUS"
+assert_empty "invalid mode stdout" "$LAST_STDOUT"
+assert_contains "invalid mode stderr" "$LAST_STDERR" "invalid file mode: invalid"
+
+run_capture "$MKDIR_BIN" "$WORKDIR/basic"
+assert_status "basic status" 0 "$LAST_STATUS"
+assert_empty "basic stdout" "$LAST_STDOUT"
+assert_empty "basic stderr" "$LAST_STDERR"
+[ -d "$WORKDIR/basic" ] || fail "basic directory missing"
+
+run_capture "$MKDIR_BIN" "$WORKDIR/basic"
+assert_status "existing dir status" 1 "$LAST_STATUS"
+assert_empty "existing dir stdout" "$LAST_STDOUT"
+assert_contains "existing dir stderr" "$LAST_STDERR" "$WORKDIR/basic"
+
+mode_dir="$WORKDIR/mode-explicit"
+run_capture sh -c 'umask 077; "$1" -m 755 "$2"' sh "$MKDIR_BIN" "$mode_dir"
+assert_status "explicit numeric mode status" 0 "$LAST_STATUS"
+assert_empty "explicit numeric stdout" "$LAST_STDOUT"
+assert_empty "explicit numeric stderr" "$LAST_STDERR"
+assert_mode 755 "$mode_dir"
+
+symbolic_dir="$WORKDIR/mode-symbolic"
+run_capture sh -c 'umask 077; "$1" -m u=rwx,go=rx "$2"' sh "$MKDIR_BIN" "$symbolic_dir"
+assert_status "explicit symbolic mode status" 0 "$LAST_STATUS"
+assert_empty "explicit symbolic stdout" "$LAST_STDOUT"
+assert_empty "explicit symbolic stderr" "$LAST_STDERR"
+assert_mode 755 "$symbolic_dir"
+
+relative_symbolic_dir="$WORKDIR/mode-relative-symbolic"
+run_capture sh -c 'umask 077; "$1" -m -w "$2"' sh "$MKDIR_BIN" "$relative_symbolic_dir"
+assert_status "relative symbolic mode status" 0 "$LAST_STATUS"
+assert_empty "relative symbolic stdout" "$LAST_STDOUT"
+assert_empty "relative symbolic stderr" "$LAST_STDERR"
+assert_mode 555 "$relative_symbolic_dir"
+
+special_mode_dir="$WORKDIR/mode-special"
+run_capture "$MKDIR_BIN" -m 1755 "$special_mode_dir"
+assert_status "special mode status" 0 "$LAST_STATUS"
+assert_empty "special mode stdout" "$LAST_STDOUT"
+assert_empty "special mode stderr" "$LAST_STDERR"
+[ "$(stat -c '%a' "$special_mode_dir")" = "1755" ] || fail "special mode bits missing"
+
+verbose_dir="$WORKDIR/verbose/a/b"
+run_capture "$MKDIR_BIN" -pv "$verbose_dir"
+assert_status "verbose recursive status" 0 "$LAST_STATUS"
+assert_eq "verbose recursive stdout" "$WORKDIR/verbose
+$WORKDIR/verbose/a
+$WORKDIR/verbose/a/b" "$LAST_STDOUT"
+assert_empty "verbose recursive stderr" "$LAST_STDERR"
+
+restricted_root="$WORKDIR/restrict"
+run_capture sh -c 'umask 0777; "$1" -p "$2/one/two"' sh "$MKDIR_BIN" "$restricted_root"
+assert_status "restrictive umask status" 0 "$LAST_STATUS"
+assert_empty "restrictive umask stdout" "$LAST_STDOUT"
+assert_empty "restrictive umask stderr" "$LAST_STDERR"
+assert_mode 300 "$restricted_root/one"
+assert_mode 0 "$restricted_root/one/two"
+
+mkdir "$WORKDIR/existing"
+run_capture "$MKDIR_BIN" -p "$WORKDIR/existing"
+assert_status "existing with -p status" 0 "$LAST_STATUS"
+assert_empty "existing with -p stdout" "$LAST_STDOUT"
+assert_empty "existing with -p stderr" "$LAST_STDERR"
+
+mkdir -p "$WORKDIR/verbose-existing/tree"
+run_capture "$MKDIR_BIN" -pv "$WORKDIR/verbose-existing/tree/new/leaf"
+assert_status "verbose skips existing status" 0 "$LAST_STATUS"
+assert_eq "verbose skips existing stdout" "$WORKDIR/verbose-existing/tree/new
+$WORKDIR/verbose-existing/tree/new/leaf" "$LAST_STDOUT"
+assert_empty "verbose skips existing stderr" "$LAST_STDERR"
+
+run_capture "$MKDIR_BIN" -p "$WORKDIR/slashes//deep///leaf///"
+assert_status "redundant slashes status" 0 "$LAST_STATUS"
+assert_empty "redundant slashes stdout" "$LAST_STDOUT"
+assert_empty "redundant slashes stderr" "$LAST_STDERR"
+[ -d "$WORKDIR/slashes/deep/leaf" ] || fail "redundant slash path missing"
+
+printf 'not-a-dir\n' >"$WORKDIR/file"
+run_capture "$MKDIR_BIN" -p "$WORKDIR/file/child"
+assert_status "non-directory component status" 1 "$LAST_STATUS"
+assert_empty "non-directory component stdout" "$LAST_STDOUT"
+assert_contains "non-directory component stderr" "$LAST_STDERR" "$WORKDIR/file"
+
+run_capture "$MKDIR_BIN" "$WORKDIR/missing/child"
+assert_status "missing parent status" 1 "$LAST_STATUS"
+assert_empty "missing parent stdout" "$LAST_STDOUT"
+assert_contains "missing parent stderr" "$LAST_STDERR" "$WORKDIR/missing/child"
+
+multi_one="$WORKDIR/multi-one"
+multi_two_parent="$WORKDIR/multi-missing"
+run_capture "$MKDIR_BIN" "$multi_one" "$multi_two_parent/child"
+assert_status "multi operand partial failure status" 1 "$LAST_STATUS"
+assert_empty "multi operand partial failure stdout" "$LAST_STDOUT"
+assert_contains "multi operand partial failure stderr" "$LAST_STDERR" "$multi_two_parent/child"
+[ -d "$multi_one" ] || fail "multi operand did not create successful directory"
+
+printf '%s\n' "PASS"