git: 3fe5961a0b70 - main - Add sysconf(8) and libbsdconf(3)
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Wed, 16 Sep 2026 21:31:28 UTC
The branch main has been updated by dteske:
URL: https://cgit.FreeBSD.org/src/commit/?id=3fe5961a0b708da599d42cbb6b5e4f030c28ea45
commit 3fe5961a0b708da599d42cbb6b5e4f030c28ea45
Author: Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-09-16 04:29:32 +0000
Commit: Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-09-16 21:28:14 +0000
Add sysconf(8) and libbsdconf(3)
Complete the native configuration trinity: sysctl(8) for live kernel
state, sysrc(8) for rc.conf(5), and sysconf(8) for the remaining base
configuration -- loader.conf(5), sysctl.conf(5), and the make.conf(5)
family -- atop libbsdconf(3).
libbsdconf resurrects figpar as a unified reader/writer. Callbacks own
semantics; statements may span multiple lines via backslash continuation;
non-seekable input is spooled; writes are atomic (mkstemp, fsync, rename)
with mode/owner preservation. Format descriptors name each target, its
files, and quoting rules without private parsers. Multi-file targets
follow boot sourcing order; loader chases loader_conf_files as the boot
loader does.
sysconf(8) is the operator-facing tool: name / name=value on a required
target, sysrc-style list edits, make append and list-strike where they
belong, jail/altroot, and a capsicum sandbox for read-only use.
Sysctl writes validate against the running kernel first -- unknown and
read-only OIDs, CTLFLAG_TUN (pointing at the loader target), and CTLTYPE
range checks -- so a typo or overflow does not land in sysctl.conf.
Make and src treat WITH_/WITHOUT_ as presence knobs (as bsd.mkopt.mk /
src.conf(5) do) and warn on the WITH_*=no form that does not disable the
option, so a bad assignment is caught before an /usr/src build surfaces
it.
The rc target passes through to sysrc(8).
Defaults querying (-d/-D/-A) mirrors sysrc for dumps and descriptions on
targets that have a defaults file; named reads already see defaults, and
-A only widens dump scope.
Manuals are split pkg(8)-style (bsdconf/put/format; sysconf plus
per-target pages). ATF coverage exercises the frontend.
Co-authored-by: Faraz Vahedi <kfv@FreeBSD.org>
Reviewed by: fuz, kfv
Differential Revision: https://reviews.freebsd.org/D58066
---
contrib/mandoc/lib.in | 1 +
etc/mtree/BSD.tests.dist | 2 +
lib/Makefile | 1 +
lib/libbsdconf/Makefile | 30 ++
lib/libbsdconf/Makefile.depend | 15 +
lib/libbsdconf/bsdconf.3 | 432 ++++++++++++++++++
lib/libbsdconf/bsdconf.c | 733 ++++++++++++++++++++++++++++++
lib/libbsdconf/bsdconf.h | 281 ++++++++++++
lib/libbsdconf/bsdconf_format.3 | 560 +++++++++++++++++++++++
lib/libbsdconf/bsdconf_format.c | 693 +++++++++++++++++++++++++++++
lib/libbsdconf/bsdconf_format_generic.c | 32 ++
lib/libbsdconf/bsdconf_format_loader.c | 51 +++
lib/libbsdconf/bsdconf_format_make.c | 31 ++
lib/libbsdconf/bsdconf_format_src.c | 38 ++
lib/libbsdconf/bsdconf_format_sysctl.c | 36 ++
lib/libbsdconf/bsdconf_formats.h | 37 ++
lib/libbsdconf/bsdconf_internal.h | 71 +++
lib/libbsdconf/bsdconf_put.3 | 290 ++++++++++++
lib/libbsdconf/bsdconf_put.c | 537 ++++++++++++++++++++++
lib/libbsdconf/bsdconf_stmt.c | 547 +++++++++++++++++++++++
lib/libbsdconf/bsdconf_string.c | 245 ++++++++++
share/mk/bsd.libnames.mk | 1 +
share/mk/src.libnames.mk | 1 +
usr.sbin/Makefile | 1 +
usr.sbin/sysconf/Makefile | 32 ++
usr.sbin/sysconf/Makefile.depend | 17 +
usr.sbin/sysconf/sysconf-generic.8 | 49 ++
usr.sbin/sysconf/sysconf-loader.8 | 151 +++++++
usr.sbin/sysconf/sysconf-make.8 | 231 ++++++++++
usr.sbin/sysconf/sysconf-rc.8 | 56 +++
usr.sbin/sysconf/sysconf-src.8 | 211 +++++++++
usr.sbin/sysconf/sysconf-sysctl.8 | 146 ++++++
usr.sbin/sysconf/sysconf-targets.8 | 174 ++++++++
usr.sbin/sysconf/sysconf.8 | 704 +++++++++++++++++++++++++++++
usr.sbin/sysconf/sysconf.c | 761 ++++++++++++++++++++++++++++++++
usr.sbin/sysconf/sysconf_edit_assign.c | 221 ++++++++++
usr.sbin/sysconf/sysconf_edit_make.c | 274 ++++++++++++
usr.sbin/sysconf/sysconf_edit_write.c | 264 +++++++++++
usr.sbin/sysconf/sysconf_print.c | 320 ++++++++++++++
usr.sbin/sysconf/sysconf_priv.h | 203 +++++++++
usr.sbin/sysconf/sysconf_query_desc.c | 209 +++++++++
usr.sbin/sysconf/sysconf_query_sysctl.c | 285 ++++++++++++
usr.sbin/sysconf/sysconf_query_value.c | 79 ++++
usr.sbin/sysconf/sysconf_resolve.c | 337 ++++++++++++++
usr.sbin/sysconf/sysconf_scan.c | 288 ++++++++++++
usr.sbin/sysconf/tests/Makefile | 11 +
usr.sbin/sysconf/tests/sysconf_test.sh | 431 ++++++++++++++++++
47 files changed, 10120 insertions(+)
diff --git a/contrib/mandoc/lib.in b/contrib/mandoc/lib.in
index 04d86f9add1e..da343264c6d4 100644
--- a/contrib/mandoc/lib.in
+++ b/contrib/mandoc/lib.in
@@ -31,6 +31,7 @@ LINE("libarm", "ARM Architecture Library (libarm, \\-larm)")
LINE("libarm32", "ARM32 Architecture Library (libarm32, \\-larm32)")
LINE("libbe", "Boot Environment Library (libbe, \\-lbe)")
LINE("libbluetooth", "Bluetooth Library (libbluetooth, \\-lbluetooth)")
+LINE("libbsdconf", "Configuration File Library (libbsdconf, \\-lbsdconf)")
LINE("libbsdxml", "eXpat XML parser library (libbsdxml, \\-lbsdxml)")
LINE("libbsm", "Basic Security Module Library (libbsm, \\-lbsm)")
LINE("libc", "Standard C\\~Library (libc, \\-lc)")
diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index 2ab781fe4b6c..84bd2f09e7cc 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1323,6 +1323,8 @@
..
sa
..
+ sysconf
+ ..
syslogd
..
sysrc
diff --git a/lib/Makefile b/lib/Makefile
index 3391f276c7ef..78b48382e12e 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -36,6 +36,7 @@ SUBDIR= ${SUBDIR_BOOTSTRAP} \
libarchive \
libbegemot \
libblocksruntime \
+ libbsdconf \
libbsddialog \
libbsdstat \
libbsm \
diff --git a/lib/libbsdconf/Makefile b/lib/libbsdconf/Makefile
new file mode 100644
index 000000000000..f9f74c82318f
--- /dev/null
+++ b/lib/libbsdconf/Makefile
@@ -0,0 +1,30 @@
+PACKAGE= utilities
+LIB= bsdconf
+SHLIB_MAJOR= 1
+INCS= bsdconf.h
+MAN= bsdconf.3 bsdconf_format.3 bsdconf_put.3
+MLINKS= bsdconf.3 bsdconf_fparse.3 \
+ bsdconf.3 bsdconf_get_option.3 \
+ bsdconf.3 bsdconf_parse.3 \
+ bsdconf.3 bsdconf_spool.3 \
+ bsdconf.3 bsdconf_unquote.3 \
+ bsdconf_format.3 bsdconf_format_derive.3 \
+ bsdconf_format.3 bsdconf_format_files.3 \
+ bsdconf_format.3 bsdconf_format_files_free.3 \
+ bsdconf_format.3 bsdconf_format_find.3 \
+ bsdconf_format.3 bsdconf_format_guess.3 \
+ bsdconf_format.3 bsdconf_format_lookup.3 \
+ bsdconf_format.3 bsdconf_format_path.3 \
+ bsdconf_format.3 bsdconf_format_processing.3 \
+ bsdconf_format.3 bsdconf_format_put.3 \
+ bsdconf_format.3 bsdconf_format_register.3 \
+ bsdconf_put.3 bsdconf_set_option.3
+
+CFLAGS+= -I${.CURDIR}
+
+SRCS= bsdconf.c bsdconf_format.c bsdconf_format_generic.c \
+ bsdconf_format_loader.c bsdconf_format_make.c \
+ bsdconf_format_src.c bsdconf_format_sysctl.c \
+ bsdconf_put.c bsdconf_stmt.c bsdconf_string.c
+
+.include <bsd.lib.mk>
diff --git a/lib/libbsdconf/Makefile.depend b/lib/libbsdconf/Makefile.depend
new file mode 100644
index 000000000000..6ef78fac5cbf
--- /dev/null
+++ b/lib/libbsdconf/Makefile.depend
@@ -0,0 +1,15 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ include \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libc \
+ lib/libcompiler_rt \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/lib/libbsdconf/bsdconf.3 b/lib/libbsdconf/bsdconf.3
new file mode 100644
index 000000000000..a1452e43adfd
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.3
@@ -0,0 +1,432 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+.Dt BSDCONF 3
+.Os
+.Sh NAME
+.Nm bsdconf ,
+.Nm bsdconf_parse ,
+.Nm bsdconf_fparse ,
+.Nm bsdconf_get_option ,
+.Nm bsdconf_spool ,
+.Nm bsdconf_unquote
+.Nd configuration file reading library
+.Sh LIBRARY
+.Lb libbsdconf
+.Sh SYNOPSIS
+.In bsdconf.h
+.Ft int
+.Fo bsdconf_parse
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *path"
+.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
+.Fa "uint32_t line"
+.Fa "char *directive"
+.Fa "char *value\*[rp]"
+.Fa "uint16_t processing_options"
+.Fc
+.Ft int
+.Fo bsdconf_fparse
+.Fa "struct bsdconf_option options[]"
+.Fa "int fd"
+.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
+.Fa "uint32_t line"
+.Fa "char *directive"
+.Fa "char *value\*[rp]"
+.Fa "uint16_t processing_options"
+.Fc
+.Ft "struct bsdconf_option *"
+.Fo bsdconf_get_option
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *directive"
+.Fc
+.Ft int
+.Fo bsdconf_spool
+.Fa "int fd"
+.Fc
+.Ft "char *"
+.Fo bsdconf_unquote
+.Fa "char *value"
+.Fc
+.Sh DESCRIPTION
+The
+.Nm
+library provides a light-weight,
+portable framework for reading and writing configuration
+files.
+It is the successor to the retired
+.Nm figpar
+library,
+extending the original read-only token parser into a unified,
+resilient reader/writer engine.
+.Pp
+Due to the fact that configuration files may have basic syntax differences,
+the library does not attempt to impose any structure on the data but instead
+provides raw data to a set of callback functions.
+These callback functions can in-turn initiate abort through their return
+value,
+allowing custom syntax validation during parsing.
+.Pp
+Syntax differences between well-known file formats are described by format
+descriptors:
+a format descriptor is a small read-only table of properties
+.Pq Vt struct bsdconf_format_def ; see Xr bsdconf_format 3
+naming a format's target keyword,
+backing files,
+and tokenizing/quoting rules,
+which parameterize a single shared engine.
+.Pp
+Despite the name, it bears no relation to a file descriptor;
+it is closer in spirit to a driver's method table:
+static data describing behavior,
+consulted rather than executed.
+Writing is documented in
+.Xr bsdconf_put 3 ;
+built-in formats and discovery in
+.Xr bsdconf_format 3 .
+.Pp
+Configuration directives,
+types,
+and callback functions are provided through data structures defined in
+.In bsdconf.h :
+.Bd -literal -offset indent
+struct bsdconf_option {
+ enum bsdconf_type type; /* value type */
+ const char *directive; /* keyword */
+ union bsdconf_value value; /* value */
+ enum bsdconf_op op; /* assignment operator */
+ uint8_t action; /* bsdconf_put() action */
+ uint16_t result; /* set by bsdconf_put() */
+ uint32_t line; /* set by bsdconf_put() */
+ uint32_t match_line;
+ /* put: 0=any, else line */
+
+ /* Pointer to function used when directive is found */
+ int (*parse)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value);
+};
+
+enum bsdconf_type {
+ BSDCONF_TYPE_NONE = 0x0000, /* directives with no value */
+ BSDCONF_TYPE_BOOL = 0x0001, /* boolean */
+ BSDCONF_TYPE_INT = 0x0002, /* signed 32-bit integer */
+ BSDCONF_TYPE_UINT = 0x0004, /* unsigned 32-bit integer */
+ BSDCONF_TYPE_STR = 0x0008, /* string pointer */
+ BSDCONF_TYPE_STRARRAY = 0x0010, /* string array pointer */
+ BSDCONF_TYPE_DATA1 = 0x0020, /* void data type-1 (open) */
+ BSDCONF_TYPE_DATA2 = 0x0040, /* void data type-2 (open) */
+ BSDCONF_TYPE_DATA3 = 0x0080, /* void data type-3 (open) */
+ BSDCONF_TYPE_INT64 = 0x0100, /* signed 64-bit integer */
+ BSDCONF_TYPE_UINT64 = 0x0200, /* unsigned 64-bit integer */
+ BSDCONF_TYPE_RESERVED = 0x0400, /* reserved */
+};
+
+union bsdconf_value {
+ void *data; /* Opaque pointer (DATA1..DATA3) */
+ char *str; /* Pointer to NUL-terminated string */
+ char **strarray; /* Pointer to an array of strings */
+ int32_t num; /* Signed 32-bit integer value */
+ uint32_t u_num; /* Unsigned 32-bit integer value */
+ int64_t num64; /* Signed 64-bit integer value */
+ uint64_t u_num64; /* Unsigned 64-bit integer value */
+ bool boolean; /* Boolean value */
+};
+.Ed
+.Pp
+The
+.Fa processing_options
+argument to
+.Fn bsdconf_parse ,
+.Fn bsdconf_fparse ,
+and
+.Xr bsdconf_put 3
+is a mask of bit fields which indicate various processing options.
+The possible flags are:
+.Bl -tag -width BSDCONF_BREAK_ON_SEMICOLON
+.It Dv BSDCONF_BREAK_ON_EQUALS
+An equals sign
+.Pq Ql =
+is normally considered part of the directive.
+This flag enables terminating the directive at the equals sign.
+Also makes equals sign optional and transient.
+.It Dv BSDCONF_BREAK_ON_SEMICOLON
+A semicolon
+.Pq Ql \&;
+is normally considered part of the value.
+This flag enables terminating the value at the semicolon.
+Also allows multiple statements on a single line separated by semicolon.
+.It Dv BSDCONF_CASE_SENSITIVE
+Normally directives are matched case insensitively using
+.Xr fnmatch 3 .
+This flag enables directive matching to be case sensitive.
+.It Dv BSDCONF_REQUIRE_EQUALS
+If a directive is not followed by an equals,
+processing is aborted.
+.It Dv BSDCONF_STRICT_EQUALS
+Equals must be part of the directive
+.Pq no whitespace before or after
+to be considered a delimiter between directive and value.
+Required by file formats whose readers reject whitespace around the equals
+sign,
+such as the
+.Fx
+boot loader's processing of
+.Xr loader.conf 5 .
+.It Dv BSDCONF_OPERATOR_EQUALS
+Recognize
+.Xr make 1
+style assignment modifiers
+.Po
+.Ql += ,
+.Ql ?= ,
+.Ql := ,
+and
+.Ql !=
+.Pc
+and split them off the tail of the directive.
+The parsed operator is reported through the
+.Va op
+member of the matched option
+.Pq one of Dv BSDCONF_OP_ASSIGN , BSDCONF_OP_APPEND , BSDCONF_OP_COND , BSDCONF_OP_EXPAND , No or Dv BSDCONF_OP_SHELL .
+.El
+.Pp
+The
+.Fa options
+struct array pointer can be NULL and every directive will run the
+.Fn unknown
+function argument.
+.Pp
+The directive for each bsdconf_option item in the
+.Fn bsdconf_parse
+options argument is matched against each parsed directive using
+.Xr fnmatch 3
+until a match is found.
+If a match is found,
+the
+.Fn parse
+function for that bsdconf_option directive is run with the line number,
+directive,
+and value.
+Otherwise if no match,
+the
+.Fn unknown
+function is run
+.Pq with the same arguments .
+When
+.Dv BSDCONF_OPERATOR_EQUALS
+is set,
+.Fn unknown
+receives a non-NULL
+.Fa option
+whose
+.Va op
+member holds the statement's assignment operator
+.Pq there is no matched options-array slot to hang it on ;
+otherwise
+.Fa option
+may be
+.Dv NULL .
+.Pp
+If either
+.Fn parse
+or
+.Fn unknown
+return non-zero,
+.Fn bsdconf_parse
+aborts reading the file and returns the error value to its caller.
+.Pp
+A value normally ends at the first unescaped newline,
+but a statement may span multiple lines:
+a backslash immediately preceding the newline continues the value on the
+next line,
+in the manner of
+.Xr make 1
+.Pq essential to Pa make.conf and its siblings .
+The backslash-newline pairs are removed from the value delivered to the
+callbacks
+.Pq surrounding whitespace is preserved verbatim ,
+and reported line numbers are those of each statement's first line.
+.Xr bsdconf_put 3
+recognizes the same continuations when locating a value;
+rewriting a continued value replaces all of its lines with the single new
+value.
+.Pp
+.Fn bsdconf_fparse
+is identical to
+.Fn bsdconf_parse
+except that it operates on an already-open file descriptor
+.Fa fd ,
+which remains open on return
+.Pq the caller retains ownership .
+This allows the caller to constrain the process
+.Pq for example with Xr capsicum 4
+before parsing begins.
+The scanner requires a seekable descriptor;
+input that cannot seek
+.Pq a pipe or socket, standard input included
+is detected up front and transparently spooled through
+.Fn bsdconf_spool ,
+at the cost of one transient copy of the data.
+.Pp
+.Fn bsdconf_spool
+copies the remaining contents of
+.Fa fd
+to an unlinked temporary file
+.Pq created with Xr tmpfile 3
+and returns a seekable descriptor referencing it,
+which the caller must
+.Xr close 2
+.Pq the backing storage is reclaimed then .
+It is exported for callers that must adapt non-seekable input themselves
+before revoking their own ability to create files,
+as
+.Xr sysconf 8
+does before entering its
+.Xr capsicum 4
+sandbox.
+.Pp
+.Fn bsdconf_get_option
+traverses the options-array and returns the option that matches via
+.Xr strcmp 3 ,
+or
+.Dv NULL
+if none matches.
+.Pp
+.Fn bsdconf_unquote
+strips one layer of surrounding double-quotes from
+.Fa value
+in place and returns it.
+Parsed values retain their quotes so that data round-trips losslessly;
+this helper is for display and comparison purposes.
+.Sh RETURN VALUES
+.Fn bsdconf_parse
+and
+.Fn bsdconf_fparse
+return zero on success;
+otherwise -1
+.Pq or the non-zero result of a callback
+is returned and the global variable
+.Va errno
+is set to indicate the error.
+.Fn bsdconf_spool
+returns a new seekable file descriptor on success;
+otherwise -1 with
+.Va errno
+set to indicate the error.
+.Fn bsdconf_get_option
+returns a pointer to the matching option,
+or
+.Dv NULL
+when none matches.
+.Sh EXAMPLES
+Read two known directives from a
+.Ql name=value
+file,
+routing every statement through a callback:
+.Bd -literal -offset indent
+#include <err.h>
+#include <stdio.h>
+#include <bsdconf.h>
+
+static int
+show(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value)
+{
+ printf("%u: %s is %s\en", line, directive,
+ bsdconf_unquote(value));
+ return (0);
+}
+
+static struct bsdconf_option options[] = {
+ { .directive = "hostname", .parse = show },
+ { .directive = "timeout", .parse = show },
+ { .directive = NULL }
+};
+
+int
+main(void)
+{
+ if (bsdconf_parse(options, "/usr/local/etc/myapp.conf",
+ NULL, BSDCONF_BREAK_ON_EQUALS) != 0)
+ err(1, "myapp.conf");
+ return (0);
+}
+.Ed
+.Pp
+Callbacks own the semantics,
+so a directive that legitimately repeats is accumulated rather than
+overwritten;
+no format descriptor is involved.
+The file here is Apache-style
+.Pq space-separated, no equals sign ,
+so
+.Dv BSDCONF_BREAK_ON_EQUALS
+is simply omitted:
+.Bd -literal -offset indent
+static char *servers[16];
+static size_t nservers;
+
+static int
+addserver(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value)
+{
+ if (nservers >= 16 ||
+ (servers[nservers] = strdup(value)) == NULL)
+ return (-1); /* abort the parse */
+ nservers++;
+ return (0);
+}
+
+static struct bsdconf_option cumulative[] = {
+ { .directive = "server", .parse = addserver },
+ { .directive = NULL }
+};
+
+ ...
+ if (bsdconf_parse(cumulative, path, NULL, 0) != 0)
+ err(1, "%s", path);
+.Ed
+.Sh SEE ALSO
+.Xr bsdconf_format 3 ,
+.Xr bsdconf_put 3 ,
+.Xr loader.conf 5 ,
+.Xr sysctl.conf 5 ,
+.Xr sysconf 8
+.Sh HISTORY
+The
+.Nm
+library first appeared in
+.Fx 16.0 .
+It supersedes the
+.Nm figpar
+library which first appeared in
+.Fx 10.2
+and was retired to the ports tree.
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@FreeBSD.org
+.Sh BUGS
+This is the first implementation of the library,
+and the interface may be subject to refinement.
+.Pp
+Write-path limitations for cumulative directives are discussed in the
+.Sx LIMITATIONS
+section of
+.Xr bsdconf_put 3 .
+.Sh SECURITY CONSIDERATIONS
+Parsing allocates buffers sized by the longest directive and value
+encountered rather than by untrusted length fields,
+and
+.Fn bsdconf_fparse
+accepts an already-open descriptor precisely so that a caller may
+sandbox itself
+.Pq for example with Xr capsicum 4
+before touching untrusted input,
+as
+.Xr sysconf 8
+does for its read-only operations.
+Write-path hardening is documented in
+.Xr bsdconf_put 3 .
diff --git a/lib/libbsdconf/bsdconf.c b/lib/libbsdconf/bsdconf.c
new file mode 100644
index 000000000000..341fad9b841c
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.c
@@ -0,0 +1,733 @@
+/*
+ * Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+/*
+ * Search for a config option (struct bsdconf_option) in the array of config
+ * options, returning the struct whose directive matches the given parameter.
+ * If no match is found, NULL is returned.
+ *
+ * This is to eliminate dependency on the index position of an item in the
+ * array, since the index position is more apt to be changed as code grows.
+ */
+struct bsdconf_option *
+bsdconf_get_option(struct bsdconf_option options[], const char *directive)
+{
+ uint32_t n;
+
+ if (options == NULL || directive == NULL)
+ return (NULL);
+
+ for (n = 0; options[n].directive != NULL; n++)
+ if (strcmp(options[n].directive, directive) == 0)
+ return (&options[n]);
+
+ return (NULL);
+}
+
+/*
+ * Strip one layer of surrounding double-quotes from `value' in place (the
+ * parser preserves quotes so that values round-trip losslessly; see
+ * bsdconf_fparse() below). Returns `value' for convenience. If the value is
+ * not a quoted string, it is returned unmodified.
+ */
+char *
+bsdconf_unquote(char *value)
+{
+ size_t len;
+
+ if (value == NULL || (len = strlen(value)) < 2)
+ return (value);
+ if (value[0] != '"' || value[len - 1] != '"')
+ return (value);
+
+ memmove(value, value + 1, len - 2);
+ value[len - 2] = '\0';
+
+ return (value);
+}
+
+/*
+ * Copy the remaining contents of the open file descriptor `fd' to an
+ * unlinked temporary file and return a seekable descriptor referencing it
+ * (which the caller must close(2); the backing storage is reclaimed then).
+ * This adapts input that cannot seek -- a pipe or socket, standard input
+ * included -- for the scanner in bsdconf_fparse() below, which seeks
+ * liberally. Returns the new descriptor on success; otherwise returns -1
+ * and errno should be consulted.
+ */
+int
+bsdconf_spool(int fd)
+{
+ FILE *tmp;
+ int error;
+ int newfd;
+ int tfd;
+ ssize_t r;
+ char buf[8192];
+
+ if ((tmp = tmpfile()) == NULL)
+ return (-1);
+ tfd = fileno(tmp);
+
+ for (;;) {
+ r = read(fd, buf, sizeof(buf));
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ goto fail;
+ }
+ if (r == 0)
+ break;
+ if (bsdconf_writeall(tfd, buf, (size_t)r) != 0)
+ goto fail;
+ }
+ if (lseek(tfd, 0, SEEK_SET) == -1)
+ goto fail;
+
+ /* Detach the descriptor from the stream before closing it */
+ if ((newfd = dup(tfd)) == -1)
+ goto fail;
+ fclose(tmp);
+ return (newfd);
+
+fail:
+ error = errno; /* preserve errno across fclose(3) */
+ fclose(tmp);
+ errno = error;
+ return (-1);
+}
+
+/*
+ * Read one byte into `*p', restarting on EINTR. Returns 1 on success, 0 on
+ * EOF, or -1 on error (with errno set). Callers must treat a negative return
+ * as failure: a loop conditioned only on `r != 0' spins forever on error
+ * because read(2) returns -1, and a length counter in such a loop can grow
+ * without bound (see the directive scan in bsdconf_fparse() below).
+ */
+static ssize_t
+bsdconf_read1(int fd, char *p)
+{
+ ssize_t r;
+
+ do {
+ r = read(fd, p, 1);
+ } while (r < 0 && errno == EINTR);
+ return (r);
+}
+
+/*
+ * Read exactly `n' bytes into `buf', restarting on EINTR. Returns 0 on
+ * success, or -1 on error / premature EOF (with errno set; EIO for a short
+ * read after the caller measured a length on a seekable descriptor).
+ */
+static int
+bsdconf_readn(int fd, void *buf, size_t n)
+{
+ char *p = buf;
+ size_t off = 0;
+ ssize_t r;
+
+ while (off < n) {
+ r = read(fd, p + off, n - off);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ return (-1);
+ }
+ if (r == 0) {
+ errno = EIO;
+ return (-1);
+ }
+ off += (size_t)r;
+ }
+ return (0);
+}
+
+/*
+ * Advance past horizontal whitespace (spaces and tabs, not newline).
+ * Updates `*r' and the byte in `*p'. Returns 0 on success, or -1 on
+ * read error (errno set).
+ */
+static int
+bsdconf_skip_hspace(int fd, char *p, ssize_t *r)
+{
+
+ while (*r > 0 && isspace((unsigned char)*p) && *p != '\n') {
+ *r = bsdconf_read1(fd, p);
+ if (*r < 0)
+ return (-1);
+ }
+ return (0);
+}
+
+/*
+ * Truncate trailing whitespace from a NUL-terminated string whose end
+ * (the NUL) is at `end'. Returns a pointer to the last remaining
+ * character, or to `value' when the string is empty.
+ */
+static char *
+bsdconf_rtrim_ws(char *value, char *end)
+{
+ char *t = end;
+
+ while (t > value && isspace((unsigned char)*--t))
+ *t = '\0';
+ return (t);
+}
+
+/*
+ * Drop a trailing inline `#' or unescaped `;' that rode along with the
+ * value (historic figpar behavior), then trim again. `ecomment' is set
+ * when the end-key scan stopped on an unquoted `#'.
+ */
+static char *
+bsdconf_trim_value_key(char *value, char *t, bool ecomment, bool bsemicolon)
+{
+ uint32_t x;
+
+ if (ecomment && t > value && *t == '#') {
+ *t = '\0';
+ return (bsdconf_rtrim_ws(value, t));
+ }
+ if (bsemicolon && t > value && *t == ';') {
+ for (x = 0; t - x > value && *(t - x - 1) == '\\'; x++)
+ ;
+ if ((x & 1) == 0) {
+ *t = '\0';
+ return (bsdconf_rtrim_ws(value, t));
+ }
+ }
+ return (t);
+}
+
+/*
+ * Invoke the unknown-directive call-back with a stack-local option that
+ * carries the statement's assignment operator (there is no matched
+ * options[] slot to hang it on). Returns the call-back's result.
+ */
+static int
+bsdconf_call_unknown(int (*unknown)(struct bsdconf_option *option,
+ uint32_t line, char *directive, char *value), enum bsdconf_op op,
+ uint32_t dline, char *directive, char *value)
+{
+ struct bsdconf_option unk;
+
+ memset(&unk, 0, sizeof(unk));
+ unk.op = op;
+ return (unknown(&unk, dline, directive, value));
+}
+
+/*
+ * Scan from the current byte in `*p' to the end of the value. Handles
+ * quotes, escaped end-keys, inline comments, and semicolon terminators.
+ * On return, `*p' holds the terminating key (or is at EOF), and `*r',
+ * `*line', `*comment', and `*ecomment' are updated. Returns 0 on
+ * success, or -1 on seek/read error (errno set).
+ */
+static int
+bsdconf_scan_value_end(int fd, char *p, ssize_t *r, uint32_t *line,
+ uint8_t *comment, uint8_t *ecomment, bool bsemicolon)
+{
+ uint8_t end = 0;
+ uint8_t quote = 0;
+ uint32_t n;
+ off_t charpos;
+
+ *ecomment = 0;
+ while (*r > 0 && end == 0) {
+ /* Advance to the next character if we know we can */
+ if (*p != '\"' && *p != '#' && *p != '\n' &&
+ (!bsemicolon || *p != ';')) {
+ *r = bsdconf_read1(fd, p);
+ if (*r < 0)
+ return (-1);
+ continue;
+ }
+
+ /*
+ * If we get this far, we've hit an end-key
+ */
+
+ /* Get the current offset */
+ if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ return (-1);
+ charpos--;
+
+ /*
+ * Go back so we can read the character before the key to
+ * check if the character is escaped (which means we should
+ * continue).
+ */
+ if (lseek(fd, -2, SEEK_CUR) == -1)
+ return (-1);
+ *r = bsdconf_read1(fd, p);
+ if (*r < 0)
+ return (-1);
+
+ /*
+ * Count how many backslashes there are (an odd number means
+ * the key is escaped, even means otherwise).
+ */
+ for (n = 1; *r > 0 && *p == '\\'; n++) {
+ /* Move back another offset to read */
+ if (lseek(fd, -2, SEEK_CUR) == -1)
+ return (-1);
+ *r = bsdconf_read1(fd, p);
+ if (*r < 0)
+ return (-1);
+ }
+
+ /* Move offset back to the key and read it */
+ if (lseek(fd, charpos, SEEK_SET) == -1)
+ return (-1);
+ *r = bsdconf_read1(fd, p);
+ if (*r < 0)
+ return (-1);
+
+ /*
+ * If an even number of backslashes was counted meaning key
+ * is not escaped, we should evaluate what to do.
+ */
+ if ((n & 1) == 1) {
+ switch (*p) {
+ case '\"':
+ /*
+ * Flag current sequence of characters to
+ * follow as being quoted (hashes are not
+ * considered comments).
+ */
+ quote = !quote;
+ break;
+ case '#':
+ /*
+ * If we aren't in a quoted series, we just
+ * hit an inline comment and have found the
+ * end of the value. Flag the remainder of
+ * the line as a comment so it is not
+ * mistaken for a new directive.
+ */
+ if (!quote) {
+ *ecomment = *comment = 1;
+ end = 1;
+ }
+ break;
+ case '\n':
+ /*
+ * Newline characters must always be escaped,
+ * whether inside a quoted series or not,
+ * otherwise they terminate the value.
+ */
+ (*line)++;
+ end = 1;
+ /* FALLTHROUGH */
+ case ';':
+ if (!quote && bsemicolon)
+ end = 1;
+ break;
+ }
+ } else if (*p == '\n')
+ /* Escaped newline character. increment */
+ (*line)++;
+
+ /* Advance to the next character */
+ *r = bsdconf_read1(fd, p);
*** 9545 LINES SKIPPED ***