Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F162105608
D58066.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
199 KB
Referenced Files
None
Subscribers
None
D58066.diff
View Options
diff --git a/lib/Makefile b/lib/Makefile
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -36,6 +36,7 @@
libarchive \
libbegemot \
libblocksruntime \
+ libbsdconf \
libbsddialog \
libbsdstat \
libbsm \
diff --git a/lib/libbsdconf/Makefile b/lib/libbsdconf/Makefile
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/Makefile
@@ -0,0 +1,30 @@
+PACKAGE= utilities
+LIB= bsdconf
+SHLIB_MAJOR= 1
+INCS= bsdconf.h
+MAN= bsdconf.3
+MLINKS= bsdconf.3 bsdconf_fparse.3 \
+ bsdconf.3 bsdconf_format_derive.3 \
+ bsdconf.3 bsdconf_format_files.3 \
+ bsdconf.3 bsdconf_format_files_free.3 \
+ bsdconf.3 bsdconf_format_find.3 \
+ bsdconf.3 bsdconf_format_guess.3 \
+ bsdconf.3 bsdconf_format_lookup.3 \
+ bsdconf.3 bsdconf_format_path.3 \
+ bsdconf.3 bsdconf_format_processing.3 \
+ bsdconf.3 bsdconf_format_put.3 \
+ bsdconf.3 bsdconf_get_option.3 \
+ bsdconf.3 bsdconf_parse.3 \
+ bsdconf.3 bsdconf_put.3 \
+ bsdconf.3 bsdconf_set_option.3 \
+ bsdconf.3 bsdconf_spool.3 \
+ bsdconf.3 bsdconf_unquote.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_src_env.c \
+ bsdconf_format_sysctl.c bsdconf_put.c bsdconf_string.c
+
+.include <bsd.lib.mk>
diff --git a/lib/libbsdconf/Makefile.depend b/lib/libbsdconf/Makefile.depend
new file mode 100644
--- /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.h b/lib/libbsdconf/bsdconf.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.h
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef _BSDCONF_H_
+#define _BSDCONF_H_
+
+#ifdef __FreeBSD__
+#include <sys/cdefs.h>
+#endif
+
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ * Supplant functionality missing on non-FreeBSD systems (musl libc, for
+ * example, provides no <sys/cdefs.h>). On glibc, <stdint.h> pulls in the
+ * definitions via <features.h>.
+ */
+#ifndef __BEGIN_DECLS
+#ifdef __cplusplus
+#define __BEGIN_DECLS extern "C" {
+#define __END_DECLS }
+#else
+#define __BEGIN_DECLS
+#define __END_DECLS
+#endif
+#endif
+
+/*
+ * Union for storing various types of data in a single common container.
+ *
+ * NB: When writing a value with bsdconf_put(), the caller supplies the value
+ * as a NUL-terminated string via the `str' member regardless of `type'; the
+ * type only governs how the value is rendered (see bsdconf_put() below).
+ */
+union bsdconf_value {
+ void *data; /* Pointer to NUL-terminated string */
+ 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 */
+ uint32_t boolean:1; /* Boolean integer value (0 or 1) */
+};
+
+/*
+ * Option types (based on above value union)
+ */
+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_RESERVED1 = 0x0100, /* reserved data type-1 */
+ BSDCONF_TYPE_RESERVED2 = 0x0200, /* reserved data type-2 */
+ BSDCONF_TYPE_RESERVED3 = 0x0400, /* reserved data type-3 */
+};
+
+/*
+ * Assignment operators. The default for a given file format is
+ * BSDCONF_OP_ASSIGN; the remainder require BSDCONF_OPERATOR_EQUALS (make(1)
+ * style configuration files such as make.conf(5)).
+ */
+enum bsdconf_op {
+ BSDCONF_OP_DEFAULT = 0, /* format's natural operator */
+ BSDCONF_OP_ASSIGN, /* `=' assign */
+ BSDCONF_OP_APPEND, /* `+=' append (make) */
+ BSDCONF_OP_COND, /* `?=' assign if undefined (make) */
+ BSDCONF_OP_EXPAND, /* `:=' assign with expansion (make) */
+ BSDCONF_OP_SHELL, /* `!=' assign shell output (make) */
+};
+
+/*
+ * Known configuration file formats (see bsdconf_format(3)). Formats numbered
+ * BSDCONF_FORMAT_USER and above are assigned by bsdconf_format_register().
+ */
+enum bsdconf_format {
+ BSDCONF_FORMAT_GENERIC = 0, /* quote values only when required */
+ BSDCONF_FORMAT_LOADER, /* loader.conf(5); always quoted */
+ BSDCONF_FORMAT_SYSCTL, /* sysctl.conf(5); quote when needed */
+ BSDCONF_FORMAT_MAKE, /* make.conf(5); `+=' et al. */
+ BSDCONF_FORMAT_SRC, /* src.conf(5); make(1) syntax */
+ BSDCONF_FORMAT_SRC_ENV, /* src-env.conf(5); make(1) syntax */
+ BSDCONF_FORMAT_USER = 32, /* first registered format */
+};
+
+/*
+ * A single entry in a format's ordered list of configuration sources. Most
+ * formats are backed by more than one file (read in a deterministic order
+ * with directives in later files overriding earlier ones) and some pull
+ * additional files from drop-in directories.
+ */
+enum bsdconf_source_type {
+ BSDCONF_SOURCE_FILE = 0, /* a single file */
+ BSDCONF_SOURCE_DIR, /* each `*.conf' in a directory */
+ BSDCONF_SOURCE_MODDIR, /* `<module>.conf' in a directory */
+};
+struct bsdconf_source {
+ enum bsdconf_source_type type; /* how to interpret path */
+ const char *path; /* file or directory path */
+};
+
+/*
+ * The format descriptor: a read-only table of properties characterizing a
+ * configuration file format (no relation to file descriptors). The built-in
+ * formats (above) are described by format descriptors of this same shape; a
+ * new format is "bolted on" by registering a format descriptor of its own --
+ * typically derived from the built-in it most resembles (see
+ * bsdconf_format_derive() below) with only the differing members adjusted.
+ *
+ * A format whose backing files are themselves configuration data -- the
+ * boot loader reads only /boot/defaults/loader.conf and discovers every
+ * other file from the loader_conf_files, loader_conf_dirs, and
+ * local_loader_conf_files directives it finds along the way -- describes
+ * that machinery with the `defaults' member quintet below, and
+ * bsdconf_format_files() performs the same discovery the consumer does.
+ * The static `sources' list remains as the fallback for systems whose
+ * defaults file is missing.
+ */
+struct bsdconf_format_def {
+ const char *keyword; /* target keyword (or NULL) */
+ const char *path; /* default write path (or NULL) */
+ const struct bsdconf_source
+ *sources; /* ordered sources; NULL-path
+ terminated (or NULL if `path'
+ is the only source) */
+ const char *defaults; /* defaults file (or NULL) */
+ const char *defaults_env; /* environment variable overriding
+ the defaults file (or NULL) */
+ const char *files_directive; /* directive listing conf files */
+ const char *dirs_directive; /* directive listing drop-in dirs */
+ const char *local_directive; /* directive listing local files */
+ uint16_t processing; /* processing_options bitmask */
+ uint16_t put; /* put_options bitmask */
+};
+
+/*
+ * Options to bsdconf_parse() and bsdconf_put() for processing_options bitmask
+ */
+#define BSDCONF_BREAK_ON_EQUALS 0x0001 /* stop reading directive at `=' */
+#define BSDCONF_BREAK_ON_SEMICOLON 0x0002 /* `;' starts a new line */
+#define BSDCONF_CASE_SENSITIVE 0x0004 /* directives are case sensitive */
+#define BSDCONF_REQUIRE_EQUALS 0x0008 /* assignment directives only */
+#define BSDCONF_STRICT_EQUALS 0x0010 /* `=' must be part of directive */
+#define BSDCONF_OPERATOR_EQUALS 0x0020 /* recognize `+=' `?=' `:=' `!=' */
+
+/*
+ * Options to bsdconf_put() for put_options bitmask
+ */
+#define BSDCONF_PUT_NO_DUPLICATES 0x0001 /* error if directive found twice */
+#define BSDCONF_PUT_ALLOW_EMPTY 0x0002 /* allow SET_VALUE of empty value */
+#define BSDCONF_PUT_BACKUP 0x0004 /* back up config file (`.bak') */
+#define BSDCONF_PUT_UNQUOTED 0x0008 /* never quote values on output */
+#define BSDCONF_PUT_QUOTE_ALWAYS 0x0010 /* always quote values on output */
+
+/*
+ * Per-directive actions for bsdconf_put()
+ */
+#define BSDCONF_ACTION_SET_VALUE 0x0000 /* set/replace value (default) */
+#define BSDCONF_ACTION_CHECK 0x0001 /* compare against current value */
+#define BSDCONF_ACTION_REMOVE 0x0002 /* remove directive from config */
+
+/*
+ * Per-directive result codes set by bsdconf_put()
+ */
+#define BSDCONF_DIRECTIVE_FOUND 0x0001 /* vs not found (see added) */
+#define BSDCONF_VALUE_CHANGED 0x0002 /* vs no change required */
+#define BSDCONF_DIRECTIVE_ADDED 0x0004 /* vs already existed */
+#define BSDCONF_DIRECTIVE_REMOVED 0x0008 /* vs not found */
+
+/*
+ * Anatomy of a config file option; used for both reading and writing.
+ *
+ * When parsing with bsdconf_parse(), `directive' is an fnmatch(3) pattern and
+ * `parse' is invoked for each matching statement (with `op' set to the
+ * statement's assignment operator beforehand).
+ *
+ * When writing with bsdconf_put(), `directive' is an exact token (a pattern
+ * is not a writable target), `action' selects the operation, and `result'
+ * and `line' report what was done.
+ */
+struct bsdconf_option {
+ enum bsdconf_type type; /* Option value type */
+ const char *directive; /* config file keyword */
+ union bsdconf_value value; /* NB: set by parse action;
+ value to write for put */
+ enum bsdconf_op op; /* assignment operator */
+ uint8_t action; /* bsdconf_put() action */
+ uint16_t result; /* NB: set by bsdconf_put() */
+ uint32_t line; /* NB: set by bsdconf_put() */
+
+ /*
+ * Function pointer; action to be taken when the directive is found
+ * by bsdconf_parse(). Non-zero return aborts the parse (and is
+ * propagated to the bsdconf_parse() caller).
+ */
+ int (*parse)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value);
+};
+extern struct bsdconf_option bsdconf_dummy_option;
+
+/*
+ * Function prototypes
+ *
+ * All functions returning int return zero on success (except
+ * bsdconf_spool(), which returns a new file descriptor); otherwise -1 (or
+ * the non-zero result of a parse call-back) and errno should be consulted.
+ */
+__BEGIN_DECLS
+int bsdconf_parse(struct bsdconf_option _options[],
+ const char *_path,
+ int (*_unknown)(struct bsdconf_option *_option,
+ uint32_t _line, char *_directive, char *_value),
+ uint16_t _processing_options);
+int bsdconf_fparse(struct bsdconf_option _options[],
+ int _fd,
+ int (*_unknown)(struct bsdconf_option *_option,
+ uint32_t _line, char *_directive, char *_value),
+ uint16_t _processing_options);
+int bsdconf_spool(int _fd);
+struct bsdconf_option *bsdconf_get_option(struct bsdconf_option _options[],
+ const char *_directive);
+char *bsdconf_unquote(char *_value);
+int bsdconf_set_option(struct bsdconf_option _options[],
+ const char *_directive,
+ union bsdconf_value *_value);
+int bsdconf_put(struct bsdconf_option _options[],
+ const char *_path, uint16_t _processing_options,
+ uint16_t _put_options);
+
+/*
+ * Format abstraction layer (see bsdconf_format(3))
+ */
+int bsdconf_format_derive(enum bsdconf_format _base,
+ struct bsdconf_format_def *_def);
+int bsdconf_format_files(enum bsdconf_format _format,
+ const char *_rootdir, const char *_module,
+ const char *_defaults, char ***_filesp,
+ size_t *_nfilesp, size_t *_write_idxp);
+void bsdconf_format_files_free(char **_files,
+ size_t _nfiles);
+int bsdconf_format_find(const char *_keyword,
+ enum bsdconf_format *_format);
+enum bsdconf_format bsdconf_format_guess(const char *_path);
+const struct bsdconf_format_def
+ *bsdconf_format_lookup(enum bsdconf_format _format);
+const char *bsdconf_format_path(enum bsdconf_format _format);
+uint16_t bsdconf_format_processing(
+ enum bsdconf_format _format);
+uint16_t bsdconf_format_put(enum bsdconf_format _format);
+int bsdconf_format_register(
+ const struct bsdconf_format_def *_def,
+ enum bsdconf_format *_format);
+__END_DECLS
+
+#endif /* !_BSDCONF_H_ */
diff --git a/lib/libbsdconf/bsdconf.3 b/lib/libbsdconf/bsdconf.3
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.3
@@ -0,0 +1,1093 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd July 6, 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 ,
+.Nm bsdconf_put ,
+.Nm bsdconf_set_option ,
+.Nm bsdconf_format_derive ,
+.Nm bsdconf_format_files ,
+.Nm bsdconf_format_files_free ,
+.Nm bsdconf_format_find ,
+.Nm bsdconf_format_guess ,
+.Nm bsdconf_format_lookup ,
+.Nm bsdconf_format_path ,
+.Nm bsdconf_format_processing ,
+.Nm bsdconf_format_put ,
+.Nm bsdconf_format_register
+.Nd configuration file reading and writing 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
+.Ft int
+.Fo bsdconf_put
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *path"
+.Fa "uint16_t processing_options"
+.Fa "uint16_t put_options"
+.Fc
+.Ft int
+.Fo bsdconf_set_option
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *directive"
+.Fa "union bsdconf_value *value"
+.Fc
+.Ft int
+.Fo bsdconf_format_derive
+.Fa "enum bsdconf_format base"
+.Fa "struct bsdconf_format_def *def"
+.Fc
+.Ft int
+.Fo bsdconf_format_files
+.Fa "enum bsdconf_format format"
+.Fa "const char *rootdir"
+.Fa "const char *module"
+.Fa "const char *defaults"
+.Fa "char ***filesp"
+.Fa "size_t *nfilesp"
+.Fa "size_t *write_idxp"
+.Fc
+.Ft void
+.Fo bsdconf_format_files_free
+.Fa "char **files"
+.Fa "size_t nfiles"
+.Fc
+.Ft int
+.Fo bsdconf_format_find
+.Fa "const char *keyword"
+.Fa "enum bsdconf_format *format"
+.Fc
+.Ft "enum bsdconf_format"
+.Fo bsdconf_format_guess
+.Fa "const char *path"
+.Fc
+.Ft "const struct bsdconf_format_def *"
+.Fo bsdconf_format_lookup
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft "const char *"
+.Fo bsdconf_format_path
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft uint16_t
+.Fo bsdconf_format_processing
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft uint16_t
+.Fo bsdconf_format_put
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft int
+.Fo bsdconf_format_register
+.Fa "const struct bsdconf_format_def *def"
+.Fa "enum bsdconf_format *format"
+.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,
+crash-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.
+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 Sx FORMATS
+naming a format's target keyword,
+backing files,
+and tokenizing/quoting rules,
+which parameterize a single shared engine.
+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.
+.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() */
+
+ /* 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_RESERVED1 = 0x0100, /* reserved data type-1 */
+ BSDCONF_TYPE_RESERVED2 = 0x0200, /* reserved data type-2 */
+ BSDCONF_TYPE_RESERVED3 = 0x0400, /* reserved data type-3 */
+};
+
+union bsdconf_value {
+ void *data; /* Pointer to NUL-terminated string */
+ 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 */
+ uint32_t boolean:1; /* Boolean integer value (0 or 1) */
+};
+.Ed
+.Pp
+The
+.Fa processing_options
+argument to
+.Fn bsdconf_parse ,
+.Fn bsdconf_fparse ,
+and
+.Fn bsdconf_put
+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 .
+.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 physical 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.
+.Fn bsdconf_put
+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 if no match a pointer to a static dummy struct is returned
+.Pq whose values are all zero or NULL .
+.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 WRITING
+.Fn bsdconf_put
+rewrites the configuration file at
+.Fa path ,
+applying the per-directive action of each option in the array:
+.Bl -tag -width BSDCONF_ACTION_SET_VALUE
+.It Dv BSDCONF_ACTION_SET_VALUE
+Set the directive to
+.Va value.str ,
+editing it in place if present or appending it to the file if absent.
+.It Dv BSDCONF_ACTION_CHECK
+Report
+.Pq via Va result
+whether the current value differs from
+.Va value.str ,
+without modifying the file.
+.It Dv BSDCONF_ACTION_REMOVE
+Delete the directive from the file.
+.El
+.Pp
+Unlike
+.Fn bsdconf_parse ,
+directives are matched exactly
+.Pq a pattern is not a writable target .
+Comments,
+blank lines,
+statement ordering,
+and the formatting of untouched statements are preserved.
+For each processed option,
+.Va result
+is set to a bitmask of
+.Dv BSDCONF_DIRECTIVE_FOUND ,
+.Dv BSDCONF_VALUE_CHANGED ,
+.Dv BSDCONF_DIRECTIVE_ADDED ,
+and
+.Dv BSDCONF_DIRECTIVE_REMOVED ,
+and
+.Va line
+is set to the line of the first match
+.Pq if found .
+.Pp
+.Fa path
+must name a regular file
+.Pq only a regular file can be atomically replaced ;
+anything else is rejected with
+.Er EINVAL .
+The file is replaced atomically:
+output is streamed to a temporary file created with
+.Xr mkstemp 3
+in the target's own directory,
+flushed to stable storage with
+.Xr fsync 2 ,
+given the mode
+.Pq and, if permitted, the ownership
+of the original,
+and then moved over the original with
+.Xr rename 2 .
+An unexpected system failure or power loss mid-transaction leaves the
+original untouched
+.Pq see also Sx SECURITY CONSIDERATIONS .
+.Pp
+.Fn bsdconf_put
+operates on exactly one file.
+For a format backed by more than one file
+.Pq see Sx FORMATS ,
+the caller decides which file to hand it,
+and the deterministic sourcing order makes that decision mechanical:
+because the last file to list a directive dictates its boot-time value,
+a directive should be rewritten in the last file of the format's ordered
+list that currently lists it
+.Pq found by parsing the files in order with Fn bsdconf_parse ,
+or appended to the format's default file when no file lists it.
+A removal,
+by contrast,
+must be applied to every file listing the directive,
+lest deleting the authoritative definition merely unmask an earlier one.
+This is the policy implemented by
+.Xr sysconf 8 .
+.Pp
+The
+.Fa put_options
+argument is a mask of the following bit fields:
+.Bl -tag -width BSDCONF_PUT_NO_DUPLICATES
+.It Dv BSDCONF_PUT_NO_DUPLICATES
+Fail with
+.Er EEXIST
+if a directive to be written appears more than once in the file.
+.It Dv BSDCONF_PUT_ALLOW_EMPTY
+Permit setting an empty value.
+.It Dv BSDCONF_PUT_BACKUP
+Save a copy of the original file with a
+.Ql .bak
+suffix before replacing it.
+.It Dv BSDCONF_PUT_UNQUOTED
+Emit values verbatim,
+never quoted
+.Pq embedded whitespace runs to the end of the line .
+A value that could not round-trip verbatim
+.Pq an embedded newline or comment marker
+is rejected with
+.Er EINVAL
+rather than silently corrupting the file.
+.It Dv BSDCONF_PUT_QUOTE_ALWAYS
+Enclose every value in double-quotes,
+escaping embedded quotes and backslashes.
+.El
+.Pp
+.Fn bsdconf_set_option
+traverses the options-array and stages
+.Fa value
+into the option whose directive matches
+.Fa directive
+via
+.Xr strcmp 3 .
+.Sh FORMATS
+Every supported configuration file format is described by the same small
+format descriptor
+.Pq introduced in Sx DESCRIPTION :
+.Bd -literal -offset indent
+struct bsdconf_format_def {
+ const char *keyword; /* target keyword (or NULL) */
+ const char *path; /* default write path (or NULL) */
+ const struct bsdconf_source
+ *sources; /* ordered sources (or NULL) */
+ const char *defaults; /* defaults file (or NULL) */
+ const char *defaults_env; /* environment override */
+ const char *files_directive; /* directive listing conf files */
+ const char *dirs_directive; /* directive listing drop-in dirs */
+ const char *local_directive; /* directive listing local files */
+ uint16_t processing; /* processing_options bitmask */
+ uint16_t put; /* put_options bitmask */
+};
+
+enum bsdconf_source_type {
+ BSDCONF_SOURCE_FILE = 0, /* a single file */
+ BSDCONF_SOURCE_DIR, /* each `*.conf' in a directory */
+ BSDCONF_SOURCE_MODDIR, /* `<module>.conf' in a directory */
+};
+
+struct bsdconf_source {
+ enum bsdconf_source_type type; /* how to interpret path */
+ const char *path; /* file or directory path */
+};
+.Ed
+.Pp
+There is exactly one parsing and writing engine;
+a format descriptor merely parameterizes it.
+The built-in formats,
+with their target keywords and default write paths,
+are:
+.Bl -column "BSDCONF_FORMAT_GENERIC" "keyword" "/boot/loader.conf"
+.It Sy format Ta Sy keyword Ta Sy "default write path"
+.It Dv BSDCONF_FORMAT_GENERIC Ta generic Ta \&-
+.It Dv BSDCONF_FORMAT_LOADER Ta loader Ta /boot/loader.conf
+.It Dv BSDCONF_FORMAT_SYSCTL Ta sysctl Ta /etc/sysctl.conf
+.It Dv BSDCONF_FORMAT_MAKE Ta make Ta /etc/make.conf
+.It Dv BSDCONF_FORMAT_SRC Ta src Ta /etc/src.conf
+.It Dv BSDCONF_FORMAT_SRC_ENV Ta src-env Ta /etc/src-env.conf
+.El
+.Pp
+The default write path
+.Pq the Va path member
+answers only the question of where a
+.Em new
+directive lands:
+the file to which a directive not yet present anywhere is appended.
+Which files are
+.Em consulted
+is a separate and potentially broader question,
+answered by the
+.Va sources
+member.
+When
+.Va sources
+is NULL
+.Po
+.Dv BSDCONF_FORMAT_MAKE ,
+.Dv BSDCONF_FORMAT_SRC ,
+and
+.Dv BSDCONF_FORMAT_SRC_ENV
+.Pc
+the default write path is the format's one and only file and the two
+questions collapse into one.
+Otherwise
+.Po
+.Dv BSDCONF_FORMAT_LOADER
+and
+.Dv BSDCONF_FORMAT_SYSCTL
+.Pc
+the format is backed by several files and
+.Va sources
+lists them:
+an ordered,
+NULL-path terminated array in which each entry contributes a single file
+.Pq Dv BSDCONF_SOURCE_FILE ,
+every
+.Ql *.conf
+found in a drop-in directory
+.Pq Dv BSDCONF_SOURCE_DIR ,
+or the file
+.Ql <module>.conf
+in a directory keyed by kernel module name
+.Pq Dv BSDCONF_SOURCE_MODDIR .
+The order matches the order in which the system sources the files at boot,
+so a directive in a later file overrides the same directive in an earlier
+one and the last file listing a directive is the authoritative source of
+its value.
+The built-in source lists are:
+.Bl -tag -width BSDCONF_FORMAT_LOADER -offset indent
+.It Dv BSDCONF_FORMAT_LOADER
+discovered from
+.Pa /boot/defaults/loader.conf
+.Pq see below ;
+on a stock system
+.Pa /boot/device.hints ,
+.Pa /boot/loader.conf ,
+each
+.Pa *.conf
+in
+.Pa /boot/loader.conf.d ,
+then
+.Pa /boot/loader.conf.local
+.Pq see Xr loader.conf 5 ;
+.It Dv BSDCONF_FORMAT_SYSCTL
+.Pa /etc/sysctl.conf ,
+.Pa /etc/sysctl.conf.local ,
+then
+.Pa /etc/sysctl.kld.d/<module>.conf
+.Pq see Xr sysctl.conf 5 ;
+.It Dv BSDCONF_FORMAT_MAKE
+.Pa /etc/make.conf
+alone;
+.It Dv BSDCONF_FORMAT_SRC , Dv BSDCONF_FORMAT_SRC_ENV
+.Pa /etc/src.conf
+and
+.Pa /etc/src-env.conf ,
+respectively,
+each alone
+.Pq see Xr src.conf 5 .
+.El
+.Pp
+A format whose backing files are themselves configuration data describes
+that machinery with the descriptor's
+.Va defaults
+member quintet rather than a static list alone.
+The boot loader hardcodes only
+.Pa /boot/defaults/loader.conf
+and discovers every other file from the
+.Va loader_conf_files ,
+.Va loader_conf_dirs ,
+and
+.Va local_loader_conf_files
+directives it encounters along the way
+.Pq any discovered file may revise those lists in turn ,
+and
+.Dv BSDCONF_FORMAT_LOADER
+names that defaults file and those three directives so that
+.Fn bsdconf_format_files
+performs the same discovery the loader does:
+the defaults file is read,
+the file-list directive is chased
+.Pq re-read after every file, since any file may revise it ,
+and the drop-in directories and local files named by the final lists are
+appended.
+The defaults file itself is deliberately excluded from the resolved list,
+mirroring how
+.Xr sysrc 8
+excludes
+.Pa /etc/defaults/rc.conf ;
+the static
+.Va sources
+list serves as the fallback when the defaults file is missing.
+.Pp
+.Fn bsdconf_format_files
+resolves the backing files of
+.Fa format
+into a newly allocated array of paths stored through
+.Fa filesp
+.Pq with the count stored through Fa nfilesp ,
+each prefixed with
+.Fa rootdir
+unless NULL or empty.
+When
+.Fa defaults
+is non-NULL,
+it overrides the descriptor's defaults file for discovery and is used
+verbatim
+.Po
+not prefixed with
+.Fa rootdir ;
+it is ignored for formats without one
+.Pc .
+When
+.Fa write_idxp
+is non-NULL,
+the index of the file recommended for directives found in no file at all
+is stored through it:
+the format's default write path,
+or the last regular
+.Pq non-drop-in, non-local
+configuration file when discovery is in play.
+Sources of type
+.Dv BSDCONF_SOURCE_FILE
+.Pq and files named by a file-list directive
+are always listed,
+whether or not the file exists;
+directory sources contribute only the entries present on disk
+.Pq sorted ;
+module sources contribute
+.Ql <module>.conf
+only when
+.Fa module
+is non-NULL.
+The caller releases the result with
+.Fn bsdconf_format_files_free .
+.Pp
+The formats also differ in quoting on output,
+each honoring the full syntax its consumer accepts:
+.Xr loader.conf 5
+values are always written quoted
+.Pq quoted or unquoted input is accepted when parsing
+and no whitespace is permitted around the equals sign,
+as demanded by the boot loader's reader;
+.Xr sysctl.conf 5
+follows the file parser of
+.Xr sysctl 8 ,
+which trims whitespace around the equals sign and strips one pair of quotes
+around the value,
+so values are written unquoted unless quoting is required
+.Pq embedded whitespace or a comment character ;
+.Pa make.conf
+follows
+.Xr make 1
+syntax where values are never quoted
+.Pq the value runs to the end of the line
+and assignment modifiers such as
+.Ql +=
+are recognized.
+.Xr src.conf 5
+and
+.Pa src-env.conf
+share the
+.Pa make.conf
+syntax exactly
+.Pq all three are read by Xr make 1 itself ,
+including the empty value,
+which for these two files is the idiom for the value-less
+.Ql WITH_*
+and
+.Ql WITHOUT_*
+build knobs.
+.Pp
+.Fn bsdconf_format_find
+maps a target keyword
+.Pq e.g., Dq loader
+to its format.
+.Fn bsdconf_format_guess
+guesses the format of an arbitrary file from the basename of
+.Fa path ,
+matching
+.Ql <keyword>.conf
+with an optional trailing suffix
+.Pq e.g., Pa /etc/sysctl.conf.local .
+The guess is advisory and offered for consumers that opt into it
+.Pq an interactive picker suggesting a default, for example ;
+where a wrong format silently misformats a file,
+as when writing,
+the caller should require the format to be stated explicitly instead,
+as
+.Xr sysconf 8
+does with its
+.Ar target
+keyword.
+.Fn bsdconf_format_lookup
+returns the format descriptor for a format handle.
+.Fn bsdconf_format_path ,
+.Fn bsdconf_format_processing ,
+and
+.Fn bsdconf_format_put
+return the default file path and the two option bitmasks,
+respectively.
+.Sh ADDING FORMATS
+Before adding a format,
+consider whether one is needed at all:
+the parser hands each statement's raw directive and value to the caller's
+callbacks and imposes no semantics of its own,
+so a file whose directives mean something unusual
+.Po
+cumulative directives that legitimately repeat,
+for example,
+which a
+.Fn parse
+callback accumulates rather than overwrites;
+see
+.Sx EXAMPLES
+.Pc
+is read with the stock engine and bespoke callbacks;
+no format descriptor,
+flag,
+or engine change is required.
+A format descriptor parameterizes only tokenization and writing;
+reach for one when a file needs a
+.Xr sysconf 8
+target keyword,
+a source list,
+or distinct quoting on output.
+Rewriting files built from cumulative directives,
+where a change is additive rather than a replacement,
+is a separate and as yet unsolved problem
+.Pq see Sx BUGS .
+.Pp
+Beyond callbacks,
+the format descriptor is the entire definition of a format;
+no format has private parsing or writing code.
+Support for a new configuration file format is therefore a data problem,
+approached one of two ways.
+.Pp
+An application whose format is mostly like an existing one bolts it on at
+run time without duplicating any logic
+.Pq see Sx EXAMPLES :
+.Fn bsdconf_format_derive
+copies the format descriptor of the nearest
+.Fa base
+format into
+.Fa def ,
+the caller adjusts only the members that differ
+.Pq typically the keyword, the paths, and one or two bitmask flags ,
+and
+.Fn bsdconf_format_register
+registers the result and returns a new format handle through
+.Fa format .
+The format descriptor is copied by value but the strings and arrays it
+references
+.Po
+.Va keyword ,
+.Va path ,
+.Va sources ,
+and the
+.Va defaults
+member quintet
+.Pc
+are not;
+they must remain valid for the life of the registration.
+Registered formats participate in keyword and basename resolution exactly
+like built-ins,
+including target resolution in
+.Xr sysconf 8 Ns -style
+consumers.
+Registration is intended to occur during program initialization and is not
+thread-safe.
+.Pp
+Within the library,
+each built-in format is a self-contained translation unit
+.Pa ( bsdconf_format_<keyword>.c )
+that documents the format's syntax rules,
+cites the authority for them
+.Pq the consumer whose reader defines what is legal ,
+and defines its format descriptor,
+including its ordered source list.
+Promoting a format into the library therefore touches no existing parsing
+or writing code:
+a new file of the same shape is dropped in,
+its descriptor is declared alongside its siblings,
+one
+.Dv BSDCONF_FORMAT_*
+constant is appended to
+.Vt enum bsdconf_format ,
+one pointer is appended to the registry table,
+the file is listed in the Makefile,
+and this manual's
+.Sx FORMATS
+section grows one row and one quoting note.
+The keyword becomes a
+.Xr sysconf 8
+target automatically.
+.Pp
+Many formats need no new engine capability at all.
+Formats whose statements are a space-separated directive and value with no
+equals sign
+.Pq the Apache-style directive files the ancestral parser was raised on
+parse today:
+with
+.Dv BSDCONF_BREAK_ON_EQUALS
+omitted,
+the first whitespace-delimited token is the directive and the remainder of
+the line is the raw value
+.Pq which a Fn parse callback may split further by its own rules ,
+and directive matching is case insensitive unless
+.Dv BSDCONF_CASE_SENSITIVE
+is set.
+Likewise
+.Dv BSDCONF_BREAK_ON_SEMICOLON
+already covers formats that terminate or chain statements with a
+semicolon.
+.Pp
+A candidate format whose syntax the existing option flags cannot express
+.Pq brace-grouped statement blocks being the canonical example
+is handled by teaching the shared engine
+.Pq the scanners in the parsing and writing cores
+one new
+.Va processing
+or
+.Va put
+flag that the new format's descriptor is the first to set.
+The capability lands once,
+composes with every existing flag,
+and becomes available to every other format
+.Pq built-in, derived, or registered
+rather than living in a private parser;
+the new format itself remains nothing more than a format descriptor in its
+own translation unit.
+.Sh RETURN VALUES
+.Fn bsdconf_parse ,
+.Fn bsdconf_fparse ,
+and
+.Fn bsdconf_put
+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_set_option
+returns 1 if a matching directive was staged,
+otherwise 0.
+.Fn bsdconf_format_derive
+and
+.Fn bsdconf_format_register
+return zero on success;
+otherwise -1 with
+.Va errno
+set to
+.Er EINVAL
+or
+.Er ENOSPC .
+.Fn bsdconf_format_find
+returns zero on success;
+otherwise -1.
+.Fn bsdconf_format_files
+returns zero on success;
+otherwise -1 with
+.Va errno
+set to indicate the error
+.Pq Er EINVAL for a format with no backing files .
+.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
+.Pp
+Bolt on a new format at run time by deriving from the built-in it most
+resembles,
+then set a directive in its file with full crash-resilient write semantics:
+.Bd -literal -offset indent
+struct bsdconf_format_def def;
+enum bsdconf_format myfmt;
+struct bsdconf_option set[] = {
+ { .type = BSDCONF_TYPE_STR, .directive = "loglevel",
+ .value = { .str = "debug" },
+ .action = BSDCONF_ACTION_SET_VALUE },
+ { .directive = NULL }
+};
+
+if (bsdconf_format_derive(BSDCONF_FORMAT_SYSCTL, &def) != 0)
+ err(1, "bsdconf_format_derive");
+def.keyword = "myapp";
+def.path = "/usr/local/etc/myapp.conf";
+def.sources = NULL; /* one file; path is the sole source */
+if (bsdconf_format_register(&def, &myfmt) != 0)
+ err(1, "bsdconf_format_register");
+
+if (bsdconf_put(set, bsdconf_format_path(myfmt),
+ bsdconf_format_processing(myfmt),
+ bsdconf_format_put(myfmt)) != 0)
+ err(1, "%s", bsdconf_format_path(myfmt));
+.Ed
+.Pp
+Promoting such a format into the library itself
+.Pq a compiled-in sibling of the built-ins
+is the same data expressed as a translation unit;
+see
+.Sx ADDING FORMATS .
+.Sh SEE ALSO
+.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@kfv.io
+.Sh BUGS
+This is the first implementation of the library,
+and the interface may be subject to refinement.
+.Pp
+.Fn bsdconf_put
+models directives as single-valued
+.Pq the last assignment dictates the value ,
+which suits every built-in format.
+Formats whose directives are cumulative
+.Pq legitimately repeating, each occurrence adding to the effect
+parse cleanly with caller-supplied callbacks,
+but no writing primitives for additive semantics
+.Pq insert, remove, or reorder individual occurrences
+are provided yet.
+.Sh SECURITY CONSIDERATIONS
+The write transaction is designed to be safe against both interruption and
+interference.
+The temporary file is created by
+.Xr mkstemp 3
+.Pq Dv O_EXCL ; never predictable, never followed
+in the target's own directory,
+so the data never crosses a filesystem boundary and never transits a
+world-writable directory such as
+.Pa /tmp .
+The mode and ownership propagated onto it are taken by
+.Xr fstat 2
+from the descriptor actually read,
+not by re-looking up the path,
+and are applied with
+.Xr fchmod 2
+and
+.Xr fchown 2
+on the open descriptor,
+so a concurrently swapped file cannot influence them.
+A backup requested with
+.Dv BSDCONF_PUT_BACKUP
+refuses to follow a symbolic link planted at the
+.Ql .bak
+name
+.Pq Dv O_NOFOLLOW .
+.Pp
+Symbolic links in
+.Fa path
+are resolved
+.Pq with Xr realpath 3
+before work begins:
+writing through a symbolic link rewrites the file it points at and
+preserves the link itself,
+rather than replacing the link with a regular file.
+The resolution is not re-verified at
+.Xr open 2
+time;
+as with any path-based interface,
+an actor with write access to a directory along the path can redirect it,
+so the containing directories must be trustworthy
+.Pq as those of system configuration files are .
+Because replacement is by
+.Xr rename 2 ,
+a target with multiple hard links is severed from its other names,
+which afterwards continue to reference the old content;
+this is inherent to atomic replacement.
+.Pp
+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.
diff --git a/lib/libbsdconf/bsdconf.c b/lib/libbsdconf/bsdconf.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.c
@@ -0,0 +1,611 @@
+/*
+ * Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+struct bsdconf_option bsdconf_dummy_option;
+
+/*
+ * 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, a pointer to the static dummy option (above) 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;
+
+ /* Check arguments */
+ if (options == NULL || directive == NULL)
+ return (&bsdconf_dummy_option);
+
+ /* Loop through the array, return the first match */
+ for (n = 0; options[n].directive != NULL; n++)
+ if (strcmp(options[n].directive, directive) == 0)
+ return (&(options[n]));
+
+ /* Re-initialize the dummy option in case it was written to */
+ memset(&bsdconf_dummy_option, 0, sizeof(bsdconf_dummy_option));
+
+ return (&bsdconf_dummy_option);
+}
+
+/*
+ * 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);
+}
+
+/*
+ * Parse the configuration data on the open file descriptor `fd' and execute
+ * the `parse' call-back functions for any directives defined by the array of
+ * config options (first argument).
+ *
+ * For unknown directives that are encountered, you can optionally pass a
+ * call-back function for the third argument to be called for unknowns.
+ *
+ * The scanner requires a seekable descriptor; input that cannot seek (a
+ * pipe or socket, standard input included) is detected up front and spooled
+ * through bsdconf_spool() above, parsed from the temporary, and costs one
+ * transient copy of the data. The descriptor is left positioned at
+ * end-of-file (non-seekable input is left drained) and remains open (the
+ * caller retains ownership).
+ *
+ * Returns zero on success; otherwise returns -1 (or the non-zero result of a
+ * call-back) and errno should be consulted.
+ */
+int
+bsdconf_fparse(struct bsdconf_option options[], int fd,
+ int (*unknown)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value), uint16_t processing_options)
+{
+ uint8_t bequals;
+ uint8_t bsemicolon;
+ uint8_t case_sensitive;
+ uint8_t comment = 0;
+ uint8_t ecomment;
+ uint8_t end;
+ uint8_t found;
+ uint8_t have_equals = 0;
+ uint8_t operator_equals;
+ uint8_t quote;
+ uint8_t require_equals;
+ uint8_t strict_equals;
+ char p[2];
+ char *directive = NULL;
+ char *t;
+ char *value = NULL;
+ enum bsdconf_op op;
+ int error;
+ int rv = 0;
+ int spoolfd = -1;
+ ssize_t r = 1;
+ uint32_t dline;
+ uint32_t dsize = 0;
+ uint32_t line = 1;
+ uint32_t n;
+ uint32_t vsize = 0;
+ uint32_t x;
+ off_t charpos;
+ off_t curpos;
+
+ /* Sanity check: if no options and no unknown function, return */
+ if (options == NULL && unknown == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Spool input that cannot seek (see bsdconf_spool() above) */
+ if (lseek(fd, 0, SEEK_CUR) == -1) {
+ if (errno != ESPIPE)
+ return (-1);
+ if ((spoolfd = bsdconf_spool(fd)) == -1)
+ return (-1);
+ fd = spoolfd;
+ }
+
+ /* Processing options */
+ bequals = (processing_options & BSDCONF_BREAK_ON_EQUALS) == 0 ? 0 : 1;
+ bsemicolon =
+ (processing_options & BSDCONF_BREAK_ON_SEMICOLON) == 0 ? 0 : 1;
+ case_sensitive =
+ (processing_options & BSDCONF_CASE_SENSITIVE) == 0 ? 0 : 1;
+ operator_equals =
+ (processing_options & BSDCONF_OPERATOR_EQUALS) == 0 ? 0 : 1;
+ require_equals =
+ (processing_options & BSDCONF_REQUIRE_EQUALS) == 0 ? 0 : 1;
+ strict_equals =
+ (processing_options & BSDCONF_STRICT_EQUALS) == 0 ? 0 : 1;
+
+ /* Read the file until EOF */
+ while (r != 0) {
+ r = read(fd, p, 1);
+
+ /* Skip to the beginning of a directive */
+ while (r != 0 && (isspace(*p) || *p == '#' || comment ||
+ (bsemicolon && *p == ';'))) {
+ if (*p == '#')
+ comment = 1;
+ else if (*p == '\n') {
+ comment = 0;
+ line++;
+ }
+ r = read(fd, p, 1);
+ }
+ /* Test for EOF; if EOF then no directive was found */
+ if (r == 0)
+ goto cleanup;
+
+ /* Record the line number the directive appears on */
+ dline = line;
+
+ /* Get the current offset */
+ if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ goto fail;
+ curpos--;
+
+ /* Find the length of the directive */
+ for (n = 0; r != 0; n++) {
+ if (isspace(*p))
+ break;
+ if (bequals && *p == '=') {
+ have_equals = 1;
+ break;
+ }
+ if (bsemicolon && *p == ';')
+ break;
+ r = read(fd, p, 1);
+ }
+
+ /* Test for EOF, if EOF then no directive was found */
+ if (n == 0 && r == 0)
+ goto cleanup;
+
+ /* Go back to the beginning of the directive */
+ if (lseek(fd, curpos, SEEK_SET) == -1)
+ goto fail;
+
+ /*
+ * Allocate and read the directive into memory. The buffer
+ * must be grown on the first pass (directive == NULL) even
+ * when the name is empty (a line beginning with `='), lest
+ * the string terminator below store through a NULL pointer.
+ */
+ if (directive == NULL || n > dsize) {
+ if ((t = realloc(directive, n + 1)) == NULL)
+ goto fail;
+ directive = t;
+ dsize = n;
+ }
+ r = read(fd, directive, n);
+
+ /* Advance beyond the equals sign if appropriate/desired */
+ if (bequals && *p == '=') {
+ if (lseek(fd, 1, SEEK_CUR) != -1)
+ r = read(fd, p, 1);
+ if (strict_equals && isspace(*p))
+ *p = '\n';
+ }
+
+ /* Terminate the string */
+ directive[n] = '\0';
+
+ /*
+ * Split a make(1)-style operator (`+=' `?=' `:=' `!=') off
+ * the tail of the directive if requested. The operator
+ * character rode along with the directive because only the
+ * `=' terminates the directive scan (above).
+ */
+ op = have_equals ? BSDCONF_OP_ASSIGN : BSDCONF_OP_DEFAULT;
+ if (operator_equals && have_equals && n > 1) {
+ switch (directive[n - 1]) {
+ case '+': op = BSDCONF_OP_APPEND; break;
+ case '?': op = BSDCONF_OP_COND; break;
+ case ':': op = BSDCONF_OP_EXPAND; break;
+ case '!': op = BSDCONF_OP_SHELL; break;
+ }
+ if (op != BSDCONF_OP_ASSIGN)
+ directive[--n] = '\0';
+ }
+
+ /* Convert directive to lower case before comparison */
+ if (!case_sensitive)
+ bsdconf_strtolower(directive);
+
+ /* Move to what may be the start of the value */
+ if (!(bsemicolon && *p == ';') &&
+ !(strict_equals && *p == '=')) {
+ while (r != 0 && isspace(*p) && *p != '\n')
+ r = read(fd, p, 1);
+ }
+
+ /* An equals sign may have stopped us, should we eat it? */
+ if (r != 0 && bequals && *p == '=' && !strict_equals) {
+ have_equals = 1;
+ r = read(fd, p, 1);
+ while (r != 0 && isspace(*p) && *p != '\n')
+ r = read(fd, p, 1);
+ }
+
+ /* If no value, allocate a dummy value and jump to action */
+ if (r == 0 || *p == '\n' || *p == '#' ||
+ (bsemicolon && *p == ';')) {
+ /* Count the consumed terminator if a newline */
+ if (r != 0 && *p == '\n')
+ line++;
+ /* Flag a trailing comment so it is skipped */
+ if (r != 0 && *p == '#')
+ comment = 1;
+ /* Initialize the value if not already done */
+ if (value == NULL && (value = malloc(1)) == NULL)
+ goto fail;
+ value[0] = '\0';
+ goto call_function;
+ }
+
+ /* Get the current offset */
+ if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ goto fail;
+ curpos--;
+
+ /* Find the end of the value */
+ quote = 0;
+ ecomment = 0;
+ end = 0;
+ while (r != 0 && end == 0) {
+ /* Advance to the next character if we know we can */
+ if (*p != '\"' && *p != '#' && *p != '\n' &&
+ (!bsemicolon || *p != ';')) {
+ r = read(fd, p, 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)
+ goto fail;
+ 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)
+ goto fail;
+ r = read(fd, p, 1);
+
+ /*
+ * Count how many backslashes there are (an odd number
+ * means the key is escaped, even means otherwise).
+ */
+ for (n = 1; *p == '\\'; n++) {
+ /* Move back another offset to read */
+ if (lseek(fd, -2, SEEK_CUR) == -1)
+ goto fail;
+ r = read(fd, p, 1);
+ }
+
+ /* Move offset back to the key and read it */
+ if (lseek(fd, charpos, SEEK_SET) == -1)
+ goto fail;
+ r = read(fd, p, 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 = read(fd, p, 1);
+ }
+
+ /* Get the current offset */
+ if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ goto fail;
+
+ /* Get the length of the value */
+ n = (uint32_t)(charpos - curpos);
+ if (r != 0) /* more to read, but don't read ending key */
+ n--;
+
+ /* Move offset back to the beginning of the value */
+ if (lseek(fd, curpos, SEEK_SET) == -1)
+ goto fail;
+
+ /* Allocate and read the value into memory */
+ if (n > vsize) {
+ if ((t = realloc(value, n + 1)) == NULL)
+ goto fail;
+ value = t;
+ vsize = n;
+ }
+ r = read(fd, value, n);
+
+ /* Terminate the string */
+ value[n] = '\0';
+
+ /* Cut trailing whitespace off by termination */
+ t = value + n;
+ while (isspace(*--t))
+ *t = '\0';
+
+ /*
+ * Cut off a trailing inline-comment or statement-terminator
+ * key likewise. The `#' or `;' that terminated the value
+ * rides along with it (historic figpar behavior) and must
+ * not be mistaken for data. An escaped `;' is data and is
+ * never a terminator (see the end-key scan above).
+ */
+ if (ecomment && t > value && *t == '#') {
+ *t = '\0';
+ while (t > value && isspace(*--t))
+ *t = '\0';
+ } else if (bsemicolon && t > value && *t == ';') {
+ for (x = 0; t - x > value &&
+ *(t - x - 1) == '\\'; x++)
+ ;
+ if ((x & 1) == 0) {
+ *t = '\0';
+ while (t > value && isspace(*--t))
+ *t = '\0';
+ }
+ }
+
+ /* Escape the escaped quotes (see bsdconf_string.c) */
+ x = bsdconf_strcount(value, "\\\"");
+ if (x != 0 && (n + x) > vsize) {
+ if ((t = realloc(value, n + x + 1)) == NULL)
+ goto fail;
+ value = t;
+ vsize = n + x;
+ }
+ if (bsdconf_replaceall(value, "\\\"", "\\\\\"") < 0)
+ goto fail;
+
+ /* Remove all escaped newline characters */
+ if (bsdconf_replaceall(value, "\\\n", "") < 0)
+ goto fail;
+
+ /* Resolve escape sequences */
+ bsdconf_strexpand(value);
+
+call_function:
+ /* Abort if we're seeking only assignments */
+ if (require_equals && !have_equals) {
+ errno = EINVAL;
+ goto fail;
+ }
+
+ found = have_equals = 0; /* reset */
+
+ /* If there are no options defined, call unknown and loop */
+ if (options == NULL && unknown != NULL) {
+ error = unknown(NULL, dline, directive, value);
+ if (error != 0) {
+ rv = error;
+ goto cleanup;
+ }
+ continue;
+ }
+
+ /* Loop through the array looking for a match */
+ for (n = 0; options[n].directive != NULL; n++) {
+ error = fnmatch(options[n].directive, directive,
+ FNM_NOESCAPE);
+ if (error == 0) {
+ found = 1;
+ /* Call function for array index item */
+ options[n].op = op;
+ if (options[n].parse != NULL) {
+ error = options[n].parse(
+ &options[n],
+ dline, directive, value);
+ if (error != 0) {
+ rv = error;
+ goto cleanup;
+ }
+ }
+ } else if (error != FNM_NOMATCH) {
+ /* An error has occurred */
+ errno = EINVAL;
+ goto fail;
+ }
+ }
+ if (!found && unknown != NULL) {
+ /*
+ * No match was found for the value we read from the
+ * file; call function designated for unknown values.
+ */
+ error = unknown(NULL, dline, directive, value);
+ if (error != 0) {
+ rv = error;
+ goto cleanup;
+ }
+ }
+ }
+
+ goto cleanup;
+
+fail:
+ rv = -1;
+
+cleanup:
+ x = errno; /* preserve errno across free(3) and close(2) */
+ if (spoolfd != -1)
+ close(spoolfd);
+ free(directive);
+ free(value);
+ errno = x;
+
+ return (rv);
+}
+
+/*
+ * Parse the configuration file at `path' and execute the `parse' call-back
+ * functions for any directives defined by the array of config options (first
+ * argument). This is a convenience wrapper around bsdconf_fparse() above.
+ *
+ * Returns zero on success; otherwise returns -1 (or the non-zero result of a
+ * call-back) and errno should be consulted.
+ */
+int
+bsdconf_parse(struct bsdconf_option options[], const char *path,
+ int (*unknown)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value), uint16_t processing_options)
+{
+ int error;
+ int fd;
+ char rpath[PATH_MAX];
+
+ /* Sanity check: if no options and no unknown function, return */
+ if (options == NULL && unknown == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Resolve the file path */
+ if (realpath(path, rpath) == NULL)
+ return (-1);
+
+ /* Open the file */
+ if ((fd = open(rpath, O_RDONLY)) < 0)
+ return (-1);
+
+ error = bsdconf_fparse(options, fd, unknown, processing_options);
+
+ close(fd);
+ return (error);
+}
diff --git a/lib/libbsdconf/bsdconf_format.c b/lib/libbsdconf/bsdconf_format.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format.c
@@ -0,0 +1,683 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "bsdconf.h"
+#include "bsdconf_formats.h"
+
+/*
+ * The format abstraction layer registry.
+ *
+ * Every configuration file format -- built-in or bolted on at run-time -- is
+ * described by the same small descriptor (struct bsdconf_format_def in
+ * bsdconf.h): a target keyword, a default write path, an ordered list of
+ * configuration sources, and the processing/put bitmasks that drive the
+ * shared parsing and writing cores. There is exactly one engine; a format
+ * merely parameterizes it.
+ *
+ * Each built-in format is a self-contained translation unit
+ * (bsdconf_format_<keyword>.c) documenting and defining its own descriptor;
+ * this file only collects the descriptors into the table indexed by enum
+ * bsdconf_format and resolves keywords, basenames, and source lists against
+ * it (see bsdconf_formats.h for the recipe to add a format).
+ *
+ * A format that is "mostly like" an existing one need not be added at all:
+ * an application calls bsdconf_format_derive() to inherit the nearest
+ * descriptor, adjusts only the members that differ, and registers the
+ * result with bsdconf_format_register(). The handle it gets back taps into
+ * the same core engine everywhere a built-in format works, including
+ * keyword, path, and source-list resolution in sysconf(8).
+ */
+
+static const struct bsdconf_format_def *bsdconf_formats[] = {
+ [BSDCONF_FORMAT_GENERIC] = &bsdconf_format_generic_def,
+ [BSDCONF_FORMAT_LOADER] = &bsdconf_format_loader_def,
+ [BSDCONF_FORMAT_SYSCTL] = &bsdconf_format_sysctl_def,
+ [BSDCONF_FORMAT_MAKE] = &bsdconf_format_make_def,
+ [BSDCONF_FORMAT_SRC] = &bsdconf_format_src_def,
+ [BSDCONF_FORMAT_SRC_ENV] = &bsdconf_format_src_env_def,
+};
+#define BSDCONF_NFORMATS \
+ (sizeof(bsdconf_formats) / sizeof(*bsdconf_formats))
+
+/*
+ * Registered (bolt-on) formats. Descriptors are copied by value; the
+ * keyword and path strings they reference remain owned by the caller and
+ * must stay valid for the life of the registration.
+ */
+#define BSDCONF_MAXUSERFORMATS 32
+static struct bsdconf_format_def bsdconf_user_formats[BSDCONF_MAXUSERFORMATS];
+static unsigned int bsdconf_nuser_formats = 0;
+
+/*
+ * Return the descriptor for `format' or NULL if the format is neither
+ * built-in nor registered.
+ */
+const struct bsdconf_format_def *
+bsdconf_format_lookup(enum bsdconf_format format)
+{
+ unsigned int n;
+
+ if ((unsigned int)format < BSDCONF_NFORMATS)
+ return (bsdconf_formats[format]);
+
+ n = (unsigned int)format - BSDCONF_FORMAT_USER;
+ if (format >= BSDCONF_FORMAT_USER && n < bsdconf_nuser_formats)
+ return (&bsdconf_user_formats[n]);
+
+ return (NULL);
+}
+
+/*
+ * Copy the descriptor of `base' into `def' so that a caller can adjust only
+ * the members that differ before registering the result as a new format.
+ * On success, returns zero; otherwise returns -1 (unknown base format) and
+ * errno is set to EINVAL.
+ */
+int
+bsdconf_format_derive(enum bsdconf_format base,
+ struct bsdconf_format_def *def)
+{
+ const struct bsdconf_format_def *found;
+
+ /* Check arguments */
+ if (def == NULL || (found = bsdconf_format_lookup(base)) == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ *def = *found;
+ return (0);
+}
+
+/*
+ * Register a new configuration file format described by `def', storing its
+ * newly assigned handle through `format'. The descriptor is copied; the
+ * keyword and path strings it references are not (see above). On success,
+ * returns zero; otherwise returns -1 and errno is set (EINVAL for a bad
+ * argument, ENOSPC when the registration table is full).
+ */
+int
+bsdconf_format_register(const struct bsdconf_format_def *def,
+ enum bsdconf_format *format)
+{
+
+ /* Check arguments */
+ if (def == NULL || format == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ if (bsdconf_nuser_formats >= BSDCONF_MAXUSERFORMATS) {
+ errno = ENOSPC;
+ return (-1);
+ }
+
+ bsdconf_user_formats[bsdconf_nuser_formats] = *def;
+ *format = (enum bsdconf_format)
+ (BSDCONF_FORMAT_USER + bsdconf_nuser_formats);
+ bsdconf_nuser_formats++;
+
+ return (0);
+}
+
+/*
+ * Map a target keyword (e.g., "loader") to its format, storing the result
+ * through `format'. Registered formats are searched after the built-ins.
+ * On success, returns zero; otherwise returns -1 (unknown keyword; `format'
+ * is left untouched).
+ */
+int
+bsdconf_format_find(const char *keyword, enum bsdconf_format *format)
+{
+ unsigned int n;
+
+ /* Check arguments */
+ if (keyword == NULL || format == NULL)
+ return (-1);
+
+ for (n = 0; n < BSDCONF_NFORMATS; n++) {
+ if (bsdconf_formats[n]->keyword != NULL &&
+ strcmp(bsdconf_formats[n]->keyword, keyword) == 0) {
+ *format = (enum bsdconf_format)n;
+ return (0);
+ }
+ }
+ for (n = 0; n < bsdconf_nuser_formats; n++) {
+ if (bsdconf_user_formats[n].keyword != NULL &&
+ strcmp(bsdconf_user_formats[n].keyword, keyword) == 0) {
+ *format = (enum bsdconf_format)
+ (BSDCONF_FORMAT_USER + n);
+ return (0);
+ }
+ }
+
+ return (-1);
+}
+
+/*
+ * Test whether the basename of `path' matches `<keyword>.conf' with an
+ * optional trailing suffix (e.g., "sysctl.conf.local" matches "sysctl").
+ */
+static int
+bsdconf_basename_matches(const char *base, const char *keyword)
+{
+ size_t klen;
+
+ if (keyword == NULL)
+ return (0);
+ klen = strlen(keyword);
+ if (strncmp(base, keyword, klen) != 0)
+ return (0);
+ return (strncmp(base + klen, ".conf", 5) == 0);
+}
+
+/*
+ * Guess the format of an arbitrary configuration file from the basename of
+ * its `path' (e.g., "/etc/sysctl.conf.local" is BSDCONF_FORMAT_SYSCTL).
+ * Registered formats are searched after the built-ins. Returns
+ * BSDCONF_FORMAT_GENERIC when nothing matches.
+ */
+enum bsdconf_format
+bsdconf_format_guess(const char *path)
+{
+ unsigned int n;
+ const char *base;
+
+ /* Check arguments */
+ if (path == NULL)
+ return (BSDCONF_FORMAT_GENERIC);
+
+ /* Isolate the basename (do not modify the argument) */
+ if ((base = strrchr(path, '/')) != NULL)
+ base++;
+ else
+ base = path;
+
+ for (n = 0; n < BSDCONF_NFORMATS; n++)
+ if (bsdconf_basename_matches(base,
+ bsdconf_formats[n]->keyword))
+ return ((enum bsdconf_format)n);
+ for (n = 0; n < bsdconf_nuser_formats; n++)
+ if (bsdconf_basename_matches(base,
+ bsdconf_user_formats[n].keyword))
+ return ((enum bsdconf_format)
+ (BSDCONF_FORMAT_USER + n));
+
+ return (BSDCONF_FORMAT_GENERIC);
+}
+
+/*
+ * Return the default file path for `format' (e.g., "/boot/loader.conf") or
+ * NULL if the format has no fixed path (e.g., BSDCONF_FORMAT_GENERIC).
+ */
+const char *
+bsdconf_format_path(enum bsdconf_format format)
+{
+ const struct bsdconf_format_def *def;
+
+ if ((def = bsdconf_format_lookup(format)) == NULL)
+ return (NULL);
+ return (def->path);
+}
+
+/*
+ * Return the processing_options bitmask suited to `format', for passing to
+ * bsdconf_parse(), bsdconf_fparse(), and bsdconf_put(). Unknown formats
+ * fall back to the generic descriptor.
+ */
+uint16_t
+bsdconf_format_processing(enum bsdconf_format format)
+{
+ const struct bsdconf_format_def *def;
+
+ if ((def = bsdconf_format_lookup(format)) == NULL)
+ def = bsdconf_formats[BSDCONF_FORMAT_GENERIC];
+ return (def->processing);
+}
+
+/*
+ * Return the put_options bitmask suited to `format', for passing to
+ * bsdconf_put(). Unknown formats fall back to the generic descriptor.
+ */
+uint16_t
+bsdconf_format_put(enum bsdconf_format format)
+{
+ const struct bsdconf_format_def *def;
+
+ if ((def = bsdconf_format_lookup(format)) == NULL)
+ def = bsdconf_formats[BSDCONF_FORMAT_GENERIC];
+ return (def->put);
+}
+
+/*
+ * Append `path' (already allocated; ownership is taken) to the growing
+ * `files' array. On success, returns zero; otherwise returns -1 (errno set
+ * by realloc(3)) and `path' is freed.
+ */
+static int
+bsdconf_files_add(char ***files, size_t *nfiles, size_t *size, char *path)
+{
+ char **tmp;
+
+ if (path == NULL)
+ return (-1);
+ if (*nfiles >= *size) {
+ *size = (*size == 0) ? 8 : *size << 1;
+ tmp = realloc(*files, *size * sizeof(**files));
+ if (tmp == NULL) {
+ free(path);
+ return (-1);
+ }
+ *files = tmp;
+ }
+ (*files)[(*nfiles)++] = path;
+ return (0);
+}
+
+/*
+ * scandir(3) filter accepting non-hidden `*.conf' entries.
+ */
+static int
+bsdconf_files_filter(const struct dirent *entry)
+{
+ size_t len;
+
+ if (entry->d_name[0] == '.')
+ return (0);
+ len = strlen(entry->d_name);
+ return (len > 5 && strcmp(entry->d_name + len - 5, ".conf") == 0);
+}
+
+/*
+ * Compose the full path `prefix' + `path' [+ `/' + `name' [+ `.conf']] into
+ * newly allocated storage. Returns NULL on allocation failure (errno set).
+ */
+static char *
+bsdconf_files_path(const char *prefix, const char *path, const char *name,
+ const char *suffix)
+{
+ char *full;
+ int len;
+
+ len = snprintf(NULL, 0, "%s%s%s%s%s", prefix, path,
+ name != NULL ? "/" : "", name != NULL ? name : "",
+ suffix != NULL ? suffix : "");
+ if (len < 0 || (full = malloc((size_t)len + 1)) == NULL)
+ return (NULL);
+ snprintf(full, (size_t)len + 1, "%s%s%s%s%s", prefix, path,
+ name != NULL ? "/" : "", name != NULL ? name : "",
+ suffix != NULL ? suffix : "");
+ return (full);
+}
+
+/*
+ * Append each existing `*.conf' entry of directory `path' (prefixed with
+ * `prefix'), sorted, to the growing `files' array. A missing or unreadable
+ * directory is not an error (a drop-in directory is optional by nature).
+ * On success, returns zero; otherwise returns -1 (errno set).
+ */
+static int
+bsdconf_files_add_dir(char ***files, size_t *nfiles, size_t *size,
+ const char *prefix, const char *path)
+{
+ int n, nentries;
+ char *dir;
+ char *full;
+ struct dirent **entries;
+
+ if ((dir = bsdconf_files_path(prefix, path, NULL, NULL)) == NULL)
+ return (-1);
+ nentries = scandir(dir, &entries, bsdconf_files_filter, alphasort);
+ free(dir);
+ if (nentries < 0)
+ return (0);
+ for (n = 0; n < nentries; n++) {
+ full = bsdconf_files_path(prefix, path, entries[n]->d_name,
+ NULL);
+ if (bsdconf_files_add(files, nfiles, size, full) != 0) {
+ while (n < nentries)
+ free(entries[n++]);
+ free(entries);
+ return (-1);
+ }
+ free(entries[n]);
+ }
+ free(entries);
+ return (0);
+}
+
+/*
+ * Parse call-back for the list directives chased by
+ * bsdconf_files_discover() below. The option's `value.data' member points
+ * at the (char *) slot holding the current value of the directive; each
+ * assignment encountered replaces the slot (last assignment wins, exactly
+ * as the consumer of the file behaves).
+ */
+static int
+bsdconf_files_list_cb(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value)
+{
+ char *copy;
+ char **slot = (char **)option->value.data;
+
+ (void)line;
+ (void)directive;
+
+ if ((copy = strdup(bsdconf_unquote(value))) == NULL)
+ return (-1);
+ free(*slot);
+ *slot = copy;
+ return (0);
+}
+
+/*
+ * Advance to the next whitespace-separated token of `list' at or beyond
+ * `cursor', storing its length through `lenp'. Returns the token or NULL
+ * when the list is exhausted.
+ */
+static const char *
+bsdconf_files_token(const char *cursor, size_t *lenp)
+{
+
+ if (cursor == NULL)
+ return (NULL);
+ cursor += strspn(cursor, " \t");
+ if ((*lenp = strcspn(cursor, " \t")) == 0)
+ return (NULL);
+ return (cursor);
+}
+
+/*
+ * Ceiling on the number of configuration files discovery will chase,
+ * bounding run-away (or maliciously self-referential) file lists.
+ */
+#define BSDCONF_DISCOVER_MAX 64
+
+/*
+ * Resolve the backing files of a format whose descriptor carries a
+ * `defaults' file by performing the same discovery its consumer does: read
+ * the defaults file, then chase the file-list directive (e.g.,
+ * loader_conf_files) it defines -- re-reading it after every file, since
+ * any file may revise the lists -- and finally append the drop-in
+ * directory entries and local files named by the other two list
+ * directives. Every file named by the file-list directive is included in
+ * the result whether or not it exists (a listed file is a legitimate
+ * write target before its first directive lands); the defaults file
+ * itself is deliberately excluded. The write candidate stored through
+ * `write_idx' is the last file-list entry, the final word among the
+ * regular configuration files.
+ *
+ * The defaults file is `defaults' verbatim when non-NULL (a caller-level
+ * override; see sysconf(8)'s LOADER_DEFAULTS); otherwise the descriptor
+ * default prefixed with `rootdir'.
+ *
+ * On success, returns zero. Returns 1 when the defaults file is missing
+ * or unreadable, directing the caller to fall back to the static source
+ * list. Otherwise returns -1 (errno set).
+ */
+static int
+bsdconf_files_discover(const struct bsdconf_format_def *def,
+ const char *rootdir, const char *defaults, char ***files,
+ size_t *nfiles, size_t *size, size_t *write_idx)
+{
+ int error, rv = -1;
+ uint16_t processing;
+ size_t len, n, nvisited = 0;
+ char *dpath = NULL;
+ char *full;
+ char *name;
+ char *conf_dirs = NULL;
+ char *conf_files = NULL;
+ char *local_files = NULL;
+ const char *token;
+ char *visited[BSDCONF_DISCOVER_MAX];
+ struct bsdconf_option options[4];
+
+ /* Wire each list directive to the slot holding its value */
+ memset(options, 0, sizeof(options));
+ n = 0;
+ if (def->files_directive != NULL) {
+ options[n].directive = def->files_directive;
+ options[n].value.data = &conf_files;
+ options[n].parse = bsdconf_files_list_cb;
+ n++;
+ }
+ if (def->dirs_directive != NULL) {
+ options[n].directive = def->dirs_directive;
+ options[n].value.data = &conf_dirs;
+ options[n].parse = bsdconf_files_list_cb;
+ n++;
+ }
+ if (def->local_directive != NULL) {
+ options[n].directive = def->local_directive;
+ options[n].value.data = &local_files;
+ options[n].parse = bsdconf_files_list_cb;
+ n++;
+ }
+
+ /* Resolve and read the defaults file (missing means fall back) */
+ if (defaults != NULL)
+ dpath = strdup(defaults);
+ else
+ dpath = bsdconf_files_path(rootdir, def->defaults, NULL,
+ NULL);
+ if (dpath == NULL)
+ goto cleanup;
+ processing = def->processing & ~BSDCONF_REQUIRE_EQUALS;
+ if (bsdconf_parse(options, dpath, NULL, processing) != 0) {
+ rv = 1;
+ goto cleanup;
+ }
+
+ /*
+ * Chase the file-list directive. Each pass rescans the current
+ * value of the list from the start for the first file not yet
+ * visited (any file read may have revised the list), reads it,
+ * and repeats until every listed file has been visited.
+ */
+ for (;;) {
+ token = conf_files;
+ while ((token = bsdconf_files_token(token, &len)) != NULL) {
+ for (n = 0; n < nvisited; n++)
+ if (strncmp(visited[n], token, len) == 0 &&
+ visited[n][len] == '\0')
+ break;
+ if (n == nvisited)
+ break;
+ token += len;
+ }
+ if (token == NULL || nvisited >= BSDCONF_DISCOVER_MAX)
+ break;
+ if ((visited[nvisited] = strndup(token, len)) == NULL)
+ goto cleanup;
+ full = bsdconf_files_path(rootdir, visited[nvisited], NULL,
+ NULL);
+ nvisited++;
+ if (bsdconf_files_add(files, nfiles, size, full) != 0)
+ goto cleanup;
+ /* A listed file that does not (yet) exist is normal */
+ (void)bsdconf_parse(options, full, NULL, processing);
+ }
+ *write_idx = (*nfiles > 0) ? *nfiles - 1 : 0;
+
+ /* Drop-in directories, then local files, from the final lists */
+ token = conf_dirs;
+ while ((token = bsdconf_files_token(token, &len)) != NULL) {
+ if ((name = strndup(token, len)) == NULL)
+ goto cleanup;
+ token += len;
+ error = bsdconf_files_add_dir(files, nfiles, size, rootdir,
+ name);
+ free(name);
+ if (error != 0)
+ goto cleanup;
+ }
+ token = local_files;
+ while ((token = bsdconf_files_token(token, &len)) != NULL) {
+ if ((name = strndup(token, len)) == NULL)
+ goto cleanup;
+ token += len;
+ full = bsdconf_files_path(rootdir, name, NULL, NULL);
+ free(name);
+ if (bsdconf_files_add(files, nfiles, size, full) != 0)
+ goto cleanup;
+ }
+
+ rv = 0;
+
+cleanup:
+ n = errno; /* preserve errno across free(3) */
+ while (nvisited > 0)
+ free(visited[--nvisited]);
+ free(conf_files);
+ free(conf_dirs);
+ free(local_files);
+ free(dpath);
+ errno = n;
+ return (rv);
+}
+
+/*
+ * Resolve the ordered list of configuration files backing `format' into a
+ * newly allocated array of newly allocated paths, stored through `filesp'
+ * with the count stored through `nfilesp'. Files appear in the order the
+ * system sources them at boot; a directive in a later file overrides the
+ * same directive in an earlier one, making the last file listing a
+ * directive the authoritative source of its value. When `write_idxp' is
+ * non-NULL, the index of the file recommended for directives found in no
+ * file at all is stored through it (the format's default write path, or
+ * the last regular configuration file when discovery is in play).
+ *
+ * A format whose descriptor names a defaults file resolves its list by
+ * discovery (see bsdconf_files_discover() above); `defaults' overrides
+ * the descriptor's defaults file when non-NULL and is used verbatim (it
+ * is not prefixed with `rootdir'). For all other formats `defaults' is
+ * ignored and the static source list governs, as it also does when the
+ * defaults file is missing.
+ *
+ * Each static path is prefixed with `rootdir' unless NULL or empty.
+ * Sources of type BSDCONF_SOURCE_FILE are always listed, whether or not
+ * the file exists; sources of type BSDCONF_SOURCE_DIR contribute each
+ * existing `*.conf' entry (sorted); sources of type BSDCONF_SOURCE_MODDIR
+ * contribute `<module>.conf' only when `module' is non-NULL. A format
+ * with no source list resolves to its default path alone.
+ *
+ * On success, returns zero and the caller frees the result with
+ * bsdconf_format_files_free(). Otherwise returns -1 and errno is set
+ * (EINVAL for an unknown format or a format with no backing files).
+ */
+int
+bsdconf_format_files(enum bsdconf_format format, const char *rootdir,
+ const char *module, const char *defaults, char ***filesp,
+ size_t *nfilesp, size_t *write_idxp)
+{
+ int error;
+ size_t nfiles = 0;
+ size_t size = 0;
+ size_t write_idx = 0;
+ char **files = NULL;
+ char *path;
+ const struct bsdconf_format_def *def;
+ const struct bsdconf_source *source;
+
+ /* Check arguments */
+ if (filesp == NULL || nfilesp == NULL ||
+ (def = bsdconf_format_lookup(format)) == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ if (rootdir == NULL)
+ rootdir = "";
+
+ /* Discover the list when the descriptor names a defaults file */
+ if (def->defaults != NULL) {
+ error = bsdconf_files_discover(def, rootdir, defaults,
+ &files, &nfiles, &size, &write_idx);
+ if (error < 0)
+ goto cleanup;
+ if (error == 0 && nfiles > 0)
+ goto done;
+ /* Missing defaults file; fall back to static sources */
+ bsdconf_format_files_free(files, nfiles);
+ files = NULL;
+ nfiles = size = write_idx = 0;
+ }
+
+ /* A format with no source list is backed by its path alone */
+ if (def->sources == NULL) {
+ if (def->path == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ path = bsdconf_files_path(rootdir, def->path, NULL, NULL);
+ if (bsdconf_files_add(&files, &nfiles, &size, path) != 0)
+ goto cleanup;
+ goto done;
+ }
+
+ for (source = def->sources; source->path != NULL; source++) {
+ switch (source->type) {
+ case BSDCONF_SOURCE_FILE:
+ path = bsdconf_files_path(rootdir, source->path,
+ NULL, NULL);
+ if (def->path != NULL &&
+ strcmp(source->path, def->path) == 0)
+ write_idx = nfiles;
+ if (bsdconf_files_add(&files, &nfiles, &size,
+ path) != 0)
+ goto cleanup;
+ break;
+ case BSDCONF_SOURCE_DIR:
+ if (bsdconf_files_add_dir(&files, &nfiles, &size,
+ rootdir, source->path) != 0)
+ goto cleanup;
+ break;
+ case BSDCONF_SOURCE_MODDIR:
+ if (module == NULL)
+ break;
+ path = bsdconf_files_path(rootdir, source->path,
+ module, ".conf");
+ if (bsdconf_files_add(&files, &nfiles, &size,
+ path) != 0)
+ goto cleanup;
+ break;
+ }
+ }
+
+done:
+ *filesp = files;
+ *nfilesp = nfiles;
+ if (write_idxp != NULL)
+ *write_idxp = write_idx;
+ return (0);
+
+cleanup:
+ bsdconf_format_files_free(files, nfiles);
+ return (-1);
+}
+
+/*
+ * Release an array of paths obtained from bsdconf_format_files().
+ */
+void
+bsdconf_format_files_free(char **files, size_t nfiles)
+{
+ size_t n;
+
+ if (files == NULL)
+ return;
+ for (n = 0; n < nfiles; n++)
+ free(files[n]);
+ free(files);
+}
diff --git a/lib/libbsdconf/bsdconf_format_generic.c b/lib/libbsdconf/bsdconf_format_generic.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_generic.c
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_GENERIC: `name[=value]' assignments with values quoted
+ * only when required to round-trip (embedded whitespace or a comment
+ * character). The fallback when nothing more specific is known about a
+ * file, and the default base for derived formats. It has a target keyword
+ * so that a sysconf(8)-style consumer can apply it to an arbitrary file
+ * explicitly, but no default path; the caller must always name the file.
+ *
+ * NB: Formats whose statements are a space-separated directive and value
+ * with no equals sign at all (the httpd.conf style the ancestral figpar
+ * engine was raised on) need no descriptor of their own to parse: with
+ * BSDCONF_BREAK_ON_EQUALS omitted, the first whitespace-delimited token is
+ * the directive and the remainder of the line is the raw value, and
+ * directive matching is case insensitive unless BSDCONF_CASE_SENSITIVE is
+ * set.
+ */
+const struct bsdconf_format_def bsdconf_format_generic_def = {
+ .keyword = "generic",
+ .path = NULL, /* no default path */
+ .sources = NULL, /* no source list */
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE,
+ .put = 0,
+};
diff --git a/lib/libbsdconf/bsdconf_format_loader.c b/lib/libbsdconf/bsdconf_format_loader.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_loader.c
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_LOADER: loader.conf(5).
+ *
+ * Assignments with no whitespace around the `=' -- the Forth reader in
+ * support.4th rejects anything else, so BSDCONF_STRICT_EQUALS is enforced.
+ * Quoted or unquoted input values are accepted when parsing but values are
+ * always written back quoted (BSDCONF_PUT_QUOTE_ALWAYS), the form every
+ * loader accepts for every value.
+ *
+ * The loader itself hardcodes only /boot/defaults/loader.conf and discovers
+ * every other file from the loader_conf_files, loader_conf_dirs, and
+ * local_loader_conf_files directives encountered along the way -- any file
+ * so discovered may revise those lists in turn. The `defaults' member
+ * quintet below hands that same discovery to bsdconf_format_files(); the
+ * static source list is only the fallback for systems whose defaults file
+ * is missing. The defaults file itself is deliberately excluded from the
+ * resolved list, mirroring how sysrc(8) excludes /etc/defaults/rc.conf.
+ */
+static const struct bsdconf_source bsdconf_loader_sources[] = {
+ { BSDCONF_SOURCE_FILE, "/boot/device.hints" },
+ { BSDCONF_SOURCE_FILE, "/boot/loader.conf" },
+ { BSDCONF_SOURCE_DIR, "/boot/loader.conf.d" },
+ { BSDCONF_SOURCE_FILE, "/boot/loader.conf.local" },
+ { BSDCONF_SOURCE_FILE, NULL },
+};
+
+const struct bsdconf_format_def bsdconf_format_loader_def = {
+ .keyword = "loader",
+ .path = "/boot/loader.conf",
+ .sources = bsdconf_loader_sources,
+ .defaults = "/boot/defaults/loader.conf",
+ .defaults_env = "LOADER_DEFAULTS",
+ .files_directive = "loader_conf_files",
+ .dirs_directive = "loader_conf_dirs",
+ .local_directive = "local_loader_conf_files",
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_REQUIRE_EQUALS | BSDCONF_STRICT_EQUALS,
+ .put = BSDCONF_PUT_QUOTE_ALWAYS | BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_format_make.c b/lib/libbsdconf/bsdconf_format_make.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_make.c
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_MAKE: make.conf(5).
+ *
+ * make(1) syntax: assignment modifiers (`+=' `?=' `:=' `!=') are
+ * recognized, matched, and rewritten (BSDCONF_OPERATOR_EQUALS), and values
+ * are never quoted (BSDCONF_PUT_UNQUOTED) -- a value runs to the end of
+ * the line, embedded whitespace included, so only a value that could not
+ * round-trip verbatim (an embedded newline or comment marker) is rejected.
+ */
+const struct bsdconf_format_def bsdconf_format_make_def = {
+ .keyword = "make",
+ .path = "/etc/make.conf",
+ .sources = NULL, /* /etc/make.conf alone */
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_OPERATOR_EQUALS,
+ .put = BSDCONF_PUT_UNQUOTED | BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_format_src.c b/lib/libbsdconf/bsdconf_format_src.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_src.c
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_SRC: src.conf(5).
+ *
+ * Identical syntax to make.conf(5) -- src.conf is read by the make(1)
+ * include machinery in share/mk -- but consulted only when building the
+ * FreeBSD source tree (typically WITH_ and WITHOUT_ knobs, which are
+ * value-less; setting one to an empty value is the idiom, hence
+ * BSDCONF_PUT_ALLOW_EMPTY).
+ */
+const struct bsdconf_format_def bsdconf_format_src_def = {
+ .keyword = "src",
+ .path = "/etc/src.conf",
+ .sources = NULL, /* /etc/src.conf alone */
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_OPERATOR_EQUALS,
+ .put = BSDCONF_PUT_UNQUOTED | BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_format_src_env.c b/lib/libbsdconf/bsdconf_format_src_env.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_src_env.c
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_SRC_ENV: src-env.conf(5).
+ *
+ * Identical syntax to make.conf(5) and src.conf(5), but read earlier in
+ * the make(1) startup sequence (before the environment is finalized;
+ * see SRC_ENV_CONF in src.conf(5)), which is why it is a separate file
+ * and hence a separate target keyword.
+ */
+const struct bsdconf_format_def bsdconf_format_src_env_def = {
+ .keyword = "src-env",
+ .path = "/etc/src-env.conf",
+ .sources = NULL, /* /etc/src-env.conf alone */
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_OPERATOR_EQUALS,
+ .put = BSDCONF_PUT_UNQUOTED | BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_format_sysctl.c b/lib/libbsdconf/bsdconf_format_sysctl.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_sysctl.c
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_SYSCTL: sysctl.conf(5).
+ *
+ * Assignments in the format of the sysctl(8) command, whose file parser
+ * trims whitespace around the `=' and strips one pair of quotes around the
+ * value, so values are written unquoted unless quoting is required to
+ * round-trip (embedded whitespace or a comment character).
+ *
+ * Sourced at boot in the order below: the first two by rc.d/sysctl (and
+ * again by rc.d/sysctl_lastload), the module-specific drop-in by
+ * rc.subr(8)'s load_kld when the named kernel module loads.
+ */
+static const struct bsdconf_source bsdconf_sysctl_sources[] = {
+ { BSDCONF_SOURCE_FILE, "/etc/sysctl.conf" },
+ { BSDCONF_SOURCE_FILE, "/etc/sysctl.conf.local" },
+ { BSDCONF_SOURCE_MODDIR, "/etc/sysctl.kld.d" },
+ { BSDCONF_SOURCE_FILE, NULL },
+};
+
+const struct bsdconf_format_def bsdconf_format_sysctl_def = {
+ .keyword = "sysctl",
+ .path = "/etc/sysctl.conf",
+ .sources = bsdconf_sysctl_sources,
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_REQUIRE_EQUALS,
+ .put = BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_formats.h b/lib/libbsdconf/bsdconf_formats.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_formats.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef _BSDCONF_FORMATS_H_
+#define _BSDCONF_FORMATS_H_
+
+#include "bsdconf.h"
+
+/*
+ * The built-in format descriptors, one translation unit per format.
+ * This header is deliberately not installed.
+ *
+ * Every format is a self-contained bsdconf_format_<keyword>.c defining a
+ * single descriptor; the registry in bsdconf_format.c does nothing but
+ * collect them into the table indexed by enum bsdconf_format. Adding a
+ * format to the library therefore touches no existing parsing or writing
+ * code: drop in the new file, declare its descriptor here, append one
+ * enum constant in bsdconf.h and one pointer to the registry table, and
+ * list the file in the Makefile.
+ *
+ * A candidate format whose syntax the existing processing/put flags cannot
+ * express (for example, brace-grouped statement blocks) is handled by
+ * teaching the shared engine (bsdconf.c and bsdconf_put.c) a new flag that
+ * the new descriptor is first to set, so the capability composes with
+ * every other format rather than living in a private parser.
+ */
+extern const struct bsdconf_format_def bsdconf_format_generic_def;
+extern const struct bsdconf_format_def bsdconf_format_loader_def;
+extern const struct bsdconf_format_def bsdconf_format_make_def;
+extern const struct bsdconf_format_def bsdconf_format_src_def;
+extern const struct bsdconf_format_def bsdconf_format_src_env_def;
+extern const struct bsdconf_format_def bsdconf_format_sysctl_def;
+
+#endif /* !_BSDCONF_FORMATS_H_ */
diff --git a/lib/libbsdconf/bsdconf_internal.h b/lib/libbsdconf/bsdconf_internal.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_internal.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2001-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef _BSDCONF_INTERNAL_H_
+#define _BSDCONF_INTERNAL_H_
+
+#include <sys/stat.h>
+
+#include <stddef.h>
+
+/*
+ * Private interfaces shared between the libbsdconf translation units.
+ * This header is deliberately not installed.
+ */
+
+/*
+ * Not every POSIX system defines ALLPERMS (notably musl libc), and the
+ * sticky bit (S_ISVTX) is an XSI extension unavailable under strict POSIX.
+ */
+#ifndef ALLPERMS
+#ifdef S_ISVTX
+#define ALLPERMS (S_ISUID|S_ISGID|S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO)
+#else
+#define ALLPERMS (S_ISUID|S_ISGID|S_IRWXU|S_IRWXG|S_IRWXO)
+#endif
+#endif
+
+/* String manipulation routines (bsdconf_string.c) */
+void bsdconf_strexpand(char *_source);
+void bsdconf_strtolower(char *_source);
+int bsdconf_replaceall(char *_source, const char *_find,
+ const char *_replace);
+unsigned int bsdconf_strcount(const char *_source, const char *_find);
+
+/* Restarting full-length write (bsdconf_put.c) */
+int bsdconf_writeall(int _fd, const void *_data, size_t _len);
+
+#endif /* !_BSDCONF_INTERNAL_H_ */
diff --git a/lib/libbsdconf/bsdconf_put.c b/lib/libbsdconf/bsdconf_put.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_put.c
@@ -0,0 +1,970 @@
+/*
+ * Copyright (c) 2015-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+/*
+ * Parsed extent of a single statement within the in-memory copy of the file.
+ * All members are byte offsets into the buffer except as noted. A statement
+ * runs from the first byte of its directive to its terminator (newline, an
+ * inline `#' comment, or a semicolon when enabled).
+ */
+struct bsdconf_stmt {
+ size_t line_start; /* start of the physical line (for removal) */
+ size_t dir_start; /* first byte of the directive */
+ size_t dir_end; /* one past the last byte of the directive */
+ size_t op_start; /* first byte of the assignment operator */
+ size_t eq; /* index of the `=' (valid iff have_equals) */
+ size_t val_start; /* first byte of the value (== val_end if none) */
+ size_t val_end; /* one past last byte of value (ws trimmed) */
+ size_t term; /* index of the terminator (`\n', `#', `;', EOF) */
+ size_t line_end; /* one past the terminating newline (for removal) */
+ uint32_t line; /* 1-based line number of the directive */
+ enum bsdconf_op op; /* assignment operator of the statement */
+ int have_value; /* nonzero if a value was present */
+ int have_equals; /* nonzero if an `=' separated directive/value */
+};
+
+/*
+ * Map an assignment operator to its config file token. BSDCONF_OP_DEFAULT
+ * maps to the plain `=' token.
+ */
+static const char *
+bsdconf_op_token(enum bsdconf_op op)
+{
+ switch (op) {
+ case BSDCONF_OP_APPEND:
+ return ("+=");
+ case BSDCONF_OP_COND:
+ return ("?=");
+ case BSDCONF_OP_EXPAND:
+ return (":=");
+ case BSDCONF_OP_SHELL:
+ return ("!=");
+ case BSDCONF_OP_DEFAULT:
+ case BSDCONF_OP_ASSIGN:
+ default:
+ return ("=");
+ }
+}
+
+/*
+ * Write exactly `len' bytes from `data' to `fd', restarting short and
+ * interrupted writes. Returns zero on success; -1 (with errno set) on error.
+ * Shared with bsdconf_spool() (see bsdconf_internal.h).
+ */
+int
+bsdconf_writeall(int fd, const void *data, size_t len)
+{
+ const char *p = data;
+ ssize_t w;
+
+ while (len > 0) {
+ w = write(fd, p, len);
+ if (w < 0) {
+ if (errno == EINTR)
+ continue;
+ return (-1);
+ }
+ p += w;
+ len -= (size_t)w;
+ }
+ return (0);
+}
+
+/*
+ * Read the entire contents of the open descriptor `fd' (whose size is `size')
+ * into a freshly allocated, NUL-terminated buffer. The actual number of bytes
+ * read is stored through `lenp'. Returns the buffer on success (which the
+ * caller must free) or NULL (with errno set) on error.
+ */
+static char *
+bsdconf_readfile(int fd, size_t size, size_t *lenp)
+{
+ char *buf;
+ size_t off = 0;
+ ssize_t r;
+
+ if ((buf = malloc(size + 1)) == NULL)
+ return (NULL);
+
+ while (off < size) {
+ r = read(fd, buf + off, size - off);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ free(buf);
+ return (NULL);
+ }
+ if (r == 0) /* premature EOF (file shrank); stop */
+ break;
+ off += (size_t)r;
+ }
+
+ buf[off] = '\0';
+ *lenp = off;
+ return (buf);
+}
+
+/*
+ * Write `len' bytes to `fd' and, when any bytes are written, remember the
+ * last one through `last' (as an unsigned char, or left untouched for a
+ * zero-length write). This lets the caller track whether the output so far
+ * ends in a newline without having to re-read the descriptor. Returns zero
+ * on success; -1 (with errno set) on error.
+ */
+static int
+bsdconf_emit(int fd, const void *data, size_t len, int *last)
+{
+
+ if (len == 0)
+ return (0);
+ if (bsdconf_writeall(fd, data, len) != 0)
+ return (-1);
+ *last = (unsigned char)((const char *)data)[len - 1];
+ return (0);
+}
+
+/*
+ * Determine whether the string value `s' must be double-quoted on output so
+ * that it round-trips through the parser unchanged. Empty strings are quoted
+ * so that they remain distinguishable from a value-less directive.
+ */
+static int
+bsdconf_needs_quote(const char *s, int bsemicolon)
+{
+ const char *p;
+
+ if (*s == '\0')
+ return (1);
+ for (p = s; *p != '\0'; p++) {
+ if (isspace((unsigned char)*p) || *p == '#' || *p == '"' ||
+ *p == '\\')
+ return (1);
+ if (bsemicolon && *p == ';')
+ return (1);
+ }
+ return (0);
+}
+
+/*
+ * Determine whether the string value `s' round-trips through the parser
+ * when written verbatim (no quoting available; make.conf(5) semantics).
+ * Embedded whitespace is fine; what is not is an embedded newline, a `#'
+ * (or, with `bsemicolon', a `;') at an unquoted position -- either would
+ * terminate the value early on re-parse -- or a trailing unescaped
+ * backslash, which would escape the statement's own newline.
+ */
+static int
+bsdconf_verbatim_ok(const char *s, int bsemicolon)
+{
+ const char *p;
+ int quote = 0;
+ size_t nbs;
+
+ for (p = s; *p != '\0'; p++) {
+ if (*p == '\\') {
+ for (nbs = 0; *p == '\\'; p++)
+ nbs++;
+ if (*p == '\0')
+ return ((nbs & 1) == 0);
+ if (*p == '\n')
+ return (0);
+ continue; /* escaped character; part of the value */
+ }
+ if (*p == '\n')
+ return (0);
+ if (*p == '"')
+ quote = !quote;
+ else if (!quote &&
+ (*p == '#' || (bsemicolon && *p == ';')))
+ return (0);
+ }
+ return (1);
+}
+
+/*
+ * Produce the textual value to be written for `option', following the value
+ * model documented in bsdconf.h: the value is always taken from value.str and
+ * the type governs only whether quoting/escaping is applied.
+ *
+ * With `quote_always' (loader.conf(5) semantics) every value is enclosed in
+ * double-quotes with embedded quotes and backslashes escaped. With `unquoted'
+ * (make.conf(5) semantics) values are emitted verbatim -- embedded
+ * whitespace included, since the value runs to the end of the line -- and a
+ * value that could not round-trip verbatim (embedded newline or comment
+ * marker) is rejected (errno = EINVAL) rather than silently corrupting the
+ * file. Otherwise (sysctl.conf(5) and generic semantics), values are quoted
+ * only when required.
+ *
+ * Returns a newly allocated NUL-terminated string (which the caller must
+ * free) or NULL (with errno set) on failure.
+ */
+static char *
+bsdconf_format_value(const struct bsdconf_option *option, int unquoted,
+ int quote_always, int bsemicolon)
+{
+ const char *s;
+ char *out;
+ char *q;
+ size_t extra;
+ size_t i;
+ size_t slen;
+
+ if (option->type == BSDCONF_TYPE_NONE)
+ return (strdup(""));
+
+ s = option->value.str != NULL ? option->value.str : "";
+
+ if (unquoted) {
+ if (!bsdconf_verbatim_ok(s, bsemicolon)) {
+ errno = EINVAL;
+ return (NULL);
+ }
+ return (strdup(s));
+ }
+
+ if (!quote_always) {
+ if (option->type == BSDCONF_TYPE_INT ||
+ option->type == BSDCONF_TYPE_UINT ||
+ option->type == BSDCONF_TYPE_BOOL)
+ return (strdup(s));
+ if (!bsdconf_needs_quote(s, bsemicolon))
+ return (strdup(s));
+ }
+
+ /* Count the characters that require a backslash escape */
+ slen = strlen(s);
+ extra = 0;
+ for (i = 0; i < slen; i++)
+ if (s[i] == '"' || s[i] == '\\')
+ extra++;
+
+ /* Enclosing quotes plus escapes plus terminator */
+ if ((out = malloc(slen + extra + 3)) == NULL)
+ return (NULL);
+ q = out;
+ *q++ = '"';
+ for (i = 0; i < slen; i++) {
+ if (s[i] == '"' || s[i] == '\\')
+ *q++ = '\\';
+ *q++ = s[i];
+ }
+ *q++ = '"';
+ *q = '\0';
+ return (out);
+}
+
+/*
+ * Test whether a SET_VALUE/CHECK value is considered "empty" for the
+ * purposes of BSDCONF_PUT_ALLOW_EMPTY.
+ */
+static int
+bsdconf_value_empty(const struct bsdconf_option *option)
+{
+
+ return (option->type != BSDCONF_TYPE_NONE &&
+ (option->value.str == NULL || option->value.str[0] == '\0'));
+}
+
+/*
+ * Match the directive spanning buf[start, end) against the directive of a
+ * bsdconf_option. Comparison is exact (whole-token); unlike bsdconf_parse(),
+ * bsdconf_put() does not glob-match, since a pattern is not a writable
+ * target.
+ */
+static int
+bsdconf_dir_matches(const char *buf, size_t start, size_t end,
+ const char *directive, int case_sensitive)
+{
+ size_t len = end - start;
+
+ if (strlen(directive) != len)
+ return (0);
+ if (case_sensitive)
+ return (strncmp(buf + start, directive, len) == 0);
+ return (strncasecmp(buf + start, directive, len) == 0);
+}
+
+/*
+ * Tokenize the statement beginning at buf[*ip], advancing *ip past it and
+ * filling in `st'. The scan mirrors bsdconf_fparse() exactly (comment,
+ * quote, operator, and backslash-escape handling included) so that a file
+ * written by bsdconf_put() re-parses identically. Returns 1 if a statement
+ * was found, or 0 at end of input.
+ */
+static int
+bsdconf_scan(const char *buf, size_t len, size_t *ip, uint32_t *linep,
+ int bequals, int bsemicolon, int strict_equals, int operator_equals,
+ struct bsdconf_stmt *st)
+{
+ size_t i = *ip;
+ size_t j;
+ uint32_t line = *linep;
+ int comment = 0;
+ int novalue = 0;
+ int quote;
+
+ /* Skip whitespace and comments to the beginning of a directive */
+ st->line_start = i;
+ while (i < len && (isspace((unsigned char)buf[i]) || buf[i] == '#' ||
+ comment || (bsemicolon && buf[i] == ';'))) {
+ if (buf[i] == '#')
+ comment = 1;
+ else if (buf[i] == '\n') {
+ comment = 0;
+ line++;
+ st->line_start = i + 1;
+ }
+ i++;
+ }
+ if (i >= len) {
+ *ip = i;
+ *linep = line;
+ return (0);
+ }
+
+ st->line = line;
+ st->dir_start = i;
+ st->have_equals = 0;
+ st->op = BSDCONF_OP_DEFAULT;
+ st->eq = 0;
+
+ /* Find the end of the directive */
+ while (i < len) {
+ if (isspace((unsigned char)buf[i]))
+ break;
+ if (bequals && buf[i] == '=') {
+ st->have_equals = 1;
+ break;
+ }
+ if (bsemicolon && buf[i] == ';')
+ break;
+ i++;
+ }
+ st->dir_end = i;
+ st->op_start = i;
+
+ /*
+ * Split a make(1)-style operator (`+=' `?=' `:=' `!=') off the tail
+ * of the directive if requested; the operator character rode along
+ * with the directive because only the `=' terminates the scan.
+ */
+ if (st->have_equals) {
+ st->eq = i;
+ st->op = BSDCONF_OP_ASSIGN;
+ if (operator_equals && st->dir_end - st->dir_start > 1) {
+ switch (buf[st->dir_end - 1]) {
+ case '+': st->op = BSDCONF_OP_APPEND; break;
+ case '?': st->op = BSDCONF_OP_COND; break;
+ case ':': st->op = BSDCONF_OP_EXPAND; break;
+ case '!': st->op = BSDCONF_OP_SHELL; break;
+ }
+ if (st->op != BSDCONF_OP_ASSIGN) {
+ st->dir_end--;
+ st->op_start = st->dir_end;
+ }
+ }
+ }
+
+ /* Step over an `=' acting as the directive terminator */
+ if (bequals && i < len && buf[i] == '=') {
+ i++;
+ if (strict_equals && i < len &&
+ isspace((unsigned char)buf[i]) && buf[i] != '\n')
+ novalue = 1; /* strict `=' then space: no value */
+ }
+
+ /* Advance across separating whitespace to the value */
+ if (!novalue && !(bsemicolon && i < len && buf[i] == ';') &&
+ !(strict_equals && i < len && buf[i] == '=')) {
+ while (i < len && isspace((unsigned char)buf[i]) &&
+ buf[i] != '\n')
+ i++;
+ }
+
+ /* Consume an `=' surrounded by whitespace (non-strict) */
+ if (!novalue && i < len && bequals && buf[i] == '=' &&
+ !strict_equals) {
+ st->have_equals = 1;
+ st->eq = i;
+ if (st->op == BSDCONF_OP_DEFAULT)
+ st->op = BSDCONF_OP_ASSIGN;
+ i++;
+ while (i < len && isspace((unsigned char)buf[i]) &&
+ buf[i] != '\n')
+ i++;
+ }
+
+ /* A directive with no value */
+ if (novalue || i >= len || buf[i] == '\n' || buf[i] == '#' ||
+ (bsemicolon && buf[i] == ';')) {
+ st->have_value = 0;
+ st->val_start = st->val_end = i;
+ } else {
+ st->have_value = 1;
+ st->val_start = i;
+ quote = 0;
+ while (i < len) {
+ char c = buf[i];
+
+ if (c != '"' && c != '#' && c != '\n' &&
+ (!bsemicolon || c != ';')) {
+ i++;
+ continue;
+ }
+
+ /* Count the backslashes immediately preceding `c' */
+ j = i;
+ while (j > st->val_start && buf[j - 1] == '\\')
+ j--;
+
+ if (((i - j) & 1) == 0) { /* not escaped */
+ if (c == '"') {
+ quote = !quote;
+ i++;
+ continue;
+ }
+ if (c == '#') {
+ if (!quote)
+ break;
+ i++;
+ continue;
+ }
+ if (c == '\n')
+ break;
+ if (c == ';') {
+ if (!quote && bsemicolon)
+ break;
+ i++;
+ continue;
+ }
+ } else { /* escaped: part of the value */
+ if (c == '\n')
+ line++;
+ i++;
+ continue;
+ }
+ }
+ /* Trim trailing whitespace from the value */
+ st->val_end = i;
+ while (st->val_end > st->val_start &&
+ isspace((unsigned char)buf[st->val_end - 1]))
+ st->val_end--;
+ }
+
+ /* Record the terminator position and the end of the physical line */
+ st->term = i;
+ st->line_end = i;
+ while (st->line_end < len && buf[st->line_end] != '\n')
+ st->line_end++;
+ if (st->line_end < len)
+ st->line_end++;
+
+ *ip = i;
+ *linep = line;
+ return (1);
+}
+
+/*
+ * Search for a config option (struct bsdconf_option) in the array of config
+ * options and stage the value of the struct whose directive matches the
+ * given parameter. On success, returns 1, otherwise 0.
+ */
+int
+bsdconf_set_option(struct bsdconf_option options[], const char *directive,
+ union bsdconf_value *value)
+{
+ uint32_t n;
+
+ /* Check arguments */
+ if (options == NULL || directive == NULL || value == NULL)
+ return (0);
+
+ /* Loop through the array, staging the first match */
+ for (n = 0; options[n].directive != NULL; n++) {
+ if (strcmp(options[n].directive, directive) == 0) {
+ options[n].value = *value;
+ return (1);
+ }
+ }
+
+ return (0);
+}
+
+/*
+ * Rewrite the configuration file at `path', applying the per-directive
+ * actions described by the array of config options (first argument). Each
+ * option's action is one of:
+ *
+ * BSDCONF_ACTION_SET_VALUE set the directive to option->value.str,
+ * editing it in place if present or appending
+ * it to the file if absent;
+ * BSDCONF_ACTION_REMOVE delete the directive from the file;
+ * BSDCONF_ACTION_CHECK report (via option->result) whether the
+ * current value differs from option->value,
+ * without modifying the file.
+ *
+ * Comments, blank lines, statement ordering, and the formatting of untouched
+ * statements are preserved. With BSDCONF_OPERATOR_EQUALS, a non-default
+ * option->op both matches and rewrites the statement's assignment operator
+ * (make.conf(5) `+=' et al.). For each processed option, option->result is
+ * set to a bitmask of BSDCONF_DIRECTIVE_FOUND, BSDCONF_VALUE_CHANGED,
+ * BSDCONF_DIRECTIVE_ADDED, and BSDCONF_DIRECTIVE_REMOVED, and option->line
+ * is set to the line of the first match (if found).
+ *
+ * The file is replaced atomically: output is written to a temporary file in
+ * the same directory, flushed to disk with fsync(2), given the mode (and, if
+ * permitted, the ownership) of the original, and then renamed over it. A
+ * crash mid-transaction leaves the original untouched.
+ *
+ * Returns zero on success; otherwise returns -1 and errno should be
+ * consulted.
+ */
+int
+bsdconf_put(struct bsdconf_option options[], const char *path,
+ uint16_t processing_options, uint16_t put_options)
+{
+ int backup;
+ int bequals;
+ int bsemicolon;
+ int case_sensitive;
+ int emptyok;
+ int nodup;
+ int operator_equals;
+ int quote_always;
+ int require_equals;
+ int strict_equals;
+ int unquoted;
+ int dirfd = -1;
+ int fd = -1;
+ int last_ch = -1; /* last byte emitted, or -1 if none */
+ int opchange;
+ int rv = -1;
+ int saved_errno;
+ int tmpfd = -1;
+ char *buf = NULL;
+ char *slash;
+ char *val = NULL;
+ const char *sep;
+ size_t buflen = 0;
+ size_t i;
+ size_t n;
+ size_t vlen;
+ size_t wc; /* write cursor: next unemitted byte of buf */
+ uint32_t line = 1;
+ struct bsdconf_option *option;
+ struct bsdconf_stmt st;
+ struct stat sb;
+ char rpath[PATH_MAX];
+ char tpath[PATH_MAX];
+
+ /* Sanity check the arguments */
+ if (options == NULL || path == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Nothing to unlink until mkstemp(3) succeeds (see cleanup) */
+ tpath[0] = '\0';
+
+ /* Decode processing options */
+ bequals = (processing_options & BSDCONF_BREAK_ON_EQUALS) != 0;
+ bsemicolon = (processing_options & BSDCONF_BREAK_ON_SEMICOLON) != 0;
+ case_sensitive = (processing_options & BSDCONF_CASE_SENSITIVE) != 0;
+ operator_equals = (processing_options & BSDCONF_OPERATOR_EQUALS) != 0;
+ require_equals = (processing_options & BSDCONF_REQUIRE_EQUALS) != 0;
+ strict_equals = (processing_options & BSDCONF_STRICT_EQUALS) != 0;
+
+ /* Decode put options */
+ backup = (put_options & BSDCONF_PUT_BACKUP) != 0;
+ emptyok = (put_options & BSDCONF_PUT_ALLOW_EMPTY) != 0;
+ nodup = (put_options & BSDCONF_PUT_NO_DUPLICATES) != 0;
+ quote_always = (put_options & BSDCONF_PUT_QUOTE_ALWAYS) != 0;
+ unquoted = (put_options & BSDCONF_PUT_UNQUOTED) != 0;
+
+ /* Quoting directives are mutually exclusive */
+ if (unquoted && quote_always) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Reset per-option results and reject empty values up front */
+ for (n = 0; options[n].directive != NULL; n++) {
+ options[n].result = 0;
+ options[n].line = 0;
+ if (!emptyok && options[n].action == BSDCONF_ACTION_SET_VALUE &&
+ bsdconf_value_empty(&options[n])) {
+ errno = EINVAL;
+ return (-1);
+ }
+ }
+
+ /* Resolve the file path */
+ if (realpath(path, rpath) == NULL)
+ return (-1);
+
+ /*
+ * Open the original for reading. Only a regular file can be
+ * atomically replaced, so anything else is rejected below;
+ * O_NONBLOCK makes that check reachable (it is a no-op for regular
+ * files, but without it opening a fifo blocks awaiting a writer).
+ */
+ if ((fd = open(rpath, O_RDONLY | O_NONBLOCK)) < 0)
+ return (-1);
+ if (fstat(fd, &sb) != 0)
+ goto cleanup;
+ if (!S_ISREG(sb.st_mode)) {
+ errno = EINVAL;
+ goto cleanup;
+ }
+
+ /* Slurp the original into memory */
+ if ((buf = bsdconf_readfile(fd, (size_t)sb.st_size, &buflen)) == NULL)
+ goto cleanup;
+
+ /* Build the temporary file path in the target's own directory */
+ if ((slash = strrchr(rpath, '/')) != NULL) {
+ if (snprintf(tpath, sizeof(tpath), "%.*s/.bsdconf.XXXXXXXXXX",
+ (int)(slash - rpath), rpath) >= (int)sizeof(tpath)) {
+ errno = ENAMETOOLONG;
+ goto cleanup;
+ }
+ } else if (snprintf(tpath, sizeof(tpath), ".bsdconf.XXXXXXXXXX") >=
+ (int)sizeof(tpath)) {
+ errno = ENAMETOOLONG;
+ goto cleanup;
+ }
+ if ((tmpfd = mkstemp(tpath)) == -1) {
+ tpath[0] = '\0'; /* nothing to unlink */
+ goto cleanup;
+ }
+
+ /* Give the temporary file the original's mode (and owner if able) */
+ if (fchmod(tmpfd, sb.st_mode & ALLPERMS) != 0)
+ goto cleanup;
+ (void)fchown(tmpfd, sb.st_uid, sb.st_gid); /* best effort */
+
+ /*
+ * Walk the original statement by statement. Bytes are emitted to the
+ * temporary file in order via the write cursor `wc'; a matched
+ * statement diverts around the region it edits or removes.
+ */
+ wc = 0;
+ i = 0;
+ while (bsdconf_scan(buf, buflen, &i, &line, bequals, bsemicolon,
+ strict_equals, operator_equals, &st)) {
+ /* Statements without an `=' are unwritable targets */
+ if (require_equals && !st.have_equals)
+ continue;
+
+ /* Locate the option (if any) matching this directive */
+ option = NULL;
+ for (n = 0; options[n].directive != NULL; n++) {
+ if (bsdconf_dir_matches(buf, st.dir_start, st.dir_end,
+ options[n].directive, case_sensitive)) {
+ option = &options[n];
+ break;
+ }
+ }
+ if (option == NULL)
+ continue; /* untouched; bytes flushed later */
+
+ /* Enforce the no-duplicates policy */
+ if (nodup && (option->result & BSDCONF_DIRECTIVE_FOUND) != 0) {
+ errno = EEXIST;
+ goto cleanup;
+ }
+ if ((option->result & BSDCONF_DIRECTIVE_FOUND) == 0)
+ option->line = st.line;
+ option->result |= BSDCONF_DIRECTIVE_FOUND;
+
+ if (option->action == BSDCONF_ACTION_REMOVE) {
+ size_t rm_start;
+ size_t rm_end;
+ size_t k;
+ int done = 0;
+ int first_on_line = 1;
+
+ /*
+ * Is this the first statement on its physical line?
+ * With BSDCONF_BREAK_ON_SEMICOLON an earlier
+ * statement may precede it on the same line.
+ */
+ for (k = st.line_start; k < st.dir_start; k++)
+ if (!isspace((unsigned char)buf[k])) {
+ first_on_line = 0;
+ break;
+ }
+
+ /*
+ * If a further statement follows on the same line
+ * (terminator is a semicolon with a directive after
+ * it), drop this statement and the trailing
+ * separator, leaving the neighbour intact.
+ */
+ if (bsemicolon && st.term < buflen &&
+ buf[st.term] == ';') {
+ size_t f = st.term + 1;
+
+ while (f < buflen &&
+ isspace((unsigned char)buf[f]) &&
+ buf[f] != '\n')
+ f++;
+ if (f < buflen && buf[f] != '\n' &&
+ buf[f] != '#') {
+ rm_start = st.dir_start;
+ rm_end = f;
+ done = 1;
+ }
+ }
+
+ if (!done && first_on_line) {
+ /* Alone on the line: drop the whole line */
+ rm_start = st.line_start;
+ rm_end = st.line_end;
+ } else if (!done) {
+ /*
+ * Last on a shared line: drop the preceding
+ * separator (whitespace, `;', whitespace)
+ * along with this statement, keeping the
+ * terminator.
+ */
+ size_t s = st.dir_start;
+
+ while (s > st.line_start &&
+ isspace((unsigned char)buf[s - 1]))
+ s--;
+ if (s > st.line_start && buf[s - 1] == ';')
+ s--;
+ while (s > st.line_start &&
+ isspace((unsigned char)buf[s - 1]))
+ s--;
+ rm_start = s;
+ rm_end = st.term;
+ }
+
+ /* Never rewind before already-emitted bytes */
+ if (rm_start < wc)
+ rm_start = wc;
+ if (bsdconf_emit(tmpfd, buf + wc, rm_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ if (rm_end > wc)
+ wc = rm_end;
+ option->result |= BSDCONF_DIRECTIVE_REMOVED;
+ continue;
+ }
+
+ /* SET_VALUE and CHECK both need the formatted value */
+ free(val);
+ if ((val = bsdconf_format_value(option, unquoted,
+ quote_always, bsemicolon)) == NULL)
+ goto cleanup;
+ vlen = strlen(val);
+
+ /* Does the assignment operator need to be rewritten? */
+ opchange = operator_equals &&
+ option->op != BSDCONF_OP_DEFAULT && st.have_equals &&
+ st.op != option->op;
+
+ /* Compare against the value currently in the file */
+ n = st.val_end - st.val_start;
+ if (!opchange && vlen == n && (n == 0 ||
+ memcmp(val, buf + st.val_start, n) == 0)) {
+ /* Unchanged; nothing to do for either action */
+ continue;
+ }
+
+ if (option->action == BSDCONF_ACTION_CHECK) {
+ option->result |= BSDCONF_VALUE_CHANGED;
+ continue;
+ }
+
+ /* BSDCONF_ACTION_SET_VALUE and an edit is required */
+ if (opchange) {
+ /* Rewrite the operator token in place */
+ if (bsdconf_emit(tmpfd, buf + wc, st.op_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ if (bsdconf_emit(tmpfd, bsdconf_op_token(option->op),
+ strlen(bsdconf_op_token(option->op)),
+ &last_ch) != 0)
+ goto cleanup;
+ wc = st.eq + 1;
+ }
+
+ if (st.have_value) {
+ /* Replace the existing value in place */
+ if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0)
+ goto cleanup;
+ wc = st.val_end;
+ } else if (option->type != BSDCONF_TYPE_NONE) {
+ /* Insert a value onto a value-less directive */
+ if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ /* Supply a separator unless one already precedes */
+ if (st.val_start == wc ||
+ (buf[st.val_start - 1] != '=' &&
+ !isspace((unsigned char)buf[st.val_start - 1]))) {
+ sep = st.have_equals || require_equals ||
+ bequals ? "=" : " ";
+ if (bsdconf_emit(tmpfd, sep, 1,
+ &last_ch) != 0)
+ goto cleanup;
+ }
+ if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0)
+ goto cleanup;
+ wc = st.val_start;
+ }
+ option->result |= BSDCONF_VALUE_CHANGED;
+ }
+
+ /* Flush the tail of the original */
+ if (bsdconf_emit(tmpfd, buf + wc, buflen - wc, &last_ch) != 0)
+ goto cleanup;
+
+ /*
+ * Append any SET_VALUE directives that were not found, and finalize
+ * the result flags for CHECK directives that were absent.
+ */
+ for (n = 0; options[n].directive != NULL; n++) {
+ option = &options[n];
+ if ((option->result & BSDCONF_DIRECTIVE_FOUND) != 0)
+ continue;
+ if (option->action == BSDCONF_ACTION_CHECK) {
+ /* Absent means it differs from the desired value */
+ option->result |= BSDCONF_VALUE_CHANGED;
+ continue;
+ }
+ if (option->action != BSDCONF_ACTION_SET_VALUE)
+ continue; /* REMOVE of an absent directive: no-op */
+
+ /* Ensure the emitted output ends with a newline first */
+ if (last_ch != -1 && last_ch != '\n') {
+ if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0)
+ goto cleanup;
+ }
+
+ if (bsdconf_emit(tmpfd, option->directive,
+ strlen(option->directive), &last_ch) != 0)
+ goto cleanup;
+ if (option->type != BSDCONF_TYPE_NONE) {
+ free(val);
+ if ((val = bsdconf_format_value(option, unquoted,
+ quote_always, bsemicolon)) == NULL)
+ goto cleanup;
+ if (operator_equals &&
+ option->op != BSDCONF_OP_DEFAULT)
+ sep = bsdconf_op_token(option->op);
+ else
+ sep = (bequals || require_equals) ? "=" : " ";
+ if (bsdconf_emit(tmpfd, sep, strlen(sep),
+ &last_ch) != 0)
+ goto cleanup;
+ if (bsdconf_emit(tmpfd, val, strlen(val),
+ &last_ch) != 0)
+ goto cleanup;
+ }
+ if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0)
+ goto cleanup;
+ option->result |=
+ BSDCONF_DIRECTIVE_ADDED | BSDCONF_VALUE_CHANGED;
+ }
+
+ /* Optionally back up the original before replacing it */
+ if (backup) {
+ char bpath[PATH_MAX];
+ int bfd;
+
+ if (snprintf(bpath, sizeof(bpath), "%s.bak", rpath) >=
+ (int)sizeof(bpath)) {
+ errno = ENAMETOOLONG;
+ goto cleanup;
+ }
+ /*
+ * O_NOFOLLOW: refuse to follow a planted symbolic link
+ * lest the backup clobber whatever it points at.
+ */
+ if ((bfd = open(bpath, O_WRONLY | O_CREAT | O_TRUNC |
+ O_NOFOLLOW, sb.st_mode & ALLPERMS)) < 0)
+ goto cleanup;
+ if (bsdconf_writeall(bfd, buf, buflen) != 0) {
+ saved_errno = errno;
+ close(bfd);
+ errno = saved_errno;
+ goto cleanup;
+ }
+ if (close(bfd) != 0)
+ goto cleanup;
+ }
+
+ /* Commit: flush to disk, then atomically replace the original */
+ if (fsync(tmpfd) != 0)
+ goto cleanup;
+ if (close(tmpfd) != 0) {
+ tmpfd = -1;
+ goto cleanup;
+ }
+ tmpfd = -1;
+ if (rename(tpath, rpath) != 0)
+ goto cleanup;
+ tpath[0] = '\0'; /* renamed away; nothing to unlink */
+
+ /* Best-effort: persist the directory entry change */
+ if ((slash = strrchr(rpath, '/')) != NULL) {
+ char dpath[PATH_MAX];
+ size_t dlen = (size_t)(slash - rpath);
+
+ if (dlen == 0)
+ dlen = 1; /* the root directory */
+ if (dlen < sizeof(dpath)) {
+ memcpy(dpath, rpath, dlen);
+ dpath[dlen] = '\0';
+ if ((dirfd = open(dpath, O_RDONLY)) >= 0) {
+ (void)fsync(dirfd);
+ close(dirfd);
+ dirfd = -1;
+ }
+ }
+ }
+
+ rv = 0;
+
+cleanup:
+ saved_errno = errno;
+ if (fd >= 0)
+ close(fd);
+ if (tmpfd >= 0)
+ close(tmpfd);
+ if (dirfd >= 0)
+ close(dirfd);
+ if (rv != 0 && tpath[0] != '\0')
+ (void)unlink(tpath); /* discard the incomplete temporary */
+ free(buf);
+ free(val);
+ errno = saved_errno;
+ return (rv);
+}
diff --git a/lib/libbsdconf/bsdconf_string.c b/lib/libbsdconf/bsdconf_string.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_string.c
@@ -0,0 +1,275 @@
+/*
+ * Copyright (c) 2001-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "bsdconf_internal.h"
+
+/*
+ * Counts the number of occurrences of one string that appear in the source
+ * string. Return value is the total count.
+ *
+ * An example use would be if you need to know how large a block of memory
+ * needs to be for a bsdconf_replaceall() series.
+ */
+unsigned int
+bsdconf_strcount(const char *source, const char *find)
+{
+ const char *p = source;
+ size_t flen;
+ unsigned int n = 0;
+
+ /* Both parameters are required */
+ if (source == NULL || find == NULL)
+ return (0);
+
+ /* Cache the length of find element */
+ flen = strlen(find);
+ if (strlen(source) == 0 || flen == 0)
+ return (0);
+
+ /* Loop until the end of the string */
+ while (*p != '\0') {
+ if (strncmp(p, find, flen) == 0) { /* found an instance */
+ p += flen;
+ n++;
+ } else
+ p++;
+ }
+
+ return (n);
+}
+
+/*
+ * Replaces all occurrences of `find' in `source' with `replace'.
+ *
+ * You should not pass a string constant as the first parameter, it needs to
+ * be a pointer to an allocated block of memory. The block of memory that
+ * source points to should be large enough to hold the result. If the length
+ * of the replacement string is greater than the length of the find string,
+ * the result will be larger than the original source string. To allocate
+ * enough space for the result, use the function bsdconf_strcount() declared
+ * above to determine the number of occurrences and how much larger the block
+ * size needs to be.
+ *
+ * If source is not large enough, the application will crash. The return value
+ * is the length (in bytes) of the result.
+ *
+ * When an error occurs, -1 is returned and the global variable errno is set
+ * accordingly.
+ */
+int
+bsdconf_replaceall(char *source, const char *find, const char *replace)
+{
+ char *p;
+ char *t;
+ char *temp;
+ size_t flen;
+ size_t rlen;
+ size_t slen;
+ uint32_t n = 0;
+
+ errno = 0; /* reset global error number */
+
+ /* Check that we have non-null parameters */
+ if (source == NULL)
+ return (0);
+ if (find == NULL)
+ return (strlen(source));
+
+ /* Cache the length of the strings */
+ slen = strlen(source);
+ flen = strlen(find);
+ rlen = replace ? strlen(replace) : 0;
+
+ /* Cases where no replacements need to be made */
+ if (slen == 0 || flen == 0 || slen < flen)
+ return (slen);
+
+ /* If replace is longer than find, we'll need to create a temp copy */
+ if (rlen > flen) {
+ temp = malloc(slen + 1);
+ if (temp == NULL) /* could not allocate memory */
+ return (-1);
+ memcpy(temp, source, slen + 1);
+ } else
+ temp = source;
+
+ /* Reconstruct the string with the replacements */
+ p = source; t = temp; /* position elements */
+
+ while (*t != '\0') {
+ if (strncmp(t, find, flen) == 0) {
+ /* found an occurrence */
+ for (n = 0; replace && replace[n]; n++)
+ *p++ = replace[n];
+ t += flen;
+ } else
+ *p++ = *t++; /* copy character and increment */
+ }
+
+ /* Terminate the string */
+ *p = '\0';
+
+ /* Free the temporary allocated memory */
+ if (temp != source)
+ free(temp);
+
+ /* Return the length of the completed string */
+ return (strlen(source));
+}
+
+/*
+ * Expands escape sequences in a buffer pointed to by `source'. This function
+ * steps through each character, and converts escape sequences such as "\n",
+ * "\r", "\t" and others into their respective meanings.
+ *
+ * You should not pass a string constant or literal to this function or the
+ * program will likely segmentation fault when it tries to modify the data.
+ *
+ * The string length will either shorten or stay the same depending on whether
+ * any escape sequences were converted but the amount of memory allocated does
+ * not change. The rewrite is done in place: because every escape sequence
+ * consumes at least as many input bytes as it produces, the output cursor
+ * never overtakes the input cursor, so the two may safely share the buffer.
+ *
+ * Interpreted sequences are:
+ *
+ * \0NNN character with octal value NNN (0 to 3 digits)
+ * \N character with octal value N (0 thru 7)
+ * \a alert (BEL)
+ * \b backspace
+ * \f form feed
+ * \n new line
+ * \r carriage return
+ * \t horizontal tab
+ * \v vertical tab
+ * \xNN byte with hexadecimal value NN (1 to 2 digits)
+ *
+ * All other sequences are unescaped (ie. '\"' and '\#').
+ */
+void
+bsdconf_strexpand(char *source)
+{
+ uint8_t c;
+ char *chr;
+ char *pos;
+ char d[4];
+
+ /* Initialize position elements */
+ pos = chr = source;
+
+ /*
+ * Loop until we hit the end of the input. The stop condition must
+ * track the input cursor (chr): expanding an escape advances chr
+ * ahead of the output cursor (pos), so once the two diverge *pos
+ * no longer reflects where the input terminates.
+ */
+ while (*chr != '\0') {
+ if (*chr != '\\') {
+ *pos = *chr; /* copy character to current offset */
+ pos++;
+ chr++;
+ continue;
+ }
+
+ /*
+ * A backslash at the very end of the string escapes nothing
+ * (there is no next character); emit it verbatim and stop
+ * rather than read past the terminator.
+ */
+ if (*(chr + 1) == '\0') {
+ *pos++ = *chr++;
+ break;
+ }
+
+ /* Replace the backslash with the correct character */
+ switch (*++chr) {
+ case 'a': *pos = '\a'; break; /* bell/alert (BEL) */
+ case 'b': *pos = '\b'; break; /* backspace */
+ case 'f': *pos = '\f'; break; /* form feed */
+ case 'n': *pos = '\n'; break; /* new line */
+ case 'r': *pos = '\r'; break; /* carriage return */
+ case 't': *pos = '\t'; break; /* horizontal tab */
+ case 'v': *pos = '\v'; break; /* vertical tab */
+ case 'x': /* hex value (1 to 2 digits)(\xNN) */
+ d[2] = '\0'; /* pre-terminate the string */
+
+ /* verify next two characters are hex */
+ d[0] = isxdigit(*(chr+1)) ? *++chr : '\0';
+ if (d[0] != '\0')
+ d[1] = isxdigit(*(chr+1)) ? *++chr : '\0';
+
+ /* convert the characters to decimal */
+ c = (uint8_t)strtoul(d, 0, 16);
+
+ /* assign the converted value */
+ *pos = (c != 0 || d[0] == '0') ? c : *++chr;
+ break;
+ case '0': /* octal value (0 to 3 digits)(\0NNN) */
+ d[3] = '\0'; /* pre-terminate the string */
+
+ /* verify next three characters are octal */
+ d[0] = (isdigit(*(chr+1)) && *(chr+1) < '8') ?
+ *++chr : '\0';
+ if (d[0] != '\0')
+ d[1] = (isdigit(*(chr+1)) && *(chr+1) < '8') ?
+ *++chr : '\0';
+ if (d[1] != '\0')
+ d[2] = (isdigit(*(chr+1)) && *(chr+1) < '8') ?
+ *++chr : '\0';
+
+ /* convert the characters to decimal */
+ c = (uint8_t)strtoul(d, 0, 8);
+
+ /* assign the converted value */
+ *pos = c;
+ break;
+ default: /* single octal (\0..7) or unknown sequence */
+ if (isdigit(*chr) && *chr < '8') {
+ d[0] = *chr;
+ d[1] = '\0';
+ *pos = (uint8_t)strtoul(d, 0, 8);
+ } else
+ *pos = *chr;
+ }
+
+ /* Increment to next offset, possible next escape sequence */
+ pos++;
+ chr++;
+ }
+
+ /*
+ * Terminate at the (possibly earlier) output cursor. When any
+ * escape was collapsed the string shrank, so the trailing bytes
+ * between pos and chr are now stale and must be cut off here.
+ */
+ *pos = '\0';
+}
+
+/*
+ * Convert a string to lower case. You should not pass a string constant to
+ * this function. Only pass pointers to allocated memory with null terminated
+ * string data.
+ */
+void
+bsdconf_strtolower(char *source)
+{
+ char *p = source;
+
+ if (source == NULL)
+ return;
+
+ while (*p != '\0') {
+ *p = tolower(*p);
+ p++;
+ }
+}
diff --git a/share/mk/bsd.libnames.mk b/share/mk/bsd.libnames.mk
--- a/share/mk/bsd.libnames.mk
+++ b/share/mk/bsd.libnames.mk
@@ -30,6 +30,7 @@
LIBBLOCKLIST?= ${LIBDESTDIR}${LIBDIR_BASE}/libblocklist.a
LIBBLOCKSRUNTIME?= ${LIBDESTDIR}${LIBDIR_BASE}/libBlocksRuntime.a
LIBBLUETOOTH?= ${LIBDESTDIR}${LIBDIR_BASE}/libbluetooth.a
+LIBBSDCONF?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsdconf.a
LIBBSDXML?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsdxml.a
LIBBSM?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsm.a
LIBBSNMP?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsnmp.a
diff --git a/share/mk/src.libnames.mk b/share/mk/src.libnames.mk
--- a/share/mk/src.libnames.mk
+++ b/share/mk/src.libnames.mk
@@ -118,6 +118,7 @@
be \
begemot \
bluetooth \
+ bsdconf \
bsdxml \
bsm \
bsnmp \
diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile
--- a/usr.sbin/Makefile
+++ b/usr.sbin/Makefile
@@ -86,6 +86,7 @@
snapinfo \
spi \
spray \
+ sysconf \
syslogd \
sysrc \
tcpdrop \
diff --git a/usr.sbin/sysconf/Makefile b/usr.sbin/sysconf/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/Makefile
@@ -0,0 +1,9 @@
+PACKAGE= utilities
+PROG= sysconf
+MAN= sysconf.8
+
+CFLAGS+= -I${SRCTOP}/lib/libbsdconf
+
+LIBADD= bsdconf jail
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/sysconf/Makefile.depend b/usr.sbin/sysconf/Makefile.depend
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/Makefile.depend
@@ -0,0 +1,17 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ include \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libbsdconf \
+ lib/libc \
+ lib/libcompiler_rt \
+ lib/libjail \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/usr.sbin/sysconf/sysconf.8 b/usr.sbin/sysconf/sysconf.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf.8
@@ -0,0 +1,732 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd July 6, 2026
+.Dt SYSCONF 8
+.Os
+.Sh NAME
+.Nm sysconf
+.Nd read and modify system configuration files
+.Sh SYNOPSIS
+.Nm
+.Ar target
+.Op Fl AcDeFinNqvx
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file | Fl k Ar module
+.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
+.Ar ...
+.Nm
+.Ar target
+.Op Fl ADeFnNqv
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file | Fl k Ar module
+.Fl a
+.Nm
+.Ar target
+.Op Fl AaDinNq
+.Op Fl j Ar jail | Fl R Ar dir
+.Fl d
+.Op Ar name ...
+.Nm
+.Ar target
+.Op Fl E
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl k Ar module
+.Fl l | L
+.Nm
+.Cm rc
+.Op Ar sysrc_argument ...
+.Sh DESCRIPTION
+The
+.Nm
+utility reads and modifies the system configuration files of the base
+system,
+completing the native configuration trinity alongside
+.Xr sysctl 8
+.Pq kernel state
+and
+.Xr sysrc 8
+.Pq Xr rc.conf 5 .
+.Pp
+The assignment syntax maps directly to
+.Xr sysctl 8 :
+a
+.Ar name
+alone reads the current value of the directive,
+while
+.Ar name Ns = Ns Ar value
+atomically writes it,
+echoing
+.Ql name: old -> new
+in the manner of
+.Xr sysctl 8
+and
+.Xr sysrc 8
+.Pq suppressed by Fl q ;
+a directive not previously set anywhere shows an empty old value.
+For directives whose value is a space-separated list
+.Po
+.Va kld_list Ns -style ;
+see for example
+.Va kernels
+in
+.Xr loader.conf 5
+.Pc ,
+.Ar name Ns += Ns Ar word
+appends each given word not already present and
+.Ar name Ns -= Ns Ar word
+strikes each given word,
+after the fashion of
+.Xr sysrc 8 ;
+striking words from a directive set nowhere is reported like any other
+unknown directive.
+For
+.Xr make 1 Ns -syntax
+targets
+.Ql +=
+is instead the
+.Xr make 1
+append operator,
+written through to the file
+.Pq see below .
+The first positional argument selects the
+.Ar target
+by keyword;
+options are equally accepted before it:
+.Bl -column "keyword" "/etc/sysctl.conf, sysctl.conf.local, sysctl.kld.d/*.conf" -offset indent
+.It Sy keyword Ta Sy files
+.It loader Ta discovered from Pa /boot/defaults/loader.conf No (see below)
+.It sysctl Ta Pa /etc/sysctl.conf , sysctl.conf.local , sysctl.kld.d/*.conf
+.It make Ta Pa /etc/make.conf
+.It src Ta Pa /etc/src.conf
+.It src-env Ta Pa /etc/src-env.conf
+.It rc Ta pass-through to Xr sysrc 8 ; see below
+.It generic Ta none; requires Fl f
+.El
+.Pp
+The
+.Cm rc
+target is a pure pass-through:
+.Xr rc.conf 5
+is
+.Xr sysrc 8 Ns 's
+domain,
+and its feature set is a superset of
+.Nm Ns 's ,
+so no option or argument is validated,
+interpreted,
+or reordered
+.Po
+.Ql --help
+and
+.Ql --version
+included
+.Pc ;
+every argument except the
+.Cm rc
+keyword itself is handed to
+.Xr sysrc 8
+verbatim.
+.Pp
+Most targets are backed by more than one file,
+consulted automatically in the deterministic order the system sources them
+at boot
+.Pq see Xr loader.conf 5 and Xr sysctl.conf 5 .
+The
+.Ql loader
+target's file list is not hardcoded:
+just as the boot loader itself hardcodes only
+.Pa /boot/defaults/loader.conf ,
+.Nm
+reads that file and chases the
+.Va loader_conf_files ,
+.Va loader_conf_dirs ,
+and
+.Va local_loader_conf_files
+directives it defines
+.Pq re-reading the lists after every file, since any file may revise them ,
+so customized layouts that redirect
+.Va loader_conf_files
+elsewhere are honored automatically.
+On a stock system this resolves to
+.Pa /boot/device.hints ,
+.Pa /boot/loader.conf ,
+.Pa /boot/loader.conf.d/*.conf ,
+and
+.Pa /boot/loader.conf.local ,
+in that order;
+when the defaults file is missing,
+that stock list is assumed.
+A directive in a later file overrides the same directive in an earlier one,
+so the last file listing a directive is the authoritative source of its
+value.
+For a target with a defaults file,
+the boot-time consumer sources the defaults before everything else,
+and so does
+.Nm :
+a directive no conf file mentions still reads back its default
+.Pq the value that does, in fact, take effect at boot ,
+with
+.Fl F
+truthfully naming the defaults file as its source.
+The defaults file is nevertheless read-only bookkeeping:
+it is never chosen as a write target,
+never satisfies a removal,
+and is never listed by
+.Fl l
+or
+.Fl L
+.Pq which enumerate only files Nm may modify .
+Reads report the authoritative value;
+writes are applied to the authoritative file
+.Pq so the change survives a reboot
+or appended to the target's default write file
+.Pq the first modifiable file listed
+when the directive is set nowhere or only in the read-only defaults;
+removals are applied to every file listing the directive,
+lest deleting the authoritative definition merely unmask an earlier one.
+For the
+.Ql sysctl
+target,
+.Fl k Ar module
+additionally consults the module-specific drop-in
+.Pa /etc/sysctl.kld.d/ Ns Ar module Ns Pa .conf
+applied by
+.Xr rc.subr 8
+when the module loads.
+.Pp
+An arbitrary configuration file may instead be selected with
+.Fl f ,
+which overrides
+.Em which
+file is consulted and modified
+.Pq the named file alone; multi-file processing is disabled
+but never
+.Em how :
+the format is always stated explicitly by the
+.Ar target
+keyword and is never inferred from the file.
+This composes freely,
+so a
+.Xr loader.conf 5 Ns -formatted
+file may be staged anywhere
+.Pq Ql sysconf loader -f /tmp/loader.conf.new ...
+and,
+conversely,
+any format may be deliberately applied to any file.
+The
+.Ql generic
+target
+.Pq quote values only when required to round-trip
+exists for files matching no known format and,
+having no default files of its own,
+always requires
+.Fl f .
+.Pp
+The
+.Nm
+utility understands the syntax particulars of each target:
+values written to
+.Xr loader.conf 5
+are always enclosed in quotes
+.Pq quoted or unquoted input is accepted
+with no whitespace around the equals sign,
+values written to
+.Xr sysctl.conf 5
+are quoted only when the value requires it
+.Pq embedded whitespace ,
+and
+.Pa make.conf
+.Po
+along with its
+.Xr make 1 Ns -syntax
+siblings
+.Pa src.conf
+and
+.Pa src-env.conf
+.Pc
+assignment modifiers are preserved and may be requested by suffixing the
+name with the modifier character,
+as in
+.Ql CFLAGS+=-DDEBUG .
+.Pp
+When writing to the
+.Ql sysctl
+target of the running system,
+each
+.Ar name
+is validated against the kernel's sysctl tree first:
+an unknown OID or an OID that cannot be set at run time is reported and
+skipped,
+while the remaining valid assignments are still applied.
+In particular,
+a loader-only tunable is reported as requiring the
+.Ql loader
+target instead.
+Validation is relaxed for unknown OIDs under
+.Fl i
+or
+.Fl k
+.Pq the module providing the OID may not be loaded
+and skipped entirely under
+.Fl R ,
+where the target system's kernel is not the running kernel.
+.Pp
+Files are never modified in place:
+updates are streamed to a temporary file which is flushed to stable storage
+and atomically renamed over the target,
+so that a crash or power loss mid-write cannot truncate or corrupt the
+configuration
+.Pq see Xr bsdconf 3 .
+.Pp
+Targets carrying a body of system-supplied defaults
+.Po
+presently
+.Ql loader ,
+whose defaults file is
+.Pa /boot/defaults/loader.conf
+.Pc
+additionally support querying those defaults:
+.Fl d
+reports directive descriptions,
+.Fl D
+consults the defaults alone,
+and
+.Fl A
+folds the defaults into the sourcing order.
+Attempting any of these against a target with no defaults
+.Po
+.Pa make.conf ,
+.Pa src.conf ,
+and
+.Pa src-env.conf
+have neither a defaults file nor directive descriptions
+.Pc
+is an error;
+the one exception is
+.Ql sysctl -d ,
+whose descriptions come from the running kernel rather than any file.
+.Pp
+The options are as follows:
+.Bl -tag -width indent
+.It Fl A
+Widen dump scope to include directives still at their system default
+.Po
+named reads always reflect the defaults;
+see above
+.Pc .
+With no
+.Ar name
+arguments,
+dumps everything
+.Pq as Fl a , defaults included .
+Combined with
+.Fl D ,
+the scoping of
+.Fl D
+wins and
+.Fl A
+retains only its dump-implying role,
+so
+.Ql -ADd
+describes all defaults and only defaults,
+as in
+.Xr sysrc 8 .
+Only valid for targets with a defaults file.
+.It Fl a
+Dump all configured directives from the target:
+the union of the target's files,
+wherein directives in later files override earlier ones.
+Directives whose value still comes from the defaults file alone are out
+of scope
+.Pq widen with Fl A .
+.It Fl c
+Check mode.
+For each
+.Ar name Ns = Ns Ar value
+argument,
+compare the effective value against
+.Ar value
+without modifying any file
+.Pq with Fl x , check that Ar name is absent .
+Exit with success if no changes would be required,
+else error.
+.It Fl D
+Consult only the target's defaults file,
+reporting the system default value of each
+.Ar name
+.Pq or, with Fl a , dumping every default .
+Defaults are read-only;
+combining
+.Fl D
+with an assignment is an error.
+Only valid for targets with a defaults file.
+.It Fl d
+Show the description of each
+.Ar name
+instead of its value.
+For targets with a defaults file,
+the description is the inline comment trailing the directive's default
+assignment,
+continued across subsequent lines of whitespace followed by a comment
+character;
+a commented-out default
+.Pq e.g., Ql #comconsole_speed=\(dq115200\(dq # Set the ...
+still yields its description.
+For the
+.Ql sysctl
+target,
+descriptions come from the running kernel
+.Pq as with Xr sysctl 8 Fl d .
+Combined with
+.Fl a ,
+.Fl A ,
+or
+.Fl D ,
+dumps the description of every directive in scope
+.Po
+of the configured directives,
+of everything defaults included,
+or of the defaults alone,
+respectively
+.Pc ,
+after the fashion of
+.Xr sysrc 8
+.Fl \&Ad ;
+a directive the defaults never mention prints an empty description.
+.It Fl E
+With
+.Fl l
+or
+.Fl L ,
+list only files that exist on disk.
+.It Fl e
+Print values in
+.Ar name Ns = Ns Ar value
+format,
+suitable for feeding back into the configuration file.
+For the
+.Ql loader
+target the value is quoted.
+.It Fl F
+Show the pathname of the file holding each directive's effective
+.Pq authoritative
+value instead of the value itself.
+For a directive still at its system default,
+this truthfully names the defaults file.
+.It Fl f Ar file
+Operate on
+.Ar file ,
+in the format of the
+.Ar target
+keyword,
+instead of the target's standard files;
+mutually exclusive with
+.Fl k .
+For read operations
+.Ar file
+may be a non-seekable stream
+.Po
+a fifo or
+.Pa /dev/stdin ,
+for example,
+spooled to a temporary automatically
+.Pc ;
+write operations require a regular file.
+A
+.Ar file
+of
+.Ql -
+means standard input
+.Pq a synonym for Pa /dev/stdin ,
+exempt from
+.Fl R .
+.It Fl h , Fl Fl help
+Print a short usage statement
+.Pq or, for the long form, the full option summary
+to stderr and exit.
+.It Fl Fl version
+Print version information to stdout and exit.
+.It Fl i
+Ignore unknown names
+.Pq and unknown sysctl OIDs when writing .
+.It Fl j Ar jail
+Operate within the jail
+.Ar jail
+.Pq name or numeric id ;
+mutually exclusive with
+.Fl R .
+.It Fl k Ar module
+Include the kernel module drop-in file for
+.Ar module
+in the target's file list
+.Pq e.g., Pa /etc/sysctl.kld.d/ Ns Ar module Ns Pa .conf .
+Only valid for targets with a module drop-in directory.
+.It Fl l
+List the pathnames of the files backing the target,
+in sourcing order,
+and exit.
+The read-only defaults file is never listed
+.Pq it is not a file Nm could or would modify .
+.It Fl L
+Like
+.Fl l ,
+but additionally list every drop-in candidate:
+for targets with a module drop-in directory,
+one candidate per loaded kernel module
+.Pq whether or not the file exists yet
+plus any drop-in already on disk.
+The read-only defaults file is likewise never listed.
+.It Fl n
+Show only directive values,
+not their names.
+.It Fl N
+Show only directive names,
+not their values.
+.It Fl q
+Quiet.
+Suppress warnings about unknown directives and the
+.Ql old -> new
+echo of write operations.
+.It Fl R Ar dir
+Operate within the root directory
+.Ar dir
+rather than
+.Pa / ;
+mutually exclusive with
+.Fl j .
+.It Fl v
+Verbose.
+Print the pathname of the configuration file being operated on.
+.It Fl x
+Remove the named directive(s) from every file of the target listing them.
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width "LOADER_DEFAULTS"
+.It Ev LOADER_DEFAULTS
+Defaults file for the
+.Ql loader
+target,
+consulted in place of
+.Pa /boot/defaults/loader.conf
+for file-list discovery and for
+.Fl d ,
+.Fl D ,
+and
+.Fl A
+.Po
+in the vein of
+.Ev RC_DEFAULTS
+in
+.Xr sysrc 8
+.Pc .
+The value is used verbatim
+.Pq not subject to Fl R .
+.El
+.Sh FILES
+.Bl -tag -width "/boot/defaults/loader.conf" -compact
+.It Pa /boot/defaults/loader.conf
+Defaults for the
+.Ql loader
+target and the authority on its file list
+.Pq see Va loader_conf_files .
+.It Pa /boot/device.hints
+Device hints,
+first in the stock
+.Va loader_conf_files
+list.
+.It Pa /boot/loader.conf
+Local boot loader settings;
+the stock write target of the
+.Ql loader
+target.
+.It Pa /boot/loader.conf.d/
+Boot loader drop-in directory.
+.It Pa /boot/loader.conf.local
+Site-local boot loader overrides,
+sourced last.
+.It Pa /etc/sysctl.conf
+Kernel state to set at boot;
+the write target of the
+.Ql sysctl
+target.
+.It Pa /etc/sysctl.conf.local
+Site-local sysctl overrides.
+.It Pa /etc/sysctl.kld.d/
+Module-specific sysctl drop-ins,
+applied when the named kernel module loads
+.Pq see Fl k .
+.It Pa /etc/make.conf
+System-wide
+.Xr make 1
+settings.
+.It Pa /etc/src.conf
+Source tree build options.
+.It Pa /etc/src-env.conf
+Source tree build options read before the environment is finalized.
+.El
+.Sh EXIT STATUS
+.Ex -std
+In check mode
+.Pq Fl c ,
+an exit status of zero means no changes are required.
+.Sh EXAMPLES
+Read the effective value of
+.Va zfs_load
+.Pq from whichever Xr loader.conf 5 file sets it last :
+.Pp
+.Dl sysconf loader zfs_load
+.Pp
+Enable the ZFS module at boot:
+.Pp
+.Dl sysconf loader zfs_load=\(dqYES\(dq
+.Pp
+Raise the maximum number of open files:
+.Pp
+.Dl sysconf sysctl kern.maxfiles=65536
+.Pp
+Show which file authoritatively sets
+.Va vfs.usermount
+for boot:
+.Pp
+.Dl sysconf sysctl -F vfs.usermount
+.Pp
+List the files consulted for the
+.Ql loader
+target
+.Pq as discovered from Pa /boot/defaults/loader.conf :
+.Pp
+.Dl sysconf loader -l
+.Pp
+Show what a directive does,
+straight from the defaults file's comments,
+then dump the description of everything in scope,
+defaults included:
+.Pp
+.Dl sysconf loader -d verbose_loading
+.Dl sysconf loader -Ad
+.Pp
+Show a sysctl OID's description from the running kernel:
+.Pp
+.Dl sysconf sysctl -d kern.maxfiles
+.Pp
+Read the system default of a directive,
+ignoring local configuration,
+then dump every default:
+.Pp
+.Dl sysconf loader -D verbose_loading
+.Dl sysconf loader -aD
+.Pp
+Read a directive's effective value with the defaults folded in
+.Pq useful when the directive is not set locally at all :
+.Pp
+.Dl sysconf loader -A autoboot_delay
+.Pp
+List every
+.Pa sysctl.kld.d
+candidate for the currently loaded kernel modules,
+then narrow the list to those that exist on disk:
+.Pp
+.Dl sysconf sysctl -L
+.Dl sysconf sysctl -LE
+.Pp
+Configure a module-specific sysctl applied when the
+.Ql ipfw
+module loads:
+.Pp
+.Dl sysconf sysctl -k ipfw net.inet.ip.fw.verbose=1
+.Pp
+Maintain a space-separated list,
+appending a word only if absent and then striking it:
+.Pp
+.Dl sysconf loader kernels+=kernel_test
+.Dl sysconf loader kernels-=kernel_test
+.Pp
+Hand an
+.Xr rc.conf 5
+request to
+.Xr sysrc 8
+untouched
+.Pq every argument after the Cm rc keyword passes through verbatim :
+.Pp
+.Dl sysconf rc kld_list+=i915kms
+.Pp
+Append a flag to
+.Va CFLAGS
+in
+.Pa /etc/make.conf
+.Po
+for
+.Xr make 1 Ns -syntax
+targets
+.Ql +=
+is the
+.Xr make 1
+operator,
+not a list edit
+.Pc :
+.Pp
+.Dl sysconf make CFLAGS+=-DDEBUG
+.Pp
+Exclude the LLDB debugger from the next
+.Sy buildworld :
+.Pp
+.Dl sysconf src WITHOUT_LLDB=
+.Pp
+Remove a directive from every file of the
+.Ql sysctl
+target listing it:
+.Pp
+.Dl sysconf -x sysctl vfs.usermount
+.Pp
+Dump the union of everything configured for the
+.Ql loader
+target:
+.Pp
+.Dl sysconf -a loader
+.Pp
+Set a value inside a jail:
+.Pp
+.Dl sysconf -j www sysctl kern.ipc.somaxconn=1024
+.Pp
+Stage a
+.Xr loader.conf 5 Ns -formatted
+file outside
+.Pa /boot ,
+quoted exactly as the boot loader demands:
+.Pp
+.Dl sysconf loader -f /tmp/loader.conf.new zfs_load=\(dqYES\(dq
+.Pp
+Manage a file matching no known format:
+.Pp
+.Dl sysconf generic -f /usr/local/etc/myapp.conf loglevel=debug
+.Sh SEE ALSO
+.Xr bsdconf 3 ,
+.Xr loader.conf 5 ,
+.Xr src.conf 5 ,
+.Xr sysctl.conf 5 ,
+.Xr jail 8 ,
+.Xr sysctl 8 ,
+.Xr sysrc 8
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
+.Sh SECURITY CONSIDERATIONS
+Read-only invocations run inside a
+.Xr capsicum 4
+sandbox:
+the backing files are opened first and the process then relinquishes all
+other capabilities before any file content is parsed.
+Write operations replace files atomically and never in place;
+see the
+.Sx SECURITY CONSIDERATIONS
+section of
+.Xr bsdconf 3
+for the properties of the write transaction.
diff --git a/usr.sbin/sysconf/sysconf.c b/usr.sbin/sysconf/sysconf.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf.c
@@ -0,0 +1,1932 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifdef __FreeBSD__
+#include <sys/param.h>
+#include <sys/capsicum.h>
+#include <sys/jail.h>
+#include <sys/linker.h>
+#include <sys/sysctl.h>
+
+#include <capsicum_helpers.h>
+#include <jail.h>
+#endif
+
+#include <bsdconf.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifndef __unused
+#define __unused /* not all compilers support attributes */
+#endif
+
+#define SYSCONF_VERSION "1.0 Jul-6,2026"
+
+/* getopt(3) optstring; shared by parse_options() and find_target() */
+#define OPTSTRING "AacdDEeFf:hij:k:lLnNqR:vx"
+
+/* Requests to process (one per name argument) */
+struct request {
+ const char *name; /* directive name */
+ const char *newvalue; /* value to write (NULL for reads) */
+ enum bsdconf_op op; /* assignment operator (writes) */
+ char listop; /* `+' append / `-' strike words */
+ char *value; /* value read from file(s) */
+ int found; /* directive seen while parsing */
+ int remove; /* nonzero for `-x' removals */
+ int srcidx; /* conf file that last set the value
+ (authoritative); -1 if unset */
+ unsigned char *infile; /* per-file presence map */
+};
+
+static struct request *reqs; /* set by main() */
+static unsigned int nreqs; /* set by main() */
+
+/* Union of all directives, for `-a' against multi-file targets */
+struct dumpent {
+ char *name; /* directive name */
+ char *value; /* last value seen */
+ int srcidx; /* conf file that last set the value */
+};
+
+static struct dumpent *dumps; /* set by scan_cb() */
+static size_t ndumps; /* set by scan_cb() */
+static size_t dumpsize; /* set by scan_cb() */
+
+/* Resolved target state */
+static char **conf_files; /* ordered backing files */
+static size_t nconf_files;
+static size_t conf_scanidx; /* file being parsed (scan_pass) */
+static size_t write_idx; /* default write target index */
+static int defaults_idx = -1; /* defaults file index in
+ conf_files (read-only,
+ never listed); -1 if none */
+static enum bsdconf_format format = BSDCONF_FORMAT_GENERIC;
+
+/* Extra display information */
+static const char *pgm; /* set to argv[0] by main() */
+static uint8_t check = 0; /* `-c' */
+static uint8_t defaults_only = 0; /* `-D' */
+static uint8_t dump_all = 0; /* `-a' */
+static uint8_t existing_only = 0; /* `-E' */
+static uint8_t ignore_unknown = 0; /* `-i' */
+static uint8_t list_all = 0; /* `-L' */
+static uint8_t list_files = 0; /* `-l' */
+static uint8_t name_only = 0; /* `-N' */
+static uint8_t quiet = 0; /* `-q' */
+static uint8_t remove_mode = 0; /* `-x' */
+static uint8_t show_desc = 0; /* `-d' */
+static uint8_t show_equals = 0; /* `-e' */
+static uint8_t show_file = 0; /* `-F' */
+static uint8_t value_only = 0; /* `-n' */
+static uint8_t verbose = 0; /* `-v' */
+static uint8_t with_defaults = 0; /* `-A' */
+
+/* Option arguments */
+static const char *file = NULL; /* `-f file' */
+static uint8_t file_stdin = 0; /* `-f -' given */
+static const char *jailname = NULL; /* `-j jail' */
+static const char *module = NULL; /* `-k module' */
+static const char *rootdir = ""; /* `-R dir' */
+
+/* Function prototypes for private functions (see style(9)) */
+static void usage(void);
+static void help(void);
+static int find_target(int argc, char *argv[]);
+static void exec_sysrc(int argc, char *argv[], int skip);
+static int parse_options(int argc, char *argv[]);
+static int list_has(const char *list, const char *word, size_t wlen);
+static char *list_merge(const char *current, const char *words, int op);
+static int merge_list_requests(void);
+static void print_pair(const char *path, const char *directive,
+ const char *value);
+static int scan_cb(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value);
+static int scan_pass(int sandbox);
+static void resolve_target(const char *target);
+static void split_request(char *arg, struct request *req, int remove);
+static char *defaults_path(void);
+static void load_descriptions(void);
+static const char *desc_find(const char *name);
+static void print_desc(const char *directive);
+static int do_list(void);
+static int do_reads(void);
+static int do_checks(void);
+static int do_writes(void);
+static int describe_defaults(void);
+#if defined(__FreeBSD__)
+static int describe_sysctl(void);
+static int sysctl_descr(const char *name, char *buf, size_t bufsize);
+static int sysctl_writable(const char *name, int tolerate_unknown);
+#endif
+
+/*
+ * Locate the target keyword in `argv' without consuming or validating
+ * anything: the first argument that is neither an option cluster nor the
+ * argument of one (per OPTSTRING; an unknown option letter is assumed to
+ * take no argument). A `--' ends option scanning as usual. Returns the
+ * argv index of the target, or -1 when there is none. Used before any
+ * getopt(3) processing so that the `rc' pass-through target (see
+ * exec_sysrc() below) can be detected while the command line is still
+ * pristine.
+ */
+static int
+find_target(int argc, char *argv[])
+{
+ int n;
+ char last;
+ const char *p;
+
+ for (n = 1; n < argc; n++) {
+ if (strcmp(argv[n], "--") == 0)
+ return (n + 1 < argc ? n + 1 : -1);
+ if (argv[n][0] == '-' && argv[n][1] != '\0') {
+ /* Skip the argument of an argument-taking option */
+ last = argv[n][strlen(argv[n]) - 1];
+ if ((p = strchr(OPTSTRING, last)) != NULL &&
+ p[1] == ':')
+ n++;
+ continue;
+ }
+ return (n);
+ }
+
+ return (-1);
+}
+
+/*
+ * The `rc' pass-through: rc.conf(5) is sysrc(8)'s domain, and its feature
+ * set is a superset of ours, so no option or argument is validated,
+ * interpreted, or reordered here -- every argument except the target
+ * keyword itself (at argv index `skip') is handed to sysrc(8) verbatim.
+ * Never returns.
+ */
+static void
+exec_sysrc(int argc, char *argv[], int skip)
+{
+ int n;
+ int nargc = 0;
+ char **nargv;
+
+ if ((nargv = calloc((size_t)argc + 1, sizeof(*nargv))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ nargv[nargc++] = (char *)(uintptr_t)"sysrc";
+ for (n = 1; n < argc; n++) {
+ if (n == skip)
+ continue;
+ nargv[nargc++] = argv[n];
+ }
+ nargv[nargc] = NULL;
+
+ execvp("sysrc", nargv);
+ err(EXIT_FAILURE, "sysrc");
+}
+
+/*
+ * Run the getopt(3) loop over `argv', setting the option globals. Called
+ * once for the options preceding the target keyword and once for those
+ * following it. Returns the number of arguments consumed (optind).
+ */
+static int
+parse_options(int argc, char *argv[])
+{
+ int ch;
+
+ while ((ch = getopt(argc, argv, OPTSTRING)) != -1) {
+ switch (ch) {
+ case 'A': /* dump all directives, defaults included */
+ with_defaults = 1;
+ break;
+ case 'a': /* dump all directives */
+ dump_all = 1;
+ break;
+ case 'c': /* check only; do not modify */
+ check = 1;
+ break;
+ case 'd': /* show directive descriptions */
+ show_desc = 1;
+ break;
+ case 'D': /* consult the defaults file only */
+ defaults_only = 1;
+ break;
+ case 'E': /* list existing files only (with -l/-L) */
+ existing_only = 1;
+ break;
+ case 'e': /* show name=value */
+ show_equals = 1;
+ break;
+ case 'F': /* show authoritative file, not value */
+ show_file = 1;
+ break;
+ case 'f': /* explicit file (`-' is standard input) */
+ if (strcmp(optarg, "-") == 0) {
+ file = "/dev/stdin";
+ file_stdin = 1;
+ } else {
+ file = optarg;
+ file_stdin = 0;
+ }
+ break;
+ case 'h': /* help/usage */
+ usage();
+ break;
+ case 'i': /* ignore unknown names */
+ ignore_unknown = 1;
+ break;
+ case 'j': /* jail name or id */
+ jailname = optarg;
+ break;
+ case 'k': /* kernel module drop-in */
+ module = optarg;
+ break;
+ case 'l': /* list backing files */
+ list_files = 1;
+ break;
+ case 'L': /* list all candidate files */
+ list_all = 1;
+ break;
+ case 'n': /* value only */
+ value_only = 1;
+ break;
+ case 'N': /* name only */
+ name_only = 1;
+ break;
+ case 'q': /* quiet */
+ quiet = 1;
+ break;
+ case 'R': /* root dir */
+ rootdir = optarg;
+ break;
+ case 'v': /* verbose */
+ verbose = 1;
+ break;
+ case 'x': /* remove */
+ remove_mode = 1;
+ break;
+ case '?': /* unknown argument (based on optstring) */
+ default: /* unhandled argument (based on switch) */
+ usage();
+ }
+ }
+
+ return (optind);
+}
+
+/*
+ * Print a directive/value pair according to the display flags. `path' is
+ * the configuration file holding the authoritative definition (or NULL).
+ */
+static void
+print_pair(const char *path, const char *directive, const char *value)
+{
+
+ if (show_file)
+ value = path != NULL ? path : "(none)";
+ if (verbose && path != NULL && !show_file)
+ printf("%s: ", path);
+ if (name_only) {
+ puts(directive);
+ return;
+ }
+ if (value_only) {
+ puts(value);
+ return;
+ }
+ if (show_equals && !show_file) {
+ if ((bsdconf_format_put(format) &
+ BSDCONF_PUT_QUOTE_ALWAYS) != 0)
+ printf("%s=\"%s\"\n", directive, value);
+ else
+ printf("%s=%s\n", directive, value);
+ return;
+ }
+ printf("%s: %s\n", directive, value);
+}
+
+/*
+ * Record `directive'/`value' in the dump union (for `-a'). A directive seen
+ * again in a later file overrides the earlier value while retaining its
+ * original position, so the output reflects what the system settles on at
+ * boot. Returns zero on success; -1 (errno set) on allocation failure.
+ */
+static int
+dump_record(const char *directive, const char *value)
+{
+ char *vcopy;
+ size_t n;
+ struct dumpent *tmp;
+
+ if ((vcopy = strdup(value)) == NULL)
+ return (-1);
+ bsdconf_unquote(vcopy);
+
+ for (n = 0; n < ndumps; n++) {
+ if (strcmp(dumps[n].name, directive) != 0)
+ continue;
+ free(dumps[n].value);
+ dumps[n].value = vcopy;
+ dumps[n].srcidx = (int)conf_scanidx;
+ return (0);
+ }
+
+ if (ndumps >= dumpsize) {
+ dumpsize = (dumpsize == 0) ? 64 : dumpsize << 1;
+ tmp = realloc(dumps, dumpsize * sizeof(*dumps));
+ if (tmp == NULL) {
+ free(vcopy);
+ return (-1);
+ }
+ dumps = tmp;
+ }
+ if ((dumps[ndumps].name = strdup(directive)) == NULL) {
+ free(vcopy);
+ return (-1);
+ }
+ dumps[ndumps].value = vcopy;
+ dumps[ndumps].srcidx = (int)conf_scanidx;
+ ndumps++;
+ return (0);
+}
+
+/*
+ * bsdconf_fparse() call-back; record each statement against the requests
+ * (and, with `-a', the dump union). A later assignment overrides an earlier
+ * one -- both within a file and across the ordered file list -- matching
+ * the semantics of the boot loader and of sysctl.conf(5) processing.
+ */
+static int
+scan_cb(struct bsdconf_option *option __unused, uint32_t line __unused,
+ char *directive, char *value)
+{
+ char *copy;
+ unsigned int n;
+
+ for (n = 0; n < nreqs; n++) {
+ if (strcmp(reqs[n].name, directive) != 0)
+ continue;
+ /*
+ * The defaults file is read-only: it informs the effective
+ * value seen by reads, checks, and list edits, but can
+ * never satisfy a removal (nothing there may be removed).
+ */
+ if (reqs[n].remove && (int)conf_scanidx == defaults_idx)
+ continue;
+ if ((copy = strdup(value)) == NULL)
+ return (-1);
+ free(reqs[n].value);
+ reqs[n].value = bsdconf_unquote(copy);
+ reqs[n].found = 1;
+ reqs[n].srcidx = (int)conf_scanidx;
+ reqs[n].infile[conf_scanidx] = 1;
+ }
+
+ if (dump_all && dump_record(directive, value) != 0)
+ return (-1);
+
+ return (0);
+}
+
+/*
+ * Parse every existing backing file in deterministic order, feeding each
+ * statement through scan_cb(). With `sandbox' set (pure read operations),
+ * all descriptors are opened up front and the remainder of the pass runs
+ * inside a Capsicum sandbox on FreeBSD. Returns EXIT_SUCCESS or exits on
+ * hard errors.
+ */
+static int
+scan_pass(int sandbox)
+{
+ int *fds;
+ size_t n;
+ uint16_t processing;
+
+ if ((fds = calloc(nconf_files, sizeof(*fds))) == NULL)
+ err(EXIT_FAILURE, NULL);
+
+ for (n = 0; n < nconf_files; n++) {
+ fds[n] = open(conf_files[n], O_RDONLY);
+ if (fds[n] < 0 && errno != ENOENT)
+ err(EXIT_FAILURE, "%s", conf_files[n]);
+
+ /*
+ * Spool input that cannot seek (a fifo or /dev/stdin named
+ * by `-f') before the sandbox slams shut: the library would
+ * otherwise spool lazily inside bsdconf_fparse(), where
+ * creating the temporary is no longer permitted.
+ */
+ if (fds[n] >= 0 && lseek(fds[n], 0, SEEK_CUR) == -1) {
+ int sfd;
+
+ if (errno != ESPIPE ||
+ (sfd = bsdconf_spool(fds[n])) == -1)
+ err(EXIT_FAILURE, "%s", conf_files[n]);
+ close(fds[n]);
+ fds[n] = sfd;
+ }
+ }
+
+#ifdef __FreeBSD__
+ if (sandbox) {
+ cap_rights_t rights;
+
+ if (caph_limit_stdio() < 0)
+ err(EXIT_FAILURE, "capsicum");
+ cap_rights_init(&rights, CAP_READ, CAP_FSTAT, CAP_SEEK);
+ for (n = 0; n < nconf_files; n++) {
+ if (fds[n] >= 0 &&
+ caph_rights_limit(fds[n], &rights) < 0)
+ err(EXIT_FAILURE, "capsicum");
+ }
+ if (caph_enter() < 0)
+ err(EXIT_FAILURE, "capsicum");
+ }
+#else
+ (void)sandbox;
+#endif
+
+ processing = bsdconf_format_processing(format);
+ for (n = 0; n < nconf_files; n++) {
+ if (fds[n] < 0)
+ continue;
+ conf_scanidx = n;
+ if (bsdconf_fparse(NULL, fds[n], scan_cb, processing) != 0)
+ err(EXIT_FAILURE, "%s", conf_files[n]);
+ close(fds[n]);
+ }
+
+ free(fds);
+ return (EXIT_SUCCESS);
+}
+
+/*
+ * Resolve the pathname of the target format's defaults file: the format's
+ * environment override (e.g., LOADER_DEFAULTS) verbatim when set and
+ * non-empty, otherwise the descriptor's defaults file prefixed with the
+ * `-R dir' root. Returns a newly allocated path, or NULL when the format
+ * has no defaults file.
+ */
+static char *
+defaults_path(void)
+{
+ char *env;
+ char *path;
+ const struct bsdconf_format_def *def;
+ char buf[PATH_MAX];
+
+ if ((def = bsdconf_format_lookup(format)) == NULL ||
+ def->defaults == NULL)
+ return (NULL);
+ if (def->defaults_env != NULL &&
+ (env = getenv(def->defaults_env)) != NULL && *env != '\0') {
+ if ((path = strdup(env)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ return (path);
+ }
+ if (snprintf(buf, sizeof(buf), "%s%s", rootdir, def->defaults) >=
+ (int)sizeof(buf))
+ errx(EXIT_FAILURE, "%s%s: %s", rootdir, def->defaults,
+ strerror(ENAMETOOLONG));
+ if ((path = strdup(buf)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ return (path);
+}
+
+/*
+ * Resolve the target format and its ordered list of backing files from the
+ * command line: either the `-f file' override (a single file; the format is
+ * guessed from its name) or the leading `target' keyword (`loader',
+ * `sysctl', `make'), whose multi-file list -- optionally including the
+ * `-k module' drop-in -- comes from the format abstraction layer (which,
+ * for targets carrying a defaults file, performs the same discovery the
+ * boot-time consumer does; see bsdconf_format_files(3)). All paths honor
+ * the `-R dir' (or `-j jail') root. A target with a defaults file always
+ * has it prepended to the scan order (defaults first, mirroring the boot
+ * loader, so reads report the effective value even for directives no conf
+ * file mentions), but the slot is read-only bookkeeping: see defaults_idx.
+ * With `-D', the defaults file replaces the list outright; `-A' widens
+ * dump scope to include directives still at their default. Combined
+ * (`-AD'), the scoping of `-D' wins and `-A' retains only its
+ * dump-implying role, so `-ADd' describes all defaults and only defaults,
+ * as in sysrc(8).
+ */
+static void
+resolve_target(const char *target)
+{
+ char *defaults;
+ const char *env = NULL;
+ const struct bsdconf_format_def *def;
+
+ if (target == NULL) {
+ warnx("no target provided");
+ usage();
+ }
+ if (bsdconf_format_find(target, &format) != 0) {
+ warnx("unknown target `%s'", target);
+ usage();
+ }
+
+ if (file != NULL) {
+ char path[PATH_MAX];
+ const char *prefix;
+
+ /*
+ * `-f file' overrides which file(s) are consulted and
+ * modified, but never the format; that is always stated
+ * explicitly by the target keyword. Standard input
+ * (`-f -') is the caller's own and is not subject to -R.
+ */
+ prefix = file_stdin ? "" : rootdir;
+ if ((conf_files = calloc(1, sizeof(*conf_files))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ if (snprintf(path, sizeof(path), "%s%s", prefix, file) >=
+ (int)sizeof(path))
+ errx(EXIT_FAILURE, "%s%s: %s", prefix, file,
+ strerror(ENAMETOOLONG));
+ if ((conf_files[0] = strdup(path)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ nconf_files = 1;
+ write_idx = 0;
+ return;
+ }
+
+ def = bsdconf_format_lookup(format);
+ if (def == NULL || (def->path == NULL && def->sources == NULL))
+ errx(EXIT_FAILURE,
+ "target `%s' has no default files; specify one with -f",
+ target);
+
+ /* `-D' consults the defaults file alone */
+ if (defaults_only) {
+ if ((conf_files = calloc(1, sizeof(*conf_files))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ conf_files[0] = defaults_path();
+ nconf_files = 1;
+ write_idx = 0;
+ return;
+ }
+
+ if (def->defaults_env != NULL &&
+ (env = getenv(def->defaults_env)) != NULL && *env == '\0')
+ env = NULL;
+ if (bsdconf_format_files(format, rootdir, module, env, &conf_files,
+ &nconf_files, &write_idx) != 0 || nconf_files == 0)
+ err(EXIT_FAILURE, "%s", target);
+
+ /*
+ * A target with a defaults file sources it first at boot, so the
+ * effective value of a directive may come from the defaults alone
+ * even when no conf file mentions it. Prepend the defaults file
+ * to the scan order (defaults first, so every override lands
+ * later) and remember its index: the defaults are read-only, so
+ * that slot is never chosen as a write target, never satisfies a
+ * removal, and is never shown by -l/-L (see defaults_idx users).
+ */
+ if ((defaults = defaults_path()) != NULL) {
+ char **tmp;
+ size_t n;
+
+ tmp = calloc(nconf_files + 1, sizeof(*tmp));
+ if (tmp == NULL)
+ err(EXIT_FAILURE, NULL);
+ tmp[0] = defaults;
+ for (n = 0; n < nconf_files; n++)
+ tmp[n + 1] = conf_files[n];
+ free(conf_files);
+ conf_files = tmp;
+ nconf_files++;
+ defaults_idx = 0;
+ write_idx++;
+ }
+}
+
+/*
+ * Split a `name[=value]' argument into a request. With `remove' set, the
+ * argument is a bare name to delete. For make(1)-style targets, a trailing
+ * operator character on the name (`+', `?', `:', `!') selects the
+ * corresponding assignment modifier; for every other target, `name+=word'
+ * and `name-=word' are sysrc(8)-style list edits, appending words not
+ * already present or striking those that are (see merge_list_requests()
+ * below).
+ */
+static void
+split_request(char *arg, struct request *req, int remove)
+{
+ char *eq;
+ size_t n;
+
+ req->name = arg;
+ req->newvalue = NULL;
+ req->op = BSDCONF_OP_DEFAULT;
+ req->listop = '\0';
+ req->value = NULL;
+ req->found = 0;
+ req->remove = remove;
+ req->srcidx = -1;
+ if ((req->infile = calloc(nconf_files, 1)) == NULL)
+ err(EXIT_FAILURE, NULL);
+
+ if (remove) {
+ if (strchr(arg, '=') != NULL)
+ errx(EXIT_FAILURE,
+ "%s: -x does not take a value", arg);
+ return;
+ }
+
+ if ((eq = strchr(arg, '=')) == NULL)
+ return;
+
+ *eq = '\0';
+
+ /*
+ * Strip one balanced layer of double-quotes from the value as a
+ * courtesy (e.g., a literal zfs_load="YES" from a quoted shell
+ * word); the library re-applies the target format's quoting rules
+ * on output.
+ */
+ req->newvalue = bsdconf_unquote(eq + 1);
+
+ /* Split an operator character off the tail of the name */
+ n = strlen(arg);
+ if ((bsdconf_format_processing(format) &
+ BSDCONF_OPERATOR_EQUALS) != 0 && n > 1) {
+ switch (arg[n - 1]) {
+ case '+': req->op = BSDCONF_OP_APPEND; break;
+ case '?': req->op = BSDCONF_OP_COND; break;
+ case ':': req->op = BSDCONF_OP_EXPAND; break;
+ case '!': req->op = BSDCONF_OP_SHELL; break;
+ case '-':
+ arg[n - 1] = '\0';
+ errx(EXIT_FAILURE,
+ "%s: -= is not a make(1) operator", arg);
+ }
+ if (req->op != BSDCONF_OP_DEFAULT)
+ arg[n - 1] = '\0';
+ } else if (n > 1 && (arg[n - 1] == '+' || arg[n - 1] == '-')) {
+ req->listop = arg[n - 1];
+ arg[n - 1] = '\0';
+ }
+
+ if (*arg == '\0')
+ errx(EXIT_FAILURE, "empty directive name");
+}
+
+/*
+ * Test whether the whitespace-separated `list' contains `word' (of length
+ * `wlen') as a whole word.
+ */
+static int
+list_has(const char *list, const char *word, size_t wlen)
+{
+ size_t len;
+
+ if (list == NULL)
+ return (0);
+ while (*list != '\0') {
+ list += strspn(list, " \t");
+ if ((len = strcspn(list, " \t")) == 0)
+ break;
+ if (len == wlen && strncmp(list, word, wlen) == 0)
+ return (1);
+ list += len;
+ }
+
+ return (0);
+}
+
+/*
+ * Merge the whitespace-separated `words' into (op `+') or out of (op `-')
+ * the whitespace-separated list `current' (NULL for an unset directive),
+ * appending only words not already present and joining the survivors with
+ * single spaces. Returns newly allocated storage.
+ */
+static char *
+list_merge(const char *current, const char *words, int op)
+{
+ size_t len;
+ size_t rlen = 0;
+ size_t size;
+ char *result;
+ const char *token;
+
+ if (current == NULL)
+ current = "";
+ size = strlen(current) + strlen(words) + 2;
+ if ((result = malloc(size)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ result[0] = '\0';
+
+ /* Copy the current words, striking matches under `-' */
+ token = current;
+ while (*token != '\0') {
+ token += strspn(token, " \t");
+ if ((len = strcspn(token, " \t")) == 0)
+ break;
+ if (op == '-' && list_has(words, token, len)) {
+ token += len;
+ continue;
+ }
+ if (rlen > 0)
+ result[rlen++] = ' ';
+ memcpy(result + rlen, token, len);
+ rlen += len;
+ result[rlen] = '\0';
+ token += len;
+ }
+
+ /* Append the new words not already present under `+' */
+ if (op == '+') {
+ token = words;
+ while (*token != '\0') {
+ token += strspn(token, " \t");
+ if ((len = strcspn(token, " \t")) == 0)
+ break;
+ if (!list_has(result, token, len)) {
+ if (rlen > 0)
+ result[rlen++] = ' ';
+ memcpy(result + rlen, token, len);
+ rlen += len;
+ result[rlen] = '\0';
+ }
+ token += len;
+ }
+ }
+
+ return (result);
+}
+
+/*
+ * Materialize list-edit requests (`name+=word ...' and `name-=word ...')
+ * against the effective values recorded by scan_pass(), rewriting each
+ * request's pending value in place so the ordinary write, check, and echo
+ * machinery sees a plain assignment. Appending to an unset directive
+ * creates it; striking words from one is reported like any other unknown
+ * directive (there is nothing to remove) and the request is neutralized.
+ * Returns EXIT_SUCCESS or EXIT_FAILURE.
+ */
+static int
+merge_list_requests(void)
+{
+ int rv = EXIT_SUCCESS;
+ unsigned int n;
+
+ for (n = 0; n < nreqs; n++) {
+ if (reqs[n].listop == '\0' || reqs[n].newvalue == NULL)
+ continue;
+ if (reqs[n].listop == '-' && !reqs[n].found) {
+ if (!ignore_unknown) {
+ if (!quiet)
+ warnx("unknown directive '%s'",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ }
+ /* Convert to a no-op */
+ reqs[n].newvalue = NULL;
+ reqs[n].name = "";
+ continue;
+ }
+ reqs[n].newvalue = list_merge(reqs[n].value,
+ reqs[n].newvalue, reqs[n].listop);
+ }
+
+ return (rv);
+}
+
+/*
+ * Print a candidate path unless `-E' excludes it for not existing or it
+ * was already printed (`seen' is a NULL-terminated list of prior paths).
+ */
+static void
+list_candidate(const char *path, char *seen[], size_t nseen)
+{
+ size_t n;
+
+ for (n = 0; n < nseen; n++)
+ if (seen[n] != NULL && strcmp(seen[n], path) == 0)
+ return;
+ if (existing_only && access(path, F_OK) != 0)
+ return;
+ puts(path);
+}
+
+/*
+ * Record `path' in the `seen' list (for suppressing duplicates) after
+ * printing it as a candidate.
+ */
+static void
+list_candidate_seen(const char *path, char **seen, size_t *nseen,
+ size_t maxseen)
+{
+
+ list_candidate(path, seen, *nseen);
+ if (*nseen < maxseen)
+ seen[(*nseen)++] = strdup(path);
+}
+
+/*
+ * Process `-l' (list the files backing the target) and `-L' (additionally
+ * list every drop-in candidate; for targets with a module drop-in
+ * directory, one candidate per loaded kernel module plus any `*.conf'
+ * already on disk). With `-E', only files that exist are shown. Returns
+ * EXIT_SUCCESS.
+ */
+static int
+do_list(void)
+{
+ size_t n;
+ size_t nseen = 0;
+ char **seen;
+ const char *moddir = NULL;
+ const struct bsdconf_format_def *def;
+ const struct bsdconf_source *source;
+#define LIST_MAXSEEN 512
+ char dpath[PATH_MAX];
+ char path[PATH_MAX];
+
+ if ((seen = calloc(LIST_MAXSEEN, sizeof(*seen))) == NULL)
+ err(EXIT_FAILURE, NULL);
+
+ for (n = 0; n < nconf_files; n++) {
+ /* The read-only defaults file is never a write candidate */
+ if ((int)n == defaults_idx)
+ continue;
+ list_candidate_seen(conf_files[n], seen, &nseen,
+ LIST_MAXSEEN);
+ }
+
+ if (!list_all)
+ goto done;
+
+ /* Locate the module drop-in directory (if the format has one) */
+ def = bsdconf_format_lookup(format);
+ if (def != NULL && def->sources != NULL)
+ for (source = def->sources; source->path != NULL; source++)
+ if (source->type == BSDCONF_SOURCE_MODDIR)
+ moddir = source->path;
+ if (moddir == NULL)
+ goto done;
+
+#ifdef __FreeBSD__
+ /*
+ * One candidate per loaded kernel module, whether or not the file
+ * exists yet (the module being loaded is what makes the file
+ * eligible for sourcing by rc.subr(8)).
+ */
+ {
+ char name[MAXPATHLEN];
+ char *dot;
+ int fileid;
+ struct kld_file_stat stat;
+
+ for (fileid = kldnext(0); fileid > 0;
+ fileid = kldnext(fileid)) {
+ stat.version = sizeof(stat);
+ if (kldstat(fileid, &stat) != 0)
+ continue;
+ if (strcmp(stat.name, "kernel") == 0)
+ continue;
+ if (strlcpy(name, stat.name, sizeof(name)) >=
+ sizeof(name))
+ continue;
+ if ((dot = strrchr(name, '.')) != NULL &&
+ strcmp(dot, ".ko") == 0)
+ *dot = '\0';
+ if (snprintf(path, sizeof(path), "%s%s/%s.conf",
+ rootdir, moddir, name) >= (int)sizeof(path))
+ continue;
+ list_candidate_seen(path, seen, &nseen,
+ LIST_MAXSEEN);
+ }
+ }
+#endif
+
+ /* Plus any drop-in already on disk (module not currently loaded) */
+ {
+ int n2;
+ int nentries;
+ struct dirent **entries;
+
+ if (snprintf(dpath, sizeof(dpath), "%s%s", rootdir,
+ moddir) >= (int)sizeof(dpath))
+ goto done;
+ nentries = scandir(dpath, &entries, NULL, alphasort);
+ for (n2 = 0; n2 < nentries; n2++) {
+ size_t len = strlen(entries[n2]->d_name);
+
+ if (entries[n2]->d_name[0] != '.' && len > 5 &&
+ strcmp(entries[n2]->d_name + len - 5,
+ ".conf") == 0 &&
+ snprintf(path, sizeof(path), "%s/%s", dpath,
+ entries[n2]->d_name) < (int)sizeof(path))
+ list_candidate_seen(path, seen, &nseen,
+ LIST_MAXSEEN);
+ free(entries[n2]);
+ }
+ if (nentries >= 0)
+ free(entries);
+ }
+
+done:
+ for (n = 0; n < nseen; n++)
+ free(seen[n]);
+ free(seen);
+ return (EXIT_SUCCESS);
+}
+
+/*
+ * Report read requests (and `-a') from the results of a completed
+ * scan_pass(). Returns EXIT_SUCCESS or EXIT_FAILURE.
+ */
+static int
+do_reads(void)
+{
+ int rv = EXIT_SUCCESS;
+ size_t d;
+ unsigned int n;
+
+ if (dump_all) {
+ for (d = 0; d < ndumps; d++) {
+ /*
+ * Without -A, a plain -a dumps only what the conf
+ * files themselves configure; a directive whose
+ * value still comes from the (always-scanned)
+ * defaults file is out of scope.
+ */
+ if (!with_defaults && defaults_idx >= 0 &&
+ dumps[d].srcidx == defaults_idx)
+ continue;
+ if (show_desc)
+ print_desc(dumps[d].name);
+ else
+ print_pair(conf_files[dumps[d].srcidx],
+ dumps[d].name, dumps[d].value);
+ }
+ return (EXIT_SUCCESS);
+ }
+
+ /* Report the results in the order requested */
+ for (n = 0; n < nreqs; n++) {
+ if (reqs[n].newvalue != NULL || reqs[n].remove ||
+ reqs[n].name[0] == '\0') /* neutralized by validation */
+ continue;
+ if (!reqs[n].found) {
+ if (ignore_unknown)
+ continue;
+ if (!quiet)
+ warnx("unknown directive '%s'",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ continue;
+ }
+ print_pair(conf_files[reqs[n].srcidx], reqs[n].name,
+ reqs[n].value);
+ }
+
+ return (rv);
+}
+
+/*
+ * Process `-c' from the results of a completed scan_pass() without touching
+ * any file: a write request differs when the authoritative value does not
+ * match, and a removal request differs when the directive is still present.
+ * Returns EXIT_SUCCESS when no changes would be required.
+ */
+static int
+do_checks(void)
+{
+ int differs;
+ int rv = EXIT_SUCCESS;
+ unsigned int n;
+
+ for (n = 0; n < nreqs; n++) {
+ if (reqs[n].newvalue == NULL && !reqs[n].remove)
+ continue;
+ if (reqs[n].remove)
+ differs = reqs[n].found;
+ else
+ differs = !reqs[n].found ||
+ strcmp(reqs[n].value, reqs[n].newvalue) != 0;
+ if (differs) {
+ if (!quiet)
+ warnx("%s: value differs", reqs[n].name);
+ rv = EXIT_FAILURE;
+ }
+ }
+
+ return (rv);
+}
+
+/*
+ * Directive descriptions harvested from the defaults file by
+ * load_descriptions() below.
+ */
+struct descent {
+ char *name; /* directive name */
+ char *desc; /* description (may be empty) */
+};
+
+static struct descent *descs; /* set by load_descriptions() */
+static size_t ndescs;
+static size_t descsize;
+
+/*
+ * Harvest every directive description from the defaults file, where a
+ * description is the inline comment trailing the directive's default
+ * assignment, continued across any subsequent lines of whitespace
+ * followed by a comment character. A commented-out default
+ * (`#comconsole_speed="115200" # Set the ...') yields its directive and
+ * description all the same -- deliberately unlike sysrc(8), which goes
+ * blind when the default itself is disabled. Comment prose is never
+ * mistaken for a directive: a name must directly abut the optional
+ * leading `#' and run unbroken to its `='.
+ */
+static void
+load_descriptions(void)
+{
+ int current = -1; /* entry whose description is growing */
+ FILE *fp;
+ ssize_t linelen;
+ size_t len;
+ size_t linesize = 0;
+ size_t n;
+ char *defaults;
+ char *line = NULL;
+ char *p;
+ char *v;
+ struct descent *tmp;
+
+ if ((defaults = defaults_path()) == NULL)
+ errx(EXIT_FAILURE, "target has no defaults file");
+ if ((fp = fopen(defaults, "r")) == NULL)
+ err(EXIT_FAILURE, "%s", defaults);
+
+ while ((linelen = getline(&line, &linesize, fp)) != -1) {
+ int quoted = 0;
+
+ if (linelen > 0 && line[linelen - 1] == '\n')
+ line[--linelen] = '\0';
+
+ /* Whitespace then `#' continues the open description */
+ p = line;
+ if (*p == ' ' || *p == '\t') {
+ p += strspn(p, " \t");
+ if (*p == '#' && current >= 0) {
+ char *grown;
+
+ p++;
+ p += strspn(p, " \t");
+ if (*p == '\0')
+ continue;
+ len = strlen(descs[current].desc) +
+ strlen(p) + 2;
+ if ((grown = malloc(len)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ snprintf(grown, len, "%s%s%s",
+ descs[current].desc,
+ *descs[current].desc != '\0' ? " " : "",
+ p);
+ free(descs[current].desc);
+ descs[current].desc = grown;
+ continue;
+ }
+ current = -1;
+ continue;
+ }
+ current = -1;
+
+ /* A commented-out default still describes its directive */
+ if (*p == '#')
+ p++;
+
+ /* A directive name runs unbroken to its `=' */
+ len = strcspn(p, " \t\"#=");
+ if (len == 0 || p[len] != '=')
+ continue;
+
+ /* First description wins on a repeated directive */
+ for (n = 0; n < ndescs; n++)
+ if (strncmp(descs[n].name, p, len) == 0 &&
+ descs[n].name[len] == '\0')
+ break;
+ if (n < ndescs)
+ continue;
+
+ /* Skip the value (quotes may hide a `#') */
+ for (v = p + len + 1; *v != '\0'; v++) {
+ if (*v == '"')
+ quoted = !quoted;
+ else if (*v == '#' && !quoted)
+ break;
+ }
+ if (*v == '#') {
+ v++;
+ v += strspn(v, " \t");
+ }
+
+ if (ndescs >= descsize) {
+ descsize = (descsize == 0) ? 64 : descsize << 1;
+ tmp = realloc(descs, descsize * sizeof(*descs));
+ if (tmp == NULL)
+ err(EXIT_FAILURE, NULL);
+ descs = tmp;
+ }
+ descs[ndescs].name = strndup(p, len);
+ descs[ndescs].desc = strdup(v);
+ if (descs[ndescs].name == NULL ||
+ descs[ndescs].desc == NULL)
+ err(EXIT_FAILURE, NULL);
+ current = (int)ndescs;
+ ndescs++;
+ }
+ free(line);
+ fclose(fp);
+ free(defaults);
+}
+
+/*
+ * Return the harvested description of `name' or NULL if the defaults file
+ * does not mention it.
+ */
+static const char *
+desc_find(const char *name)
+{
+ size_t n;
+
+ for (n = 0; n < ndescs; n++)
+ if (strcmp(descs[n].name, name) == 0)
+ return (descs[n].desc);
+ return (NULL);
+}
+
+#if defined(__FreeBSD__)
+/*
+ * Fetch the running kernel's description of the sysctl OID `name' into
+ * `buf' via CTL_SYSCTL_OIDDESCR (the same information sysctl(8) prints
+ * with its own -d; an OID with no description yields the empty string).
+ * Returns zero on success; -1 when the OID does not resolve.
+ */
+static int
+sysctl_descr(const char *name, char *buf, size_t bufsize)
+{
+ int mib[CTL_MAXNAME];
+ int qoid[CTL_MAXNAME + 2];
+ size_t len;
+ size_t size;
+
+ buf[0] = '\0';
+
+ qoid[0] = CTL_SYSCTL;
+ qoid[1] = CTL_SYSCTL_NAME2OID;
+ size = sizeof(mib);
+ if (sysctl(qoid, 2, mib, &size, name, strlen(name)) != 0)
+ return (-1);
+ len = size / sizeof(int);
+
+ qoid[1] = CTL_SYSCTL_OIDDESCR;
+ memcpy(qoid + 2, mib, len * sizeof(int));
+ size = bufsize - 1;
+ if (sysctl(qoid, (u_int)len + 2, buf, &size, 0, 0) != 0)
+ buf[0] = '\0';
+ buf[bufsize - 1] = '\0';
+
+ return (0);
+}
+#endif
+
+/*
+ * Print the description of `directive' according to the display flags: for
+ * the sysctl target, from the running kernel; otherwise from the table
+ * harvested off the defaults file (a directive the defaults never mention
+ * prints an empty description).
+ */
+static void
+print_desc(const char *directive)
+{
+ const char *desc = NULL;
+#if defined(__FreeBSD__)
+ char buf[BUFSIZ];
+
+ if (format == BSDCONF_FORMAT_SYSCTL) {
+ (void)sysctl_descr(directive, buf, sizeof(buf));
+ desc = buf;
+ }
+#endif
+ if (desc == NULL && (desc = desc_find(directive)) == NULL)
+ desc = "";
+ if (name_only)
+ puts(directive);
+ else if (value_only)
+ puts(desc);
+ else
+ printf("%s: %s\n", directive, desc);
+}
+
+/*
+ * Process `-d' with explicit names for a defaults-backed target. Returns
+ * EXIT_SUCCESS or EXIT_FAILURE.
+ */
+static int
+describe_defaults(void)
+{
+ int rv = EXIT_SUCCESS;
+ unsigned int n;
+
+ load_descriptions();
+ for (n = 0; n < nreqs; n++) {
+ if (desc_find(reqs[n].name) == NULL) {
+ if (ignore_unknown)
+ continue;
+ if (!quiet)
+ warnx("unknown directive '%s'",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ continue;
+ }
+ print_desc(reqs[n].name);
+ }
+
+ return (rv);
+}
+
+#if defined(__FreeBSD__)
+/*
+ * Process `-d' with explicit names for the sysctl target: report each
+ * requested OID's description from the running kernel. Returns
+ * EXIT_SUCCESS or EXIT_FAILURE.
+ */
+static int
+describe_sysctl(void)
+{
+ int rv = EXIT_SUCCESS;
+ unsigned int n;
+ char buf[BUFSIZ];
+
+ for (n = 0; n < nreqs; n++) {
+ if (sysctl_descr(reqs[n].name, buf, sizeof(buf)) != 0) {
+ if (ignore_unknown)
+ continue;
+ if (!quiet)
+ warnx("unknown oid '%s'", reqs[n].name);
+ rv = EXIT_FAILURE;
+ continue;
+ }
+ if (name_only)
+ puts(reqs[n].name);
+ else if (value_only)
+ puts(buf);
+ else
+ printf("%s: %s\n", reqs[n].name, buf);
+ }
+
+ return (rv);
+}
+
+/*
+ * Determine whether the sysctl OID `name' can take effect when set from
+ * sysctl.conf(5): the OID must exist, must be a leaf, and must be writable
+ * at run time. An OID that is only tunable from the boot loader is
+ * reported as such (pointing at the loader target). Mirrors the checks
+ * sysctl(8) applies before setting a value. With `tolerate_unknown', an
+ * OID that does not resolve is allowed through (the module providing it
+ * may simply not be loaded); the flag checks are only possible against a
+ * resolvable OID. Returns zero when the value should be written; -1
+ * (after warning) otherwise.
+ */
+static int
+sysctl_writable(const char *name, int tolerate_unknown)
+{
+ int mib[CTL_MAXNAME];
+ int qoid[CTL_MAXNAME + 2];
+ size_t len;
+ size_t size;
+ u_int kind;
+ u_char buf[BUFSIZ];
+
+ qoid[0] = CTL_SYSCTL;
+ qoid[1] = CTL_SYSCTL_NAME2OID;
+ size = sizeof(mib);
+ if (sysctl(qoid, 2, mib, &size, name, strlen(name)) != 0) {
+ if (tolerate_unknown)
+ return (0);
+ warnx("unknown oid '%s'", name);
+ return (-1);
+ }
+ len = size / sizeof(int);
+
+ qoid[1] = CTL_SYSCTL_OIDFMT;
+ memcpy(qoid + 2, mib, len * sizeof(int));
+ size = sizeof(buf);
+ if (sysctl(qoid, (u_int)len + 2, buf, &size, 0, 0) != 0) {
+ warnx("couldn't find format of oid '%s'", name);
+ return (-1);
+ }
+ kind = *(u_int *)(void *)buf;
+
+ if ((kind & CTLTYPE) == CTLTYPE_NODE) {
+ warnx("oid '%s' isn't a leaf node", name);
+ return (-1);
+ }
+ if ((kind & CTLFLAG_WR) == 0) {
+ if ((kind & CTLFLAG_TUN) != 0) {
+ warnx("oid '%s' is a read only tunable", name);
+ warnx("use: %s loader %s=<value>", pgm, name);
+ } else
+ warnx("oid '%s' is read only", name);
+ return (-1);
+ }
+
+ return (0);
+}
+#endif
+
+/*
+ * Process write and remove requests using the srcidx/infile maps recorded
+ * by scan_pass(). A directive is rewritten in the file that authoritatively
+ * defines it (the last file in sourcing order that lists it) so the change
+ * survives a reboot; a directive defined nowhere is appended to the
+ * format's default file. Removals are applied to every file listing the
+ * directive, lest an earlier definition be unmasked. Each modified file is
+ * replaced in one atomic bsdconf_put() transaction. Returns EXIT_SUCCESS
+ * or EXIT_FAILURE.
+ */
+static int
+do_writes(void)
+{
+ int fd;
+ int rv = EXIT_SUCCESS;
+ size_t f;
+ unsigned int n;
+ unsigned int nopts;
+ struct bsdconf_option *opt;
+ struct bsdconf_option *options;
+ struct request **owner;
+
+ if ((options = calloc(nreqs + 1, sizeof(*options))) == NULL ||
+ (owner = calloc(nreqs, sizeof(*owner))) == NULL)
+ err(EXIT_FAILURE, NULL);
+
+ for (f = 0; f < nconf_files; f++) {
+ /* Collect the requests this file is responsible for */
+ nopts = 0;
+ for (n = 0; n < nreqs; n++) {
+ size_t target;
+
+ if (reqs[n].remove) {
+ if (!reqs[n].infile[f])
+ continue;
+ } else if (reqs[n].newvalue != NULL) {
+ /*
+ * A directive whose only definition is
+ * the read-only defaults file is written
+ * like one defined nowhere: appended to
+ * the format's default write target.
+ */
+ if (reqs[n].srcidx < 0 ||
+ reqs[n].srcidx == defaults_idx)
+ target = write_idx;
+ else
+ target = (size_t)reqs[n].srcidx;
+ if (target != f)
+ continue;
+ } else
+ continue;
+ opt = &options[nopts];
+ memset(opt, 0, sizeof(*opt));
+ opt->type = BSDCONF_TYPE_STR;
+ opt->directive = reqs[n].name;
+ opt->op = reqs[n].op;
+ if (reqs[n].remove) {
+ opt->action = BSDCONF_ACTION_REMOVE;
+ opt->value.str = NULL;
+ } else {
+ opt->action = BSDCONF_ACTION_SET_VALUE;
+ opt->value.str =
+ (char *)(uintptr_t)reqs[n].newvalue;
+ }
+ owner[nopts++] = &reqs[n];
+ }
+ if (nopts == 0)
+ continue;
+ memset(&options[nopts], 0, sizeof(options[nopts]));
+
+ /*
+ * Create the target if it does not yet exist. O_EXCL makes
+ * the test-and-create atomic: an existing file (or one that
+ * appears concurrently) is left untouched.
+ */
+ if ((fd = open(conf_files[f], O_WRONLY | O_CREAT | O_EXCL,
+ 0644)) >= 0)
+ close(fd);
+ else if (errno != EEXIST)
+ err(EXIT_FAILURE, "%s", conf_files[f]);
+
+ if (bsdconf_put(options, conf_files[f],
+ bsdconf_format_processing(format),
+ bsdconf_format_put(format)) != 0)
+ err(EXIT_FAILURE, "%s", conf_files[f]);
+
+ /* Report the results */
+ for (n = 0; n < nopts; n++) {
+ opt = &options[n];
+ if (opt->action == BSDCONF_ACTION_REMOVE) {
+ if ((opt->result &
+ BSDCONF_DIRECTIVE_REMOVED) != 0) {
+ owner[n]->found = 1;
+ if (verbose)
+ printf("%s: %s: removed\n",
+ conf_files[f],
+ opt->directive);
+ }
+ continue;
+ }
+ if (!quiet) {
+ const char *old = owner[n]->value;
+
+ /*
+ * Echo `name: old -> new' in the manner of
+ * sysctl(8) and sysrc(8); a directive not
+ * previously set anywhere has an empty old
+ * value.
+ */
+ if (verbose)
+ printf("%s: ", conf_files[f]);
+ printf("%s: %s -> %s\n", opt->directive,
+ old == NULL ? "" : old,
+ opt->value.str);
+ }
+ }
+ }
+
+ /* A removal that matched no file at all is an unknown directive */
+ for (n = 0; n < nreqs; n++) {
+ if (!reqs[n].remove || reqs[n].found)
+ continue;
+ if (ignore_unknown)
+ continue;
+ if (!quiet)
+ warnx("unknown directive '%s'", reqs[n].name);
+ rv = EXIT_FAILURE;
+ }
+
+ free(options);
+ free(owner);
+ return (rv);
+}
+
+/*
+ * The native configuration trinity's third member: read and modify
+ * loader.conf(5), sysctl.conf(5), make.conf(5), and arbitrary configuration
+ * files with sysctl(8) assignment syntax.
+ */
+int
+main(int argc, char *argv[])
+{
+ int have_reads = 0;
+ int have_writes = 0;
+ int n;
+ int rv = EXIT_SUCCESS;
+ const char *target = NULL;
+
+ pgm = argv[0]; /* store a copy of invocation name */
+
+ /*
+ * The `rc' target is a pure pass-through to sysrc(8), detected
+ * while the command line is still pristine so that nothing --
+ * `--help' and `--version' included -- is intercepted on its way
+ * there (see exec_sysrc()).
+ */
+ n = find_target(argc, argv);
+ if (n > 0 && strcmp(argv[n], "rc") == 0)
+ exec_sysrc(argc, argv, n); /* never returns */
+
+ /*
+ * Honor `--help' and `--version' wherever they appear (getopt(3)
+ * knows nothing of long options and would reject them as illegal
+ * short options). A bare `--' ends option processing as usual.
+ */
+ for (n = 1; n < argc; n++) {
+ if (strcmp(argv[n], "--") == 0)
+ break;
+ if (strcmp(argv[n], "--help") == 0)
+ help();
+ if (strcmp(argv[n], "--version") == 0) {
+ puts(SYSCONF_VERSION);
+ exit(EXIT_SUCCESS);
+ }
+ }
+
+ /*
+ * Process command-line options
+ */
+ n = parse_options(argc, argv);
+ argc -= n;
+ argv += n;
+
+ /*
+ * Consume the (required) target keyword and allow further options
+ * to follow it, mirroring the option placement freedom of
+ * sysctl(8) (e.g., `sysconf sysctl -L'). getopt(3) skips over its
+ * first argument, so hand it the target keyword's slot as the
+ * program name.
+ */
+ if (argc > 0) {
+ target = argv[0];
+ argc--;
+ argv++;
+#ifdef __FreeBSD__
+ optreset = 1;
+ optind = 1;
+#else
+ optind = 0; /* glibc and musl re-initialize at zero */
+#endif
+ n = parse_options(argc + 1, argv - 1) - 1;
+ argc -= n;
+ argv += n;
+ }
+
+ if (jailname != NULL && *rootdir != '\0') {
+ warnx("-j and -R are mutually exclusive");
+ usage();
+ }
+ if (file != NULL && module != NULL) {
+ warnx("-f and -k are mutually exclusive");
+ usage();
+ }
+ if (existing_only && !list_files && !list_all) {
+ warnx("-E requires -l or -L");
+ usage();
+ }
+
+#ifdef __FreeBSD__
+ if (jailname != NULL) {
+ int jid;
+
+ if ((jid = jail_getid(jailname)) < 0)
+ errx(EXIT_FAILURE, "%s", jail_errmsg);
+ if (jail_attach(jid) != 0)
+ err(EXIT_FAILURE, "jail_attach(%d)", jid);
+ }
+#else
+ if (jailname != NULL)
+ errx(EXIT_FAILURE, "-j is not supported on this platform");
+#endif
+
+ /* Resolve the target format and its backing files */
+ resolve_target(target);
+
+ /* `-k' requires a target with a module drop-in directory */
+ if (module != NULL) {
+ const struct bsdconf_format_def *def;
+ const struct bsdconf_source *source;
+ int has_moddir = 0;
+
+ def = bsdconf_format_lookup(format);
+ if (def != NULL && def->sources != NULL)
+ for (source = def->sources; source->path != NULL;
+ source++)
+ if (source->type == BSDCONF_SOURCE_MODDIR)
+ has_moddir = 1;
+ if (!has_moddir) {
+ warnx("target does not support -k");
+ usage();
+ }
+ }
+
+ /*
+ * `-d', `-D', and `-A' consult a body of defaults; only targets
+ * that have one support them (make.conf and friends have no
+ * defaults file, and no directive descriptions to give). The
+ * exception is `sysctl -d', whose descriptions come from the
+ * running kernel rather than any file.
+ */
+ {
+ const struct bsdconf_format_def *def;
+ int has_defaults;
+
+ def = bsdconf_format_lookup(format);
+ has_defaults = def != NULL && def->defaults != NULL;
+
+ if (show_desc) {
+ if (format != BSDCONF_FORMAT_SYSCTL &&
+ !has_defaults) {
+ warnx("target does not support -d");
+ usage();
+ }
+ if (file != NULL) {
+ warnx("-d and -f are mutually exclusive");
+ usage();
+ }
+ if (list_files || list_all || remove_mode || check) {
+ warnx("-d cannot be combined with "
+ "-c/-l/-L/-x");
+ usage();
+ }
+#ifdef __FreeBSD__
+ if (format == BSDCONF_FORMAT_SYSCTL &&
+ *rootdir != '\0')
+ errx(EXIT_FAILURE, "sysctl descriptions "
+ "come from the running kernel; "
+ "-R does not apply");
+#endif
+ }
+ if ((defaults_only || with_defaults) && !has_defaults) {
+ warnx("target does not support -%c",
+ defaults_only ? 'D' : 'A');
+ usage();
+ }
+ if ((defaults_only || with_defaults) && file != NULL) {
+ warnx("-%c and -f are mutually exclusive",
+ defaults_only ? 'D' : 'A');
+ usage();
+ }
+ }
+
+ /* File listings take no names and perform no other work */
+ if (list_files || list_all) {
+ if (argc != 0 || dump_all) {
+ warnx("-l/-L take no names");
+ usage();
+ }
+ exit(do_list());
+ }
+
+ /* `-A' with no names dumps everything, defaults included */
+ if (with_defaults && argc == 0)
+ dump_all = 1;
+
+ /* Display usage and exit if not given at least one name */
+ if (argc == 0 && !dump_all) {
+ warnx("no names provided");
+ usage();
+ }
+ if (argc != 0 && dump_all) {
+ warnx("-a does not take names");
+ usage();
+ }
+
+ /* Classify the remaining arguments */
+ nreqs = (unsigned int)argc;
+ if (nreqs > 0 &&
+ (reqs = calloc(nreqs, sizeof(*reqs))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ for (n = 0; n < argc; n++) {
+ split_request(argv[n], &reqs[n], remove_mode);
+ if (reqs[n].remove || reqs[n].newvalue != NULL)
+ have_writes = 1;
+ else
+ have_reads = 1;
+ }
+ if (check && !have_writes) {
+ warnx("-c requires at least one name=value");
+ usage();
+ }
+
+ /* Descriptions are read-only */
+ if (show_desc) {
+ if (have_writes) {
+ warnx("-d does not take values");
+ usage();
+ }
+#ifndef __FreeBSD__
+ if (format == BSDCONF_FORMAT_SYSCTL)
+ errx(EXIT_FAILURE,
+ "sysctl descriptions require FreeBSD");
+#endif
+ /* Explicit names need no file scan at all */
+ if (!dump_all) {
+#ifdef __FreeBSD__
+ if (format == BSDCONF_FORMAT_SYSCTL)
+ exit(describe_sysctl());
+#endif
+ exit(describe_defaults());
+ }
+
+ /*
+ * Dumps (-ad, -Ad, -aDd) pair each scanned directive with
+ * its description; harvest the descriptions before the
+ * read sandbox slams shut.
+ */
+ if (format != BSDCONF_FORMAT_SYSCTL)
+ load_descriptions();
+ }
+
+ /* Defaults can be read and checked against, never rewritten */
+ if ((defaults_only || with_defaults) && have_writes && !check)
+ errx(EXIT_FAILURE, "defaults are read-only");
+
+ /* Standard input can be read and checked, never rewritten */
+ if (file_stdin && have_writes && !check)
+ errx(EXIT_FAILURE, "cannot write to standard input");
+
+#if defined(__FreeBSD__)
+ /*
+ * Refuse up front to set sysctl OIDs that can never take effect
+ * from sysctl.conf(5); apply the remaining (valid) requests. Only
+ * meaningful against the running system's sysctl tree, so skipped
+ * under -R. Unknown OIDs are permitted with -i or -k (a module
+ * drop-in typically configures a module that is not yet loaded).
+ */
+ if (format == BSDCONF_FORMAT_SYSCTL && have_writes && !check &&
+ *rootdir == '\0') {
+ int tolerate = ignore_unknown || module != NULL;
+ unsigned int u;
+
+ for (u = 0; u < nreqs; u++) {
+ if (reqs[u].newvalue == NULL)
+ continue;
+ if (sysctl_writable(reqs[u].name, tolerate) != 0) {
+ /* Convert to a no-op; count the failure */
+ reqs[u].newvalue = NULL;
+ reqs[u].remove = 0;
+ reqs[u].name = "";
+ rv = EXIT_FAILURE;
+ }
+ }
+ have_writes = 0;
+ have_reads = 0;
+ for (u = 0; u < nreqs; u++) {
+ if (reqs[u].remove || reqs[u].newvalue != NULL)
+ have_writes = 1;
+ else if (reqs[u].name[0] != '\0')
+ have_reads = 1;
+ }
+ if (rv != EXIT_SUCCESS && !have_writes && !have_reads)
+ exit(rv); /* nothing settable remains */
+ }
+#endif
+
+ /*
+ * Scan the backing files to locate each request's authoritative
+ * definition. Pure read operations run the scan (and the rest of
+ * their lives) inside a sandbox -- except a sysctl description
+ * dump, whose kernel queries the sandbox would deny.
+ */
+ scan_pass(!have_writes && !check &&
+ !(show_desc && format == BSDCONF_FORMAT_SYSCTL));
+
+ /* Materialize list edits now that effective values are known */
+ if (merge_list_requests() != EXIT_SUCCESS)
+ rv = EXIT_FAILURE;
+
+ if (check) {
+ if (do_checks() != EXIT_SUCCESS)
+ rv = EXIT_FAILURE;
+ exit(rv);
+ }
+
+ /* Apply writes first so that subsequent reads see the result */
+ if (have_writes) {
+ int wrv;
+
+ if ((wrv = do_writes()) != EXIT_SUCCESS)
+ rv = wrv;
+
+ /* Refresh the scan for any trailing read requests */
+ if (have_reads || dump_all) {
+ for (n = 0; (unsigned int)n < nreqs; n++) {
+ reqs[n].found = 0;
+ reqs[n].srcidx = -1;
+ memset(reqs[n].infile, 0, nconf_files);
+ }
+ scan_pass(0);
+ }
+ }
+
+ if (have_reads || dump_all) {
+ int rrv;
+
+ if ((rrv = do_reads()) != EXIT_SUCCESS)
+ rv = rrv;
+ }
+
+ exit(rv);
+}
+
+/*
+ * Print short usage statement to stderr and exit with error status.
+ */
+static void
+usage(void)
+{
+
+ fprintf(stderr,
+ "usage: %s target [-AcDeFinNqvx] [-j jail | -R dir]"
+ " [-f file | -k module]\n"
+ " name[[+|-]=value] ...\n", pgm);
+ fprintf(stderr,
+ " %s target [-ADeFnNqv] [-j jail | -R dir]"
+ " [-f file | -k module] -a\n", pgm);
+ fprintf(stderr,
+ " %s target [-AaDinNq] [-j jail | -R dir]"
+ " -d [name ...]\n", pgm);
+ fprintf(stderr,
+ " %s target [-E] [-j jail | -R dir] [-k module]"
+ " -l | -L\n", pgm);
+ fprintf(stderr,
+ " %s rc [sysrc(8) argument ...]\n", pgm);
+ fprintf(stderr, "Try `%s --help' for more information.\n", pgm);
+ exit(EXIT_FAILURE);
+}
+
+/*
+ * Print long usage statement to stderr and exit with error status.
+ */
+static void
+help(void)
+{
+
+ fprintf(stderr,
+ "usage: %s target [-AcDeFinNqvx] [-j jail | -R dir]"
+ " [-f file | -k module] name[[+|-]=value] ...\n", pgm);
+ fprintf(stderr, "TARGETS:\n");
+#define TGTFMT "\t%-9s %s\n"
+ fprintf(stderr, TGTFMT, "loader",
+ "files named by loader_conf_files et al. in");
+ fprintf(stderr, TGTFMT, "",
+ "/boot/defaults/loader.conf (typically /boot/loader.conf,");
+ fprintf(stderr, TGTFMT, "",
+ "loader.conf.d/*.conf, loader.conf.local)");
+ fprintf(stderr, TGTFMT, "sysctl",
+ "/etc/sysctl.conf, sysctl.conf.local, sysctl.kld.d/*.conf");
+ fprintf(stderr, TGTFMT, "make", "/etc/make.conf");
+ fprintf(stderr, TGTFMT, "src", "/etc/src.conf");
+ fprintf(stderr, TGTFMT, "src-env", "/etc/src-env.conf");
+ fprintf(stderr, TGTFMT, "rc",
+ "pass-through: all other arguments go to sysrc(8) verbatim");
+ fprintf(stderr, TGTFMT, "generic",
+ "no default files; requires -f file");
+ fprintf(stderr, "OPTIONS:\n");
+#define OPTFMT "\t%-9s %s\n"
+ fprintf(stderr, OPTFMT, "-A",
+ "Include the target's defaults file in the sourcing order");
+ fprintf(stderr, OPTFMT, "",
+ "(alone, dumps everything, defaults included).");
+ fprintf(stderr, OPTFMT, "-a",
+ "Dump the union of all directives from the target's files.");
+ fprintf(stderr, OPTFMT, "-c",
+ "Check. Return success if no changes needed, else error.");
+ fprintf(stderr, OPTFMT, "-D",
+ "Consult only the target's defaults file.");
+ fprintf(stderr, OPTFMT, "-d",
+ "Show directive descriptions (from the defaults file's");
+ fprintf(stderr, OPTFMT, "",
+ "comments; for sysctl, from the running kernel). With");
+ fprintf(stderr, OPTFMT, "",
+ "-a, -A, or -D, describe every directive in scope.");
+ fprintf(stderr, OPTFMT, "-E",
+ "With -l or -L, list only files that exist on disk.");
+ fprintf(stderr, OPTFMT, "-e",
+ "Separate the name and value of directive(s) with `='.");
+ fprintf(stderr, OPTFMT, "-F",
+ "Show the file holding each directive's effective value.");
+ fprintf(stderr, OPTFMT, "-f file",
+ "Operate on file, in the target's format, instead of the");
+ fprintf(stderr, OPTFMT, "",
+ "target's standard files (`-' means standard input).");
+ fprintf(stderr, OPTFMT, "-h",
+ "Print a short usage statement to stderr and exit.");
+ fprintf(stderr, OPTFMT, "--help",
+ "Print this message to stderr and exit.");
+ fprintf(stderr, OPTFMT, "--version",
+ "Print version information to stdout and exit.");
+ fprintf(stderr, OPTFMT, "-i",
+ "Ignore unknown names (and unknown sysctl OIDs).");
+ fprintf(stderr, OPTFMT, "-j jail",
+ "Operate within the jail `jail' (name or numeric id).");
+ fprintf(stderr, OPTFMT, "-k module",
+ "Include the kernel module drop-in file for `module'.");
+ fprintf(stderr, OPTFMT, "-l",
+ "List the pathnames of the target's backing files.");
+ fprintf(stderr, OPTFMT, "-L",
+ "List all candidate files, including module drop-ins.");
+ fprintf(stderr, OPTFMT, "-n",
+ "Show only directive values, not their names.");
+ fprintf(stderr, OPTFMT, "-N",
+ "Show only directive names, not their values.");
+ fprintf(stderr, OPTFMT, "-q",
+ "Quiet. Suppress unknown-directive warnings and the");
+ fprintf(stderr, OPTFMT, "",
+ "`old -> new' echo of writes.");
+ fprintf(stderr, OPTFMT, "-R dir",
+ "Operate within the root directory `dir' rather than `/'.");
+ fprintf(stderr, OPTFMT, "-v",
+ "Verbose. Print the pathname of the configuration file.");
+ fprintf(stderr, OPTFMT, "-x",
+ "Remove name(s) from the target's files.");
+ fprintf(stderr, "ENVIRONMENT:\n");
+ fprintf(stderr, OPTFMT, "LOADER_DEFAULTS",
+ "Defaults file for the loader target (in place of");
+ fprintf(stderr, OPTFMT, "",
+ "/boot/defaults/loader.conf).");
+ exit(EXIT_FAILURE);
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Jul 10, 7:57 PM (2 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34897487
Default Alt Text
D58066.diff (199 KB)
Attached To
Mode
D58066: Add sysconf(8) and libbsdconf(3)
Attached
Detach File
Event Timeline
Log In to Comment