summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-02-28 23:08:05 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-02-28 23:09:26 +0300
commitb19672b599d80bb29d054b7bd39abb3f146037e3 (patch)
tree9f5a9bc7683d181466be2f925da34963c811f5de
downloadProject-Tick-b19672b599d80bb29d054b7bd39abb3f146037e3.tar.gz
Project-Tick-b19672b599d80bb29d054b7bd39abb3f146037e3.zip
init Standalone musl-libc-based Linux port of FreeBSD `hostname` for Project Tick BSD/Linux Distribution.
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
-rw-r--r--.gitignore25
-rw-r--r--GNUmakefile35
-rw-r--r--LICENSE32
-rw-r--r--LICENSES/BSD-3-Clause.txt11
-rw-r--r--README.md27
-rw-r--r--hostname.1105
-rw-r--r--hostname.c240
-rw-r--r--tests/test.sh269
8 files changed, 744 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/.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/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000000..5b9c3e9200
--- /dev/null
+++ b/GNUmakefile
@@ -0,0 +1,35 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS += -D_GNU_SOURCE
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/hostname
+OBJS := $(OBJDIR)/hostname.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/hostname.o: $(CURDIR)/hostname.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/hostname.c" -o "$@"
+
+test: $(TARGET)
+ CC="$(CC)" HOSTNAME_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..3435fefe5c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 1988, 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
+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.
diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000000..ea890afbc7
--- /dev/null
+++ b/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/README.md b/README.md
new file mode 100644
index 0000000000..11d83c35ae
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# hostname
+
+Standalone musl-libc-based Linux port of FreeBSD `hostname` 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
+```
+
+## Notes
+
+- Port strategy is Linux-native UTS syscall/API mapping, not a BSD compatibility shim.
+- Reading the hostname uses `uname(2)` and the Linux UTS `nodename` field.
+- Setting the hostname uses `sethostname(2)`.
+- `-s` returns the label before the first `.` in the kernel hostname; `-d` returns the suffix after the first `.` or an empty string if no suffix exists.
+- `-f` is intentionally unsupported on Linux in this port. A canonical FQDN requires NSS/DNS resolution policy outside the kernel UTS hostname, so the program exits with an explicit error instead of guessing.
+- Linux supports at most 64 bytes for the UTS hostname. Longer inputs are rejected with an explicit error instead of truncation.
+- Mutation tests run only inside a private UTS namespace when `unshare(1)` and the required namespace permissions are available; otherwise they are skipped to keep CI/container runs stable.
diff --git a/hostname.1 b/hostname.1
new file mode 100644
index 0000000000..eb38edd572
--- /dev/null
+++ b/hostname.1
@@ -0,0 +1,105 @@
+.\"-
+.\" Copyright (c) 1983, 1988, 1990, 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.
+.\"
+.Dd February 28, 2026
+.Dt HOSTNAME 1
+.Os
+.Sh NAME
+.Nm hostname
+.Nd set or print the name of current host system
+.Sh SYNOPSIS
+.Nm
+.Op Fl s | d
+.Op Ar name-of-host
+.Sh DESCRIPTION
+The
+.Nm
+utility prints the name of the current host.
+The super-user can
+set the hostname by supplying an argument; this is usually done in the
+initialization script
+.Pa /etc/rc.d/hostname ,
+normally run at boot
+time.
+This script uses the
+.Va hostname
+variable in
+.Pa /etc/rc.conf .
+.Pp
+Options:
+.Bl -tag -width flag
+.It Fl s
+Print only the hostname label before the first
+.Ql \&.
+in the Linux UTS hostname.
+.It Fl d
+Print only the suffix after the first
+.Ql \&.
+in the Linux UTS hostname.
+If no suffix exists, an empty line is printed.
+.El
+.Pp
+This Linux port reads the hostname from
+.Xr uname 2
+and writes it with
+.Xr sethostname 2 .
+The
+.Fl f
+flag is intentionally unsupported because a canonical FQDN requires
+NSS/DNS resolution policy outside the kernel hostname state.
+.Sh EXAMPLES
+Set the host name of the machine and check the result:
+.Bd -literal -offset indent
+$ hostname beastie.localdomain.org
+$ hostname
+beastie.localdomain.org
+.Ed
+.Pp
+Show only the host label:
+.Bd -literal -offset indent
+$ hostname -s
+beastie
+.Ed
+.Pp
+Show only domain information:
+.Bd -literal -offset indent
+$ hostname -d
+localdomain.org
+.Ed
+.Sh SEE ALSO
+.Xr sethostname 2 ,
+.Xr uname 2 ,
+.Xr rc.conf 5
+.Sh HISTORY
+The
+.Nm
+command appeared in
+.Bx 4.2 .
diff --git a/hostname.c b/hostname.c
new file mode 100644
index 0000000000..e7e80b02be
--- /dev/null
+++ b/hostname.c
@@ -0,0 +1,240 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1988, 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.
+ */
+
+#define _GNU_SOURCE 1
+
+#include <sys/utsname.h>
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+enum hostname_mode {
+ HOSTNAME_FULL,
+ HOSTNAME_SHORT,
+ HOSTNAME_DOMAIN
+};
+
+struct options {
+ enum hostname_mode mode;
+ const char *set_name;
+};
+
+static const char *progname = "hostname";
+
+static void die_errno(const char *what) __attribute__((noreturn));
+static void diex(const char *fmt, ...) __attribute__((format(printf, 1, 2),
+ noreturn));
+static char *dup_hostname(void);
+static size_t linux_hostname_max(void);
+static void parse_args(int argc, char *argv[], struct options *options);
+static void print_hostname(enum hostname_mode mode);
+static void set_hostname(const char *hostname);
+static void usage(void) __attribute__((noreturn));
+
+int
+main(int argc, char *argv[])
+{
+ struct options options;
+ const char *slash;
+
+ if (argv[0] != NULL && argv[0][0] != '\0') {
+ slash = strrchr(argv[0], '/');
+ progname = (slash != NULL && slash[1] != '\0') ? slash + 1 : argv[0];
+ }
+
+ parse_args(argc, argv, &options);
+ if (options.set_name != NULL)
+ set_hostname(options.set_name);
+ else
+ print_hostname(options.mode);
+
+ return (0);
+}
+
+static void
+parse_args(int argc, char *argv[], struct options *options)
+{
+ bool mode_selected;
+ int ch;
+
+ options->mode = HOSTNAME_FULL;
+ options->set_name = NULL;
+ mode_selected = false;
+
+ opterr = 0;
+ while ((ch = getopt(argc, argv, "fsd")) != -1) {
+ switch (ch) {
+ case 'f':
+ diex("option -f is not supported on Linux: FQDN resolution "
+ "depends on NSS/DNS, not the kernel hostname");
+ case 's':
+ if (mode_selected)
+ usage();
+ options->mode = HOSTNAME_SHORT;
+ mode_selected = true;
+ break;
+ case 'd':
+ if (mode_selected)
+ usage();
+ options->mode = HOSTNAME_DOMAIN;
+ mode_selected = true;
+ break;
+ case '?':
+ default:
+ usage();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc > 1)
+ usage();
+
+ if (argc == 1) {
+ if (options->mode != HOSTNAME_FULL)
+ usage();
+ options->set_name = argv[0];
+ }
+}
+
+static char *
+dup_hostname(void)
+{
+ struct utsname uts;
+ size_t len;
+ char *name;
+
+ if (uname(&uts) != 0)
+ die_errno("uname");
+
+ len = strnlen(uts.nodename, sizeof(uts.nodename));
+ if (len >= sizeof(uts.nodename))
+ diex("uname returned a non-terminated nodename");
+
+ name = malloc(len + 1);
+ if (name == NULL)
+ die_errno("malloc");
+
+ memcpy(name, uts.nodename, len + 1);
+ return (name);
+}
+
+static void
+print_hostname(enum hostname_mode mode)
+{
+ char *hostname;
+ char *dot;
+ const char *out;
+
+ hostname = dup_hostname();
+ out = hostname;
+ dot = strchr(hostname, '.');
+
+ switch (mode) {
+ case HOSTNAME_FULL:
+ break;
+ case HOSTNAME_SHORT:
+ if (dot != NULL)
+ *dot = '\0';
+ break;
+ case HOSTNAME_DOMAIN:
+ out = (dot != NULL) ? dot + 1 : "";
+ break;
+ }
+
+ if (fputs(out, stdout) == EOF || fputc('\n', stdout) == EOF)
+ die_errno("stdout");
+
+ free(hostname);
+}
+
+static void
+set_hostname(const char *hostname)
+{
+ size_t len;
+ size_t max_len;
+
+ len = strlen(hostname);
+ max_len = linux_hostname_max();
+ if (len > max_len) {
+ diex("host name too long for Linux UTS namespace: %zu bytes given, "
+ "limit is %zu", len, max_len);
+ }
+
+ if (sethostname(hostname, len) != 0)
+ die_errno("sethostname");
+}
+
+static size_t
+linux_hostname_max(void)
+{
+ struct utsname uts;
+
+ return (sizeof(uts.nodename) - 1);
+}
+
+static void
+die_errno(const char *what)
+{
+ fprintf(stderr, "%s: %s: %s\n", progname, what, strerror(errno));
+ exit(1);
+}
+
+static void
+diex(const char *fmt, ...)
+{
+ va_list ap;
+
+ fprintf(stderr, "%s: ", progname);
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ fputc('\n', stderr);
+ exit(1);
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-s | -d] [name-of-host]\n", progname);
+ exit(1);
+}
diff --git a/tests/test.sh b/tests/test.sh
new file mode 100644
index 0000000000..e8bd9316eb
--- /dev/null
+++ b/tests/test.sh
@@ -0,0 +1,269 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+HOSTNAME_BIN=${HOSTNAME_BIN:-"$ROOT/out/hostname"}
+CC=${CC:-cc}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/hostname-test.XXXXXX")
+HELPER_C="$WORKDIR/get-hostname.c"
+HELPER_BIN="$WORKDIR/get-hostname"
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+trap 'rm -rf "$WORKDIR"' EXIT INT TERM
+
+export LC_ALL=C
+
+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
+}
+
+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")
+}
+
+build_helper() {
+ cat >"$HELPER_C" <<'EOF'
+#define _GNU_SOURCE 1
+#include <sys/utsname.h>
+
+#include <stdio.h>
+
+int
+main(void)
+{
+ struct utsname uts;
+
+ if (uname(&uts) != 0)
+ return (1);
+ if (fputs(uts.nodename, stdout) == EOF || fputc('\n', stdout) == EOF)
+ return (1);
+ return (0);
+}
+EOF
+
+ "$CC" -O2 -std=c17 -Wall -Wextra -Werror "$HELPER_C" -o "$HELPER_BIN" \
+ || fail "failed to build helper with $CC"
+}
+
+repeat_a() {
+ count=$1
+ result=
+ while [ "$count" -gt 0 ]; do
+ result="${result}a"
+ count=$((count - 1))
+ done
+ printf '%s' "$result"
+}
+
+run_namespace_mutation_test() {
+ max_name=$1
+ unshare_script='
+ bin=$1
+ helper=$2
+ max_name=$3
+
+ "$bin" freebsdport
+ [ "$("$helper")" = "freebsdport" ]
+ [ "$("$bin")" = "freebsdport" ]
+ [ "$("$bin" -s)" = "freebsdport" ]
+ [ "$("$bin" -d)" = "" ]
+
+ "$bin" "$max_name"
+ [ "$("$helper")" = "$max_name" ]
+ [ "$("$bin")" = "$max_name" ]
+ [ "$("$bin" -s)" = "$max_name" ]
+ [ "$("$bin" -d)" = "" ]
+
+ "$bin" freebsd-linux-port.example.test
+ [ "$("$helper")" = "freebsd-linux-port.example.test" ]
+ [ "$("$bin")" = "freebsd-linux-port.example.test" ]
+ [ "$("$bin" -s)" = "freebsd-linux-port" ]
+ [ "$("$bin" -d)" = "example.test" ]
+
+ "$bin" -- -leading-dash
+ [ "$("$helper")" = "-leading-dash" ]
+ [ "$("$bin" -s)" = "-leading-dash" ]
+ [ "$("$bin" -d)" = "" ]
+ '
+
+ if ! command -v unshare >/dev/null 2>&1; then
+ printf '%s\n' "SKIP: mutation test (missing unshare)" >&2
+ return 0
+ fi
+
+ set +e
+ output=$(unshare -Ur -u sh -eu -c "$unshare_script" sh "$HOSTNAME_BIN" \
+ "$HELPER_BIN" "$max_name" 2>&1)
+ status=$?
+ set -e
+
+ if [ "$status" -eq 0 ]; then
+ return 0
+ fi
+
+ case $output in
+ *"cannot open /proc/self/uid_map: Permission denied"*|\
+ *"unshare: invalid option -- 'r'"*|\
+ *"unshare: unrecognized option '--map-root-user'"*)
+ set +e
+ output=$(unshare -u sh -eu -c "$unshare_script" sh "$HOSTNAME_BIN" \
+ "$HELPER_BIN" "$max_name" 2>&1)
+ status=$?
+ set -e
+ if [ "$status" -eq 0 ]; then
+ return 0
+ fi
+ ;;
+ esac
+
+ case $output in
+ *"unshare failed: Operation not permitted"*|\
+ *"unshare failed: Invalid argument"*|\
+ *"unshare failed: No space left on device"*|\
+ *"unshare: cannot open /proc/self/ns/uts: Permission denied"*|\
+ *"unshare: invalid option"*|\
+ *"unshare: unshare failed: Operation not permitted"*|\
+ *"unshare: unshare failed: Invalid argument"*|\
+ *"unshare: unshare failed: No space left on device"*|\
+ *"sethostname: Operation not permitted"*)
+ printf '%s\n' "SKIP: mutation test ($output)" >&2
+ return 0
+ ;;
+ esac
+
+ fail "namespace mutation test failed: $output"
+}
+
+[ -x "$HOSTNAME_BIN" ] || fail "missing binary: $HOSTNAME_BIN"
+build_helper
+
+expected_hostname=$("$HELPER_BIN")
+case $expected_hostname in
+*.*)
+ expected_short=${expected_hostname%%.*}
+ expected_domain=${expected_hostname#*.}
+ ;;
+*)
+ expected_short=$expected_hostname
+ expected_domain=
+ ;;
+esac
+
+run_capture "$HOSTNAME_BIN"
+assert_status "current hostname status" 0 "$LAST_STATUS"
+assert_eq "current hostname stdout" "$expected_hostname" "$LAST_STDOUT"
+assert_empty "current hostname stderr" "$LAST_STDERR"
+
+run_capture "$HOSTNAME_BIN" -s
+assert_status "short hostname status" 0 "$LAST_STATUS"
+assert_eq "short hostname stdout" "$expected_short" "$LAST_STDOUT"
+assert_empty "short hostname stderr" "$LAST_STDERR"
+
+run_capture "$HOSTNAME_BIN" -d
+assert_status "domain hostname status" 0 "$LAST_STATUS"
+assert_eq "domain hostname stdout" "$expected_domain" "$LAST_STDOUT"
+assert_empty "domain hostname stderr" "$LAST_STDERR"
+
+run_capture "$HOSTNAME_BIN" -x
+assert_status "bad flag status" 1 "$LAST_STATUS"
+assert_empty "bad flag stdout" "$LAST_STDOUT"
+assert_eq "bad flag stderr" "usage: hostname [-s | -d] [name-of-host]" \
+ "$LAST_STDERR"
+
+run_capture "$HOSTNAME_BIN" first second
+assert_status "too many args status" 1 "$LAST_STATUS"
+assert_empty "too many args stdout" "$LAST_STDOUT"
+assert_eq "too many args stderr" "usage: hostname [-s | -d] [name-of-host]" \
+ "$LAST_STDERR"
+
+run_capture "$HOSTNAME_BIN" -s -d
+assert_status "conflicting flags status" 1 "$LAST_STATUS"
+assert_empty "conflicting flags stdout" "$LAST_STDOUT"
+assert_eq "conflicting flags stderr" "usage: hostname [-s | -d] [name-of-host]" \
+ "$LAST_STDERR"
+
+run_capture "$HOSTNAME_BIN" -s newname
+assert_status "set with mode status" 1 "$LAST_STATUS"
+assert_empty "set with mode stdout" "$LAST_STDOUT"
+assert_eq "set with mode stderr" "usage: hostname [-s | -d] [name-of-host]" \
+ "$LAST_STDERR"
+
+run_capture "$HOSTNAME_BIN" -f
+assert_status "unsupported -f status" 1 "$LAST_STATUS"
+assert_empty "unsupported -f stdout" "$LAST_STDOUT"
+assert_eq "unsupported -f stderr" \
+ "hostname: option -f is not supported on Linux: FQDN resolution depends on NSS/DNS, not the kernel hostname" \
+ "$LAST_STDERR"
+
+long_name=$(repeat_a 65)
+max_name=$(repeat_a 64)
+
+run_capture "$HOSTNAME_BIN" "$long_name"
+assert_status "too-long hostname status" 1 "$LAST_STATUS"
+assert_empty "too-long hostname stdout" "$LAST_STDOUT"
+assert_eq "too-long hostname stderr" \
+ "hostname: host name too long for Linux UTS namespace: 65 bytes given, limit is 64" \
+ "$LAST_STDERR"
+
+run_namespace_mutation_test "$max_name"
+
+printf '%s\n' "PASS"