Add sysconf(8) and libbsdconf(3)

Complete the native configuration trinity: sysctl(8) for live kernel
state, sysrc(8) for rc.conf(5), and sysconf(8) for the remaining base
configuration -- loader.conf(5), sysctl.conf(5), and the make.conf(5)
family -- atop libbsdconf(3).

libbsdconf resurrects figpar as a unified reader/writer.  Callbacks own
semantics; statements may span multiple lines via backslash continuation;
non-seekable input is spooled; writes are atomic (mkstemp, fsync, rename)
with mode/owner preservation.  Format descriptors name each target, its
files, and quoting rules without private parsers.  Multi-file targets
follow boot sourcing order; loader chases loader_conf_files as the boot
loader does.

sysconf(8) is the operator-facing tool: name / name=value on a required
target, sysrc-style list edits, make append and list-strike where they
belong, jail/altroot, and a capsicum sandbox for read-only use.

Sysctl writes validate against the running kernel first -- unknown and
read-only OIDs, CTLFLAG_TUN (pointing at the loader target), and CTLTYPE
range checks -- so a typo or overflow does not land in sysctl.conf.

Make and src treat WITH_/WITHOUT_ as presence knobs (as bsd.mkopt.mk /
src.conf(5) do) and warn on the WITH_*=no form that does not disable the
option, so a bad assignment is caught before an /usr/src build surfaces
it.

The rc target passes through to sysrc(8).

Defaults querying (-d/-D/-A) mirrors sysrc for dumps and descriptions on
targets that have a defaults file; named reads already see defaults, and
-A only widens dump scope.

Manuals are split pkg(8)-style (bsdconf/put/format; sysconf plus
per-target pages).  ATF coverage exercises the frontend.

Co-authored-by:	Faraz Vahedi <[email protected]>
Reviewed by:	fuz, kfv
Differential Revision:	https://reviews.freebsd.org/D58066
This commit is contained in:
Devin Teske committed 2026-09-16 14:28:14 -07:00
1 parent f308d6d484
commit 3fe5961a0b
47 files changed
+10120

No files matched your search

+1
View File
@@ -31,6 +31,7 @@ LINE("libarm", "ARM Architecture Library (libarm, \\-larm)")
LINE("libarm32", "ARM32 Architecture Library (libarm32, \\-larm32)")
LINE("libbe", "Boot Environment Library (libbe, \\-lbe)")
LINE("libbluetooth", "Bluetooth Library (libbluetooth, \\-lbluetooth)")
LINE("libbsdconf", "Configuration File Library (libbsdconf, \\-lbsdconf)")
LINE("libbsdxml", "eXpat XML parser library (libbsdxml, \\-lbsdxml)")
LINE("libbsm", "Basic Security Module Library (libbsm, \\-lbsm)")
LINE("libc", "Standard C\\~Library (libc, \\-lc)")
+2
View File
@@ -1323,6 +1323,8 @@
..
sa
..
sysconf
..
syslogd
..
sysrc
+1
View File
@@ -36,6 +36,7 @@ SUBDIR= ${SUBDIR_BOOTSTRAP} \
libarchive \
libbegemot \
libblocksruntime \
libbsdconf \
libbsddialog \
libbsdstat \
libbsm \
+30
View File
@@ -0,0 +1,30 @@
PACKAGE= utilities
LIB= bsdconf
SHLIB_MAJOR= 1
INCS= bsdconf.h
MAN= bsdconf.3 bsdconf_format.3 bsdconf_put.3
MLINKS= bsdconf.3 bsdconf_fparse.3 \
bsdconf.3 bsdconf_get_option.3 \
bsdconf.3 bsdconf_parse.3 \
bsdconf.3 bsdconf_spool.3 \
bsdconf.3 bsdconf_unquote.3 \
bsdconf_format.3 bsdconf_format_derive.3 \
bsdconf_format.3 bsdconf_format_files.3 \
bsdconf_format.3 bsdconf_format_files_free.3 \
bsdconf_format.3 bsdconf_format_find.3 \
bsdconf_format.3 bsdconf_format_guess.3 \
bsdconf_format.3 bsdconf_format_lookup.3 \
bsdconf_format.3 bsdconf_format_path.3 \
bsdconf_format.3 bsdconf_format_processing.3 \
bsdconf_format.3 bsdconf_format_put.3 \
bsdconf_format.3 bsdconf_format_register.3 \
bsdconf_put.3 bsdconf_set_option.3
CFLAGS+= -I${.CURDIR}
SRCS= bsdconf.c bsdconf_format.c bsdconf_format_generic.c \
bsdconf_format_loader.c bsdconf_format_make.c \
bsdconf_format_src.c bsdconf_format_sysctl.c \
bsdconf_put.c bsdconf_stmt.c bsdconf_string.c
.include <bsd.lib.mk>
+15
View File
@@ -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
+432
View File
@@ -0,0 +1,432 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
.Dt BSDCONF 3
.Os
.Sh NAME
.Nm bsdconf ,
.Nm bsdconf_parse ,
.Nm bsdconf_fparse ,
.Nm bsdconf_get_option ,
.Nm bsdconf_spool ,
.Nm bsdconf_unquote
.Nd configuration file reading library
.Sh LIBRARY
.Lb libbsdconf
.Sh SYNOPSIS
.In bsdconf.h
.Ft int
.Fo bsdconf_parse
.Fa "struct bsdconf_option options[]"
.Fa "const char *path"
.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
.Fa "uint32_t line"
.Fa "char *directive"
.Fa "char *value\*[rp]"
.Fa "uint16_t processing_options"
.Fc
.Ft int
.Fo bsdconf_fparse
.Fa "struct bsdconf_option options[]"
.Fa "int fd"
.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
.Fa "uint32_t line"
.Fa "char *directive"
.Fa "char *value\*[rp]"
.Fa "uint16_t processing_options"
.Fc
.Ft "struct bsdconf_option *"
.Fo bsdconf_get_option
.Fa "struct bsdconf_option options[]"
.Fa "const char *directive"
.Fc
.Ft int
.Fo bsdconf_spool
.Fa "int fd"
.Fc
.Ft "char *"
.Fo bsdconf_unquote
.Fa "char *value"
.Fc
.Sh DESCRIPTION
The
.Nm
library provides a light-weight,
portable framework for reading and writing configuration
files.
It is the successor to the retired
.Nm figpar
library,
extending the original read-only token parser into a unified,
resilient reader/writer engine.
.Pp
Due to the fact that configuration files may have basic syntax differences,
the library does not attempt to impose any structure on the data but instead
provides raw data to a set of callback functions.
These callback functions can in-turn initiate abort through their return
value,
allowing custom syntax validation during parsing.
.Pp
Syntax differences between well-known file formats are described by format
descriptors:
a format descriptor is a small read-only table of properties
.Pq Vt struct bsdconf_format_def ; see Xr bsdconf_format 3
naming a format's target keyword,
backing files,
and tokenizing/quoting rules,
which parameterize a single shared engine.
.Pp
Despite the name, it bears no relation to a file descriptor;
it is closer in spirit to a driver's method table:
static data describing behavior,
consulted rather than executed.
Writing is documented in
.Xr bsdconf_put 3 ;
built-in formats and discovery in
.Xr bsdconf_format 3 .
.Pp
Configuration directives,
types,
and callback functions are provided through data structures defined in
.In bsdconf.h :
.Bd -literal -offset indent
struct bsdconf_option {
enum bsdconf_type type; /* value type */
const char *directive; /* keyword */
union bsdconf_value value; /* value */
enum bsdconf_op op; /* assignment operator */
uint8_t action; /* bsdconf_put() action */
uint16_t result; /* set by bsdconf_put() */
uint32_t line; /* set by bsdconf_put() */
uint32_t match_line;
/* put: 0=any, else line */
/* Pointer to function used when directive is found */
int (*parse)(struct bsdconf_option *option, uint32_t line,
char *directive, char *value);
};
enum bsdconf_type {
BSDCONF_TYPE_NONE = 0x0000, /* directives with no value */
BSDCONF_TYPE_BOOL = 0x0001, /* boolean */
BSDCONF_TYPE_INT = 0x0002, /* signed 32-bit integer */
BSDCONF_TYPE_UINT = 0x0004, /* unsigned 32-bit integer */
BSDCONF_TYPE_STR = 0x0008, /* string pointer */
BSDCONF_TYPE_STRARRAY = 0x0010, /* string array pointer */
BSDCONF_TYPE_DATA1 = 0x0020, /* void data type-1 (open) */
BSDCONF_TYPE_DATA2 = 0x0040, /* void data type-2 (open) */
BSDCONF_TYPE_DATA3 = 0x0080, /* void data type-3 (open) */
BSDCONF_TYPE_INT64 = 0x0100, /* signed 64-bit integer */
BSDCONF_TYPE_UINT64 = 0x0200, /* unsigned 64-bit integer */
BSDCONF_TYPE_RESERVED = 0x0400, /* reserved */
};
union bsdconf_value {
void *data; /* Opaque pointer (DATA1..DATA3) */
char *str; /* Pointer to NUL-terminated string */
char **strarray; /* Pointer to an array of strings */
int32_t num; /* Signed 32-bit integer value */
uint32_t u_num; /* Unsigned 32-bit integer value */
int64_t num64; /* Signed 64-bit integer value */
uint64_t u_num64; /* Unsigned 64-bit integer value */
bool boolean; /* Boolean value */
};
.Ed
.Pp
The
.Fa processing_options
argument to
.Fn bsdconf_parse ,
.Fn bsdconf_fparse ,
and
.Xr bsdconf_put 3
is a mask of bit fields which indicate various processing options.
The possible flags are:
.Bl -tag -width BSDCONF_BREAK_ON_SEMICOLON
.It Dv BSDCONF_BREAK_ON_EQUALS
An equals sign
.Pq Ql =
is normally considered part of the directive.
This flag enables terminating the directive at the equals sign.
Also makes equals sign optional and transient.
.It Dv BSDCONF_BREAK_ON_SEMICOLON
A semicolon
.Pq Ql \&;
is normally considered part of the value.
This flag enables terminating the value at the semicolon.
Also allows multiple statements on a single line separated by semicolon.
.It Dv BSDCONF_CASE_SENSITIVE
Normally directives are matched case insensitively using
.Xr fnmatch 3 .
This flag enables directive matching to be case sensitive.
.It Dv BSDCONF_REQUIRE_EQUALS
If a directive is not followed by an equals,
processing is aborted.
.It Dv BSDCONF_STRICT_EQUALS
Equals must be part of the directive
.Pq no whitespace before or after
to be considered a delimiter between directive and value.
Required by file formats whose readers reject whitespace around the equals
sign,
such as the
.Fx
boot loader's processing of
.Xr loader.conf 5 .
.It Dv BSDCONF_OPERATOR_EQUALS
Recognize
.Xr make 1
style assignment modifiers
.Po
.Ql += ,
.Ql ?= ,
.Ql := ,
and
.Ql !=
.Pc
and split them off the tail of the directive.
The parsed operator is reported through the
.Va op
member of the matched option
.Pq one of Dv BSDCONF_OP_ASSIGN , BSDCONF_OP_APPEND , BSDCONF_OP_COND , BSDCONF_OP_EXPAND , No or Dv BSDCONF_OP_SHELL .
.El
.Pp
The
.Fa options
struct array pointer can be NULL and every directive will run the
.Fn unknown
function argument.
.Pp
The directive for each bsdconf_option item in the
.Fn bsdconf_parse
options argument is matched against each parsed directive using
.Xr fnmatch 3
until a match is found.
If a match is found,
the
.Fn parse
function for that bsdconf_option directive is run with the line number,
directive,
and value.
Otherwise if no match,
the
.Fn unknown
function is run
.Pq with the same arguments .
When
.Dv BSDCONF_OPERATOR_EQUALS
is set,
.Fn unknown
receives a non-NULL
.Fa option
whose
.Va op
member holds the statement's assignment operator
.Pq there is no matched options-array slot to hang it on ;
otherwise
.Fa option
may be
.Dv NULL .
.Pp
If either
.Fn parse
or
.Fn unknown
return non-zero,
.Fn bsdconf_parse
aborts reading the file and returns the error value to its caller.
.Pp
A value normally ends at the first unescaped newline,
but a statement may span multiple lines:
a backslash immediately preceding the newline continues the value on the
next line,
in the manner of
.Xr make 1
.Pq essential to Pa make.conf and its siblings .
The backslash-newline pairs are removed from the value delivered to the
callbacks
.Pq surrounding whitespace is preserved verbatim ,
and reported line numbers are those of each statement's first line.
.Xr bsdconf_put 3
recognizes the same continuations when locating a value;
rewriting a continued value replaces all of its lines with the single new
value.
.Pp
.Fn bsdconf_fparse
is identical to
.Fn bsdconf_parse
except that it operates on an already-open file descriptor
.Fa fd ,
which remains open on return
.Pq the caller retains ownership .
This allows the caller to constrain the process
.Pq for example with Xr capsicum 4
before parsing begins.
The scanner requires a seekable descriptor;
input that cannot seek
.Pq a pipe or socket, standard input included
is detected up front and transparently spooled through
.Fn bsdconf_spool ,
at the cost of one transient copy of the data.
.Pp
.Fn bsdconf_spool
copies the remaining contents of
.Fa fd
to an unlinked temporary file
.Pq created with Xr tmpfile 3
and returns a seekable descriptor referencing it,
which the caller must
.Xr close 2
.Pq the backing storage is reclaimed then .
It is exported for callers that must adapt non-seekable input themselves
before revoking their own ability to create files,
as
.Xr sysconf 8
does before entering its
.Xr capsicum 4
sandbox.
.Pp
.Fn bsdconf_get_option
traverses the options-array and returns the option that matches via
.Xr strcmp 3 ,
or
.Dv NULL
if none matches.
.Pp
.Fn bsdconf_unquote
strips one layer of surrounding double-quotes from
.Fa value
in place and returns it.
Parsed values retain their quotes so that data round-trips losslessly;
this helper is for display and comparison purposes.
.Sh RETURN VALUES
.Fn bsdconf_parse
and
.Fn bsdconf_fparse
return zero on success;
otherwise -1
.Pq or the non-zero result of a callback
is returned and the global variable
.Va errno
is set to indicate the error.
.Fn bsdconf_spool
returns a new seekable file descriptor on success;
otherwise -1 with
.Va errno
set to indicate the error.
.Fn bsdconf_get_option
returns a pointer to the matching option,
or
.Dv NULL
when none matches.
.Sh EXAMPLES
Read two known directives from a
.Ql name=value
file,
routing every statement through a callback:
.Bd -literal -offset indent
#include <err.h>
#include <stdio.h>
#include <bsdconf.h>
static int
show(struct bsdconf_option *option, uint32_t line,
char *directive, char *value)
{
printf("%u: %s is %s\en", line, directive,
bsdconf_unquote(value));
return (0);
}
static struct bsdconf_option options[] = {
{ .directive = "hostname", .parse = show },
{ .directive = "timeout", .parse = show },
{ .directive = NULL }
};
int
main(void)
{
if (bsdconf_parse(options, "/usr/local/etc/myapp.conf",
NULL, BSDCONF_BREAK_ON_EQUALS) != 0)
err(1, "myapp.conf");
return (0);
}
.Ed
.Pp
Callbacks own the semantics,
so a directive that legitimately repeats is accumulated rather than
overwritten;
no format descriptor is involved.
The file here is Apache-style
.Pq space-separated, no equals sign ,
so
.Dv BSDCONF_BREAK_ON_EQUALS
is simply omitted:
.Bd -literal -offset indent
static char *servers[16];
static size_t nservers;
static int
addserver(struct bsdconf_option *option, uint32_t line,
char *directive, char *value)
{
if (nservers >= 16 ||
(servers[nservers] = strdup(value)) == NULL)
return (-1); /* abort the parse */
nservers++;
return (0);
}
static struct bsdconf_option cumulative[] = {
{ .directive = "server", .parse = addserver },
{ .directive = NULL }
};
...
if (bsdconf_parse(cumulative, path, NULL, 0) != 0)
err(1, "%s", path);
.Ed
.Sh SEE ALSO
.Xr bsdconf_format 3 ,
.Xr bsdconf_put 3 ,
.Xr loader.conf 5 ,
.Xr sysctl.conf 5 ,
.Xr sysconf 8
.Sh HISTORY
The
.Nm
library first appeared in
.Fx 16.0 .
It supersedes the
.Nm figpar
library which first appeared in
.Fx 10.2
and was retired to the ports tree.
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
.Sh BUGS
This is the first implementation of the library,
and the interface may be subject to refinement.
.Pp
Write-path limitations for cumulative directives are discussed in the
.Sx LIMITATIONS
section of
.Xr bsdconf_put 3 .
.Sh SECURITY CONSIDERATIONS
Parsing allocates buffers sized by the longest directive and value
encountered rather than by untrusted length fields,
and
.Fn bsdconf_fparse
accepts an already-open descriptor precisely so that a caller may
sandbox itself
.Pq for example with Xr capsicum 4
before touching untrusted input,
as
.Xr sysconf 8
does for its read-only operations.
Write-path hardening is documented in
.Xr bsdconf_put 3 .
+733
View File
@@ -0,0 +1,733 @@
/*
* Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "bsdconf.h"
#include "bsdconf_internal.h"
/*
* Search for a config option (struct bsdconf_option) in the array of config
* options, returning the struct whose directive matches the given parameter.
* If no match is found, NULL is returned.
*
* This is to eliminate dependency on the index position of an item in the
* array, since the index position is more apt to be changed as code grows.
*/
struct bsdconf_option *
bsdconf_get_option(struct bsdconf_option options[], const char *directive)
{
uint32_t n;
if (options == NULL || directive == NULL)
return (NULL);
for (n = 0; options[n].directive != NULL; n++)
if (strcmp(options[n].directive, directive) == 0)
return (&options[n]);
return (NULL);
}
/*
* Strip one layer of surrounding double-quotes from `value' in place (the
* parser preserves quotes so that values round-trip losslessly; see
* bsdconf_fparse() below). Returns `value' for convenience. If the value is
* not a quoted string, it is returned unmodified.
*/
char *
bsdconf_unquote(char *value)
{
size_t len;
if (value == NULL || (len = strlen(value)) < 2)
return (value);
if (value[0] != '"' || value[len - 1] != '"')
return (value);
memmove(value, value + 1, len - 2);
value[len - 2] = '\0';
return (value);
}
/*
* Copy the remaining contents of the open file descriptor `fd' to an
* unlinked temporary file and return a seekable descriptor referencing it
* (which the caller must close(2); the backing storage is reclaimed then).
* This adapts input that cannot seek -- a pipe or socket, standard input
* included -- for the scanner in bsdconf_fparse() below, which seeks
* liberally. Returns the new descriptor on success; otherwise returns -1
* and errno should be consulted.
*/
int
bsdconf_spool(int fd)
{
FILE *tmp;
int error;
int newfd;
int tfd;
ssize_t r;
char buf[8192];
if ((tmp = tmpfile()) == NULL)
return (-1);
tfd = fileno(tmp);
for (;;) {
r = read(fd, buf, sizeof(buf));
if (r < 0) {
if (errno == EINTR)
continue;
goto fail;
}
if (r == 0)
break;
if (bsdconf_writeall(tfd, buf, (size_t)r) != 0)
goto fail;
}
if (lseek(tfd, 0, SEEK_SET) == -1)
goto fail;
/* Detach the descriptor from the stream before closing it */
if ((newfd = dup(tfd)) == -1)
goto fail;
fclose(tmp);
return (newfd);
fail:
error = errno; /* preserve errno across fclose(3) */
fclose(tmp);
errno = error;
return (-1);
}
/*
* Read one byte into `*p', restarting on EINTR. Returns 1 on success, 0 on
* EOF, or -1 on error (with errno set). Callers must treat a negative return
* as failure: a loop conditioned only on `r != 0' spins forever on error
* because read(2) returns -1, and a length counter in such a loop can grow
* without bound (see the directive scan in bsdconf_fparse() below).
*/
static ssize_t
bsdconf_read1(int fd, char *p)
{
ssize_t r;
do {
r = read(fd, p, 1);
} while (r < 0 && errno == EINTR);
return (r);
}
/*
* Read exactly `n' bytes into `buf', restarting on EINTR. Returns 0 on
* success, or -1 on error / premature EOF (with errno set; EIO for a short
* read after the caller measured a length on a seekable descriptor).
*/
static int
bsdconf_readn(int fd, void *buf, size_t n)
{
char *p = buf;
size_t off = 0;
ssize_t r;
while (off < n) {
r = read(fd, p + off, n - off);
if (r < 0) {
if (errno == EINTR)
continue;
return (-1);
}
if (r == 0) {
errno = EIO;
return (-1);
}
off += (size_t)r;
}
return (0);
}
/*
* Advance past horizontal whitespace (spaces and tabs, not newline).
* Updates `*r' and the byte in `*p'. Returns 0 on success, or -1 on
* read error (errno set).
*/
static int
bsdconf_skip_hspace(int fd, char *p, ssize_t *r)
{
while (*r > 0 && isspace((unsigned char)*p) && *p != '\n') {
*r = bsdconf_read1(fd, p);
if (*r < 0)
return (-1);
}
return (0);
}
/*
* Truncate trailing whitespace from a NUL-terminated string whose end
* (the NUL) is at `end'. Returns a pointer to the last remaining
* character, or to `value' when the string is empty.
*/
static char *
bsdconf_rtrim_ws(char *value, char *end)
{
char *t = end;
while (t > value && isspace((unsigned char)*--t))
*t = '\0';
return (t);
}
/*
* Drop a trailing inline `#' or unescaped `;' that rode along with the
* value (historic figpar behavior), then trim again. `ecomment' is set
* when the end-key scan stopped on an unquoted `#'.
*/
static char *
bsdconf_trim_value_key(char *value, char *t, bool ecomment, bool bsemicolon)
{
uint32_t x;
if (ecomment && t > value && *t == '#') {
*t = '\0';
return (bsdconf_rtrim_ws(value, t));
}
if (bsemicolon && t > value && *t == ';') {
for (x = 0; t - x > value && *(t - x - 1) == '\\'; x++)
;
if ((x & 1) == 0) {
*t = '\0';
return (bsdconf_rtrim_ws(value, t));
}
}
return (t);
}
/*
* Invoke the unknown-directive call-back with a stack-local option that
* carries the statement's assignment operator (there is no matched
* options[] slot to hang it on). Returns the call-back's result.
*/
static int
bsdconf_call_unknown(int (*unknown)(struct bsdconf_option *option,
uint32_t line, char *directive, char *value), enum bsdconf_op op,
uint32_t dline, char *directive, char *value)
{
struct bsdconf_option unk;
memset(&unk, 0, sizeof(unk));
unk.op = op;
return (unknown(&unk, dline, directive, value));
}
/*
* Scan from the current byte in `*p' to the end of the value. Handles
* quotes, escaped end-keys, inline comments, and semicolon terminators.
* On return, `*p' holds the terminating key (or is at EOF), and `*r',
* `*line', `*comment', and `*ecomment' are updated. Returns 0 on
* success, or -1 on seek/read error (errno set).
*/
static int
bsdconf_scan_value_end(int fd, char *p, ssize_t *r, uint32_t *line,
uint8_t *comment, uint8_t *ecomment, bool bsemicolon)
{
uint8_t end = 0;
uint8_t quote = 0;
uint32_t n;
off_t charpos;
*ecomment = 0;
while (*r > 0 && end == 0) {
/* Advance to the next character if we know we can */
if (*p != '\"' && *p != '#' && *p != '\n' &&
(!bsemicolon || *p != ';')) {
*r = bsdconf_read1(fd, p);
if (*r < 0)
return (-1);
continue;
}
/*
* If we get this far, we've hit an end-key
*/
/* Get the current offset */
if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
return (-1);
charpos--;
/*
* Go back so we can read the character before the key to
* check if the character is escaped (which means we should
* continue).
*/
if (lseek(fd, -2, SEEK_CUR) == -1)
return (-1);
*r = bsdconf_read1(fd, p);
if (*r < 0)
return (-1);
/*
* Count how many backslashes there are (an odd number means
* the key is escaped, even means otherwise).
*/
for (n = 1; *r > 0 && *p == '\\'; n++) {
/* Move back another offset to read */
if (lseek(fd, -2, SEEK_CUR) == -1)
return (-1);
*r = bsdconf_read1(fd, p);
if (*r < 0)
return (-1);
}
/* Move offset back to the key and read it */
if (lseek(fd, charpos, SEEK_SET) == -1)
return (-1);
*r = bsdconf_read1(fd, p);
if (*r < 0)
return (-1);
/*
* If an even number of backslashes was counted meaning key
* is not escaped, we should evaluate what to do.
*/
if ((n & 1) == 1) {
switch (*p) {
case '\"':
/*
* Flag current sequence of characters to
* follow as being quoted (hashes are not
* considered comments).
*/
quote = !quote;
break;
case '#':
/*
* If we aren't in a quoted series, we just
* hit an inline comment and have found the
* end of the value. Flag the remainder of
* the line as a comment so it is not
* mistaken for a new directive.
*/
if (!quote) {
*ecomment = *comment = 1;
end = 1;
}
break;
case '\n':
/*
* Newline characters must always be escaped,
* whether inside a quoted series or not,
* otherwise they terminate the value.
*/
(*line)++;
end = 1;
/* FALLTHROUGH */
case ';':
if (!quote && bsemicolon)
end = 1;
break;
}
} else if (*p == '\n')
/* Escaped newline character. increment */
(*line)++;
/* Advance to the next character */
*r = bsdconf_read1(fd, p);
if (*r < 0)
return (-1);
}
return (0);
}
/*
* 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)
{
bool bequals;
bool bsemicolon;
bool case_sensitive;
bool operator_equals;
bool require_equals;
bool strict_equals;
uint8_t comment = 0;
uint8_t ecomment;
uint8_t found;
uint8_t have_equals = 0;
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;
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;
/* Read the file until EOF */
while (r > 0) {
r = bsdconf_read1(fd, p);
if (r < 0)
goto fail;
/* Skip to the beginning of a directive */
while (r > 0 && (isspace((unsigned char)*p) || *p == '#' ||
comment || (bsemicolon && *p == ';'))) {
if (*p == '#')
comment = 1;
else if (*p == '\n') {
comment = 0;
line++;
}
r = bsdconf_read1(fd, p);
if (r < 0)
goto fail;
}
/* 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((unsigned char)*p))
break;
if (bequals && *p == '=') {
have_equals = 1;
break;
}
if (bsemicolon && *p == ';')
break;
r = bsdconf_read1(fd, p);
if (r < 0)
goto fail;
}
/* 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;
}
if (bsdconf_readn(fd, directive, n) != 0)
goto fail;
/* Advance beyond the equals sign if appropriate/desired */
if (bequals && *p == '=') {
if (lseek(fd, 1, SEEK_CUR) != -1) {
r = bsdconf_read1(fd, p);
if (r < 0)
goto fail;
}
if (strict_equals && isspace((unsigned char)*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 == '=')) {
if (bsdconf_skip_hspace(fd, p, &r) != 0)
goto fail;
}
/* An equals sign may have stopped us, should we eat it? */
if (r > 0 && bequals && *p == '=' && !strict_equals) {
have_equals = 1;
r = bsdconf_read1(fd, p);
if (r < 0)
goto fail;
if (bsdconf_skip_hspace(fd, p, &r) != 0)
goto fail;
}
/* 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 */
if (bsdconf_scan_value_end(fd, p, &r, &line, &comment,
&ecomment, bsemicolon) != 0)
goto fail;
/* 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;
}
if (bsdconf_readn(fd, value, n) != 0)
goto fail;
/* Terminate the string */
value[n] = '\0';
/* Cut trailing whitespace and a trailing `#' / `;' key */
t = bsdconf_rtrim_ws(value, value + n);
t = bsdconf_trim_value_key(value, t, ecomment != 0,
bsemicolon);
/* 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, vsize + 1, "\\\"", "\\\\\"") < 0)
goto fail;
/* Remove all escaped newline characters */
if (bsdconf_replaceall(value, vsize + 1, "\\\n", "") < 0)
goto fail;
/* Resolve escape sequences */
bsdconf_strunexpand(value, value);
call_function:
/* Abort if we're seeking only assignments */
if (require_equals && !have_equals) {
errno = EINVAL;
goto fail;
}
found = have_equals = 0; /* reset */
/*
* Report the statement's assignment operator through a
* stack-local option when invoking the unknown call-back
* (there is no matched options[] slot to hang it on).
*/
if (options == NULL && unknown != NULL) {
error = bsdconf_call_unknown(unknown, op, 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 = bsdconf_call_unknown(unknown, op, 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;
/* Sanity check: if no options and no unknown function, return */
if (path == NULL || (options == NULL && unknown == NULL)) {
errno = EINVAL;
return (-1);
}
/* Open the file */
if ((fd = open(path, O_RDONLY)) < 0)
return (-1);
error = bsdconf_fparse(options, fd, unknown, processing_options);
close(fd);
return (error);
}
+281
View File
@@ -0,0 +1,281 @@
/*
* Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#ifndef _BSDCONF_H_
#define _BSDCONF_H_
#ifdef __FreeBSD__
#include <sys/cdefs.h>
#endif
#include <stdbool.h>
#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).
* Signed and unsigned 32- and 64-bit members match sysctl(3) CTLTYPE widths.
* Until CTLTYPE grows a floating-point kind, the union needs no float member;
* a value that happens to contain a decimal point is still read and written
* as a string.
*/
union bsdconf_value {
void *data; /* Opaque pointer (DATA1..DATA3) */
char *str; /* Pointer to NUL-terminated string */
char **strarray; /* Pointer to an array of strings */
int32_t num; /* Signed 32-bit integer value */
uint32_t u_num; /* Unsigned 32-bit integer value */
int64_t num64; /* Signed 64-bit integer value */
uint64_t u_num64; /* Unsigned 64-bit integer value */
bool boolean; /* Boolean value */
};
/*
* 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_INT64 = 0x0100, /* signed 64-bit integer */
BSDCONF_TYPE_UINT64 = 0x0200, /* unsigned 64-bit integer */
BSDCONF_TYPE_RESERVED = 0x0400, /* reserved */
};
/*
* 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 build triad; 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
*/
enum bsdconf_processing {
BSDCONF_BREAK_ON_EQUALS = 0x0001, /* stop at `=' */
BSDCONF_BREAK_ON_SEMICOLON = 0x0002, /* `;' starts a new line */
BSDCONF_CASE_SENSITIVE = 0x0004, /* applies to directives */
BSDCONF_REQUIRE_EQUALS = 0x0008, /* assignment directives */
BSDCONF_STRICT_EQUALS = 0x0010, /* `=' part of directive */
BSDCONF_OPERATOR_EQUALS = 0x0020, /* `+=' `?=' `:=' `!=' */
};
/*
* Options to bsdconf_put() for put_options bitmask
*/
enum bsdconf_put_flags {
BSDCONF_PUT_NO_DUPLICATES = 0x0001, /* error if found twice */
BSDCONF_PUT_ALLOW_EMPTY = 0x0002, /* allow empty SET_VALUE */
BSDCONF_PUT_BACKUP = 0x0004, /* back up file (`.bak') */
BSDCONF_PUT_UNQUOTED = 0x0008, /* value.str as file text */
BSDCONF_PUT_QUOTE_ALWAYS = 0x0010, /* always quote on output */
};
/*
* Per-directive actions for bsdconf_put()
*/
enum bsdconf_action {
BSDCONF_ACTION_SET_VALUE = 0x0000, /* set/replace (default) */
BSDCONF_ACTION_CHECK = 0x0001, /* compare against current */
BSDCONF_ACTION_REMOVE = 0x0002, /* remove from config */
};
/*
* Per-directive result codes set by bsdconf_put()
*/
enum bsdconf_result {
BSDCONF_DIRECTIVE_FOUND = 0x0001, /* vs not found (see added) */
BSDCONF_VALUE_CHANGED = 0x0002, /* vs no change required */
BSDCONF_DIRECTIVE_ADDED = 0x0004, /* vs already existed */
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. A non-zero `match_line' restricts the
* put to the statement on that physical line (0 matches any, as before);
* make(1) `+=' SET_VALUE still appends a new line when `match_line' is 0,
* but rewrites the matched statement when `match_line' selects it.
*/
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() */
uint32_t match_line; /* put: 0=any, else only line */
/*
* 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);
};
/*
* 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_ */
+560
View File
@@ -0,0 +1,560 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
.Dt BSDCONF_FORMAT 3
.Os
.Sh NAME
.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 format descriptors and discovery
.Sh LIBRARY
.Lb libbsdconf
.Sh SYNOPSIS
.In bsdconf.h
.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
Every supported configuration file format is described by the same small
format descriptor
.Pq introduced in Xr bsdconf 3 :
.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
.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
.Pq as for Dv BSDCONF_FORMAT_MAKE
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 ,
.Dv BSDCONF_FORMAT_SYSCTL ,
and
.Dv BSDCONF_FORMAT_SRC
.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
.Pa /etc/src-env.conf ,
.Pa /etc/make.conf ,
then
.Pa /etc/src.conf
.Pq the /usr/src build triad; see Xr src.conf 5 ;
new writes prefer
.Pa src.conf .
.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 see Xr loader.conf 5 ,
and
.Dv BSDCONF_FORMAT_LOADER
names that defaults file and those three directives so that
.Fn bsdconf_format_files
matches that discovery:
the defaults file is read,
.Va loader_conf_files
is chased
.Po
re-read after every file, as the loader does when a file queues
additional names
.Pc ,
and the drop-in directories and local files named by the final
.Va loader_conf_dirs
and
.Va local_loader_conf_files
values are appended
.Pq the loader likewise applies those only after the file-list walk .
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.
The
.Dv BSDCONF_FORMAT_SRC
triad
.Po
.Pa src-env.conf ,
.Pa make.conf ,
.Pa src.conf
.Pc
shares that
.Pa make.conf
syntax exactly
.Pq all three are read by Xr make 1 when building /usr/src ,
including the empty value,
which for
.Pa src.conf
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
.Xr bsdconf 3
.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 only partly covered by
.Va match_line
selection in
.Xr bsdconf_put 3 ;
higher-level additive policy remains the caller's concern
.Pq see that page's Sx LIMITATIONS .
.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
runtime 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 DESCRIPTION
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
.Po which a
.Fn parse
callback may split further by its own rules
.Pc ,
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_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
Bolt on a new format at runtime by deriving from the built-in it most
resembles,
then set a directive in its file with full 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 bsdconf 3 ,
.Xr bsdconf_put 3 ,
.Xr loader.conf 5 ,
.Xr src.conf 5 ,
.Xr sysctl.conf 5 ,
.Xr sysconf 8
.Sh HISTORY
The format descriptor interface first appeared in
.Fx 16.0
as part of
.Xr bsdconf 3 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+693
View File
@@ -0,0 +1,693 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <libgen.h>
#include <limits.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,
};
#define BSDCONF_NFORMATS \
(sizeof(bsdconf_formats) / sizeof(*bsdconf_formats))
#define BSDCONF_CONF_SUFFIX ".conf"
#define BSDCONF_CONF_SUFFIX_LEN (sizeof(BSDCONF_CONF_SUFFIX) - 1)
/*
* 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, BSDCONF_CONF_SUFFIX,
BSDCONF_CONF_SUFFIX_LEN) == 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;
char tmp[PATH_MAX];
char *base;
if (path == NULL)
return (BSDCONF_FORMAT_GENERIC);
if (strlen(path) >= sizeof(tmp))
return (BSDCONF_FORMAT_GENERIC);
memcpy(tmp, path, strlen(path) + 1);
base = basename(tmp);
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 > BSDCONF_CONF_SUFFIX_LEN &&
strcmp(entry->d_name + len - BSDCONF_CONF_SUFFIX_LEN,
BSDCONF_CONF_SUFFIX) == 0);
}
/*
* Compose the full path `prefix' + `path' [+ `/' + `name' [+ `.conf']] into
* newly allocated storage. Returns NULL on allocation failure (errno set).
*
* asprintf(3) is not in POSIX.1-2008; size with snprintf(NULL, 0) then fill
* so the library still compiles under _POSIX_C_SOURCE=200809L.
*/
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) -- re-reading it after every file, as the loader does
* when a file queues additional names -- and finally append the drop-in
* directory entries and local files named by the final values of the other
* two list directives (likewise applied only after the file-list walk).
* 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, BSDCONF_CONF_SUFFIX);
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);
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* 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,
};
+51
View File
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* 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,
};
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* 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.
* A put of `+=' appends a new `name+=value' line rather than rewriting a
* prior assignment (see bsdconf_put()); callers that accumulate effective
* values honor the same operators on read. sysconf(8) `name-=word' strikes
* rewrite or remove a specific prior statement via match_line (make has no
* `-=` operator of its own).
*/
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,
};
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "bsdconf_formats.h"
/*
* BSDCONF_FORMAT_SRC: /usr/src build audience
*
* Make(1) syntax, matching make.conf(5) / src.conf(5). The FreeBSD source
* tree consults three configuration files in order (see src.sys.env.mk,
* sys.mk, and src.sys.mk): src-env.conf, make.conf, then src.conf. New
* directives are written to src.conf (def->path); later files override
* earlier ones for `=' and accumulate for `+='. Environment overrides
* (SRC_ENV_CONF, __MAKE_CONF, SRCCONF) are applied by sysconf(8).
*
* sys.mk / local.sys.mk and `.if' / control flow are intentionally out of
* scope: conf files may contain them, but this format does not evaluate
* them (see LIMITATIONS in sysconf(8)).
*/
static const struct bsdconf_source bsdconf_src_sources[] = {
{ BSDCONF_SOURCE_FILE, "/etc/src-env.conf" },
{ BSDCONF_SOURCE_FILE, "/etc/make.conf" },
{ BSDCONF_SOURCE_FILE, "/etc/src.conf" },
{ BSDCONF_SOURCE_FILE, NULL },
};
const struct bsdconf_format_def bsdconf_format_src_def = {
.keyword = "src",
.path = "/etc/src.conf",
.sources = bsdconf_src_sources,
.processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
BSDCONF_OPERATOR_EQUALS,
.put = BSDCONF_PUT_UNQUOTED | BSDCONF_PUT_ALLOW_EMPTY,
};
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* 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,
};
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* 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_sysctl_def;
#endif /* !_BSDCONF_FORMATS_H_ */
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2001-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#ifndef _BSDCONF_INTERNAL_H_
#define _BSDCONF_INTERNAL_H_
#include <sys/stat.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "bsdconf.h"
/*
* Private interfaces shared between the libbsdconf translation units.
* This header is deliberately not installed.
*/
/* String manipulation routines (bsdconf_string.c) */
void bsdconf_strunexpand(char *_dst, const char *_src);
void bsdconf_strtolower(char *_buf);
int bsdconf_replaceall(char *_buf, size_t _buflen,
const char *_find, const char *_replace);
unsigned int bsdconf_strcount(const char *_source, const char *_find);
/*
* 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 */
bool have_value; /* a value was present */
bool have_equals; /* an `=' separated directive/value */
};
/* Statement scan / put helpers (bsdconf_stmt.c) */
const char *bsdconf_op_token(enum bsdconf_op _op);
int bsdconf_writeall(int _fd, const void *_data, size_t _len);
char *bsdconf_readfile(int _fd, size_t _size, size_t *_lenp);
int bsdconf_emit(int _fd, const void *_data, size_t _len,
int *_last);
int bsdconf_ensure_tmp(int *_tmpfdp, char *_tpath, size_t _tpathsz,
const char *_rpath, const struct stat *_sb);
char *bsdconf_format_value(const struct bsdconf_option *_option,
bool _unquoted, bool _quote_always, bool _bsemicolon);
int bsdconf_value_empty(const struct bsdconf_option *_option);
int bsdconf_dir_matches(const char *_buf, size_t _start,
size_t _end, const char *_directive, bool _case_sensitive);
int bsdconf_scan(const char *_buf, size_t _len, size_t *_ip,
uint32_t *_linep, bool _bequals, bool _bsemicolon,
bool _strict_equals, bool _operator_equals,
struct bsdconf_stmt *_st);
#endif /* !_BSDCONF_INTERNAL_H_ */
+290
View File
@@ -0,0 +1,290 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
.Dt BSDCONF_PUT 3
.Os
.Sh NAME
.Nm bsdconf_put ,
.Nm bsdconf_set_option
.Nd resilient configuration file writing
.Sh LIBRARY
.Lb libbsdconf
.Sh SYNOPSIS
.In bsdconf.h
.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
.Sh DESCRIPTION
.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 .
A non-zero
.Va match_line
on input restricts the put to the statement on that physical line
.Pq zero matches any statement, as before ;
this allows a single
.Ql name+=value
among several to be rewritten or removed without appending a new line
or touching the others
.Pq make(1) list-strike edits in Xr sysconf 8 .
.Pp
The
.Fa path
argument to
.Fn bsdconf_put
must name a regular file
.Pq only a regular file can be atomically replaced ;
anything else is rejected with
.Er EINVAL .
When every option is
.Dv BSDCONF_ACTION_CHECK ,
or every
.Dv BSDCONF_ACTION_SET_VALUE
and
.Dv BSDCONF_ACTION_REMOVE
would leave the file unchanged,
the original is left untouched:
no temporary is created,
.Va mtime
is not bumped,
and hard links are not severed.
Otherwise 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 Xr bsdconf_format 3 ,
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 effective 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
.Va value.str
as literal file text:
no quotes are added and no characters are backslash-escaped
.Pq the caller supplies the bytes that should appear on disk .
Embedded whitespace is fine
.Pq the value runs to the end of the line .
A value that could not round-trip under the parser
.Pq an embedded newline, unescaped comment marker, or trailing unescaped backslash
is rejected with
.Er EINVAL
rather than silently corrupting the file.
The reader still runs
.Fn bsdconf_strunexpand
on input, so escape sequences present in the file become the logical value
on read; this flag does not re-encode them on write.
.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 RETURN VALUES
.Fn bsdconf_put
returns zero on success;
otherwise -1 is returned and the global variable
.Va errno
is set to indicate the error.
.Fn bsdconf_set_option
returns 1 if a matching directive was staged,
otherwise 0.
.Sh SEE ALSO
.Xr bsdconf 3 ,
.Xr bsdconf_format 3 ,
.Xr sysconf 8
.Sh HISTORY
The
.Fn bsdconf_put
and
.Fn bsdconf_set_option
functions first appeared in
.Fx 16.0
as part of
.Xr bsdconf 3 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
.Sh LIMITATIONS
.Fn bsdconf_put
matches directives by exact name and, by default, treats each name as
single-valued: the first matching statement is rewritten, or a new line
is appended when none matches.
That suits the built-in formats when the last assignment wins.
.Pp
Make(1)-style cumulative
.Ql +=
chains are supported only in part.
With
.Va match_line
unset, a
.Ql +=
.Dv BSDCONF_ACTION_SET_VALUE
appends a new statement rather than collapsing earlier ones; with
.Va match_line
set, one physical line can be rewritten or removed
.Pq as Xr sysconf 8 does for make/src list strikes .
Word-level
.Ql -=
policy and effective-value accumulation remain the caller's
responsibility;
the library does not implement
.Ql -=
itself.
.Pp
Formats whose directives legitimately repeat without make(1) operators
.Pq for example Apache-style cumulative keywords; see Xr bsdconf 3
likewise parse with caller-supplied callbacks, but
.Fn bsdconf_put
offers no first-class insert, remove, or reorder of occurrence
.Em N
among equals—only exact-name match or
.Va match_line
selection.
.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
.Pq the original mode masked to 0666, so setuid, setgid, and execute bits are not copied
and
.Xr fchown 2
on the open descriptor,
so a concurrently swapped file cannot influence them.
A backup requested with
.Dv BSDCONF_PUT_BACKUP
is created mode 0600 then
.Xr fchmod 2 Ns 'd
to the original mode masked to 0777
.Pq setuid and setgid bits are not restored
and refuses to follow a symbolic link planted at the
.Ql .bak
name
.Pq Dv O_NOFOLLOW .
.Pp
Symbolic links in the
.Fa path
argument to
.Fn bsdconf_put
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.
+537
View File
@@ -0,0 +1,537 @@
/*
* Copyright (c) 2015-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* 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 <unistd.h>
#include "bsdconf.h"
#include "bsdconf_internal.h"
/*
* 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).
*
* When every option is BSDCONF_ACTION_CHECK, or every SET_VALUE/REMOVE would
* leave the file unchanged, the original is left untouched: no temporary is
* created, mtime is not bumped, and hard links are not severed. Otherwise
* the file is replaced atomically: output is written to a temporary file in
* the same directory, flushed to disk with fsync(2), given the original's
* mode masked to 0666 (and, if permitted, its ownership), 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)
{
bool backup;
bool bequals;
bool bsemicolon;
bool case_sensitive;
bool emptyok;
bool nodup;
bool operator_equals;
bool quote_always;
bool require_equals;
bool strict_equals;
bool 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;
/*
* Walk the original statement by statement. The replacement temporary
* is created lazily on the first real edit (see bsdconf_ensure_tmp());
* CHECK and equal-value paths never open it. Bytes are emitted 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))
continue;
/*
* A non-zero match_line selects one physical
* statement (e.g. make(1) `+=' strike/edit of the
* last assignment containing a word).
*/
if (options[n].match_line != 0 &&
options[n].match_line != st.line)
continue;
option = &options[n];
break;
}
if (option == NULL)
continue; /* untouched; bytes flushed later */
/*
* make(1)-style `+=': when match_line is unset, never rewrite
* an existing statement and do not mark the directive FOUND,
* so a fresh `name+=value' line is appended below. Existing
* assignments for the same name are left intact so make's
* cumulative semantics are preserved. With match_line set,
* rewrite (or remove) that specific statement instead. The
* caller (sysconf(8)) skips the put entirely when an
* identical `+=value' line is already present.
*/
if (operator_equals &&
option->action == BSDCONF_ACTION_SET_VALUE &&
option->op == BSDCONF_OP_APPEND &&
option->match_line == 0)
continue;
/* 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_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
rpath, &sb) != 0)
goto cleanup;
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 (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
rpath, &sb) != 0)
goto cleanup;
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;
}
/*
* Append any SET_VALUE directives that were not found, and finalize
* the result flags for CHECK directives that were absent. The tail of
* the original is flushed only when a temporary already exists or an
* append forces one into being.
*/
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 */
if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
rpath, &sb) != 0)
goto cleanup;
/* Flush any unread prefix once before the first append */
if (wc < buflen) {
if (bsdconf_emit(tmpfd, buf + wc, buflen - wc,
&last_ch) != 0)
goto cleanup;
wc = buflen;
}
/* 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;
/*
* Line number of the statement appended at EOF: one past
* the number of newlines in the original, or the next line
* if we have to emit a separating newline first.
*/
{
uint32_t al = 1;
size_t k;
for (k = 0; k < buflen; k++)
if (buf[k] == '\n')
al++;
if (buflen > 0 && buf[buflen - 1] != '\n')
al++;
option->line = al;
}
option->result |=
BSDCONF_DIRECTIVE_ADDED | BSDCONF_VALUE_CHANGED;
}
/*
* Nothing changed: every option was CHECK or an equal-value
* SET_VALUE/REMOVE-miss. Leave the original file alone.
*/
if (tmpfd < 0) {
rv = 0;
goto cleanup;
}
/* Flush any remaining unread tail of the original */
if (bsdconf_emit(tmpfd, buf + wc, buflen - wc, &last_ch) != 0)
goto cleanup;
/* 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, 0600)) < 0)
goto cleanup;
if (fchmod(bfd, sb.st_mode & 0777) != 0) {
saved_errno = errno;
close(bfd);
errno = saved_errno;
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);
}
+547
View File
@@ -0,0 +1,547 @@
/*
* Copyright (c) 2015-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Statement scanning, value rendering, and related helpers used by
* bsdconf_put(). Kept separate so bsdconf_put.c stays focused on the
* rewrite driver.
*/
#include <sys/stat.h>
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include "bsdconf.h"
#include "bsdconf_internal.h"
/*
* Map an assignment operator to its config file token. BSDCONF_OP_DEFAULT
* maps to the plain `=' token.
*/
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);
}
if (w == 0) {
errno = EIO;
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.
*/
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.
*/
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);
}
/*
* Lazily create the replacement temporary in the target's directory. Called
* on the first edit so that CHECK-only and equal-value SET_VALUE paths never
* open a writable descriptor (and never bump mtime or sever hard links).
*/
int
bsdconf_ensure_tmp(int *tmpfdp, char *tpath, size_t tpathsz,
const char *rpath, const struct stat *sb)
{
char *slash;
if (*tmpfdp >= 0)
return (0);
if ((slash = strrchr(rpath, '/')) != NULL) {
if (snprintf(tpath, tpathsz, "%.*s/.bsdconf.XXXXXXXXXX",
(int)(slash - rpath), rpath) >= (int)tpathsz) {
errno = ENAMETOOLONG;
return (-1);
}
} else if (snprintf(tpath, tpathsz, ".bsdconf.XXXXXXXXXX") >=
(int)tpathsz) {
errno = ENAMETOOLONG;
return (-1);
}
if ((*tmpfdp = mkstemp(tpath)) == -1) {
tpath[0] = '\0';
return (-1);
}
if (fchmod(*tmpfdp, sb->st_mode & 0666) != 0)
return (-1);
(void)fchown(*tmpfdp, sb->st_uid, sb->st_gid); /* best effort */
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, bool 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.
*
* After counting a run of backslashes, `p' already points at the following
* byte. An odd-length run escapes that byte (skip it); an even-length run
* leaves it unescaped, so it must be re-examined by the loop body below
* (do not `continue', which would advance past it a second time).
*/
static int
bsdconf_verbatim_ok(const char *s, bool 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 ((nbs & 1) != 0) {
/* Odd run: the following byte is escaped */
if (*p == '\n')
return (0);
continue;
}
/* Even run: *p is unescaped; fall through */
}
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 / BSDCONF_PUT_UNQUOTED) `value.str' is emitted as
* literal file text -- no quotes are added and no characters are
* backslash-escaped -- so the caller supplies the bytes that should appear
* on disk. Embedded whitespace is fine (the value runs to the end of the
* line); a value that could not round-trip under the parser (embedded
* newline, unescaped comment marker, or trailing unescaped backslash) is
* rejected (errno = EINVAL) rather than silently corrupting the file. Note
* the asymmetry with the reader, which still runs bsdconf_strunexpand() on
* input: escape sequences present in the file become the logical value on
* read, but this path does not re-encode them. 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.
*/
char *
bsdconf_format_value(const struct bsdconf_option *option, bool unquoted,
bool quote_always, bool 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_INT64 ||
option->type == BSDCONF_TYPE_UINT64 ||
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.
*/
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.
*/
int
bsdconf_dir_matches(const char *buf, size_t start, size_t end,
const char *directive, bool case_sensitive)
{
size_t len = end - start;
if (strlen(directive) != len)
return (0);
if (case_sensitive)
return (memcmp(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.
*/
int
bsdconf_scan(const char *buf, size_t len, size_t *ip, uint32_t *linep,
bool bequals, bool bsemicolon, bool strict_equals, bool operator_equals,
struct bsdconf_stmt *st)
{
size_t i = *ip;
size_t j;
uint32_t line = *linep;
bool comment = false;
bool novalue = false;
bool quote = false;
/* 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 = true;
else if (buf[i] == '\n') {
comment = false;
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 = false;
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 = true;
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 = true; /* 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 = true;
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 = false;
st->val_start = st->val_end = i;
} else {
st->have_value = true;
st->val_start = i;
quote = false;
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);
}
+245
View File
@@ -0,0 +1,245 @@
/*
* Copyright (c) 2001-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <ctype.h>
#include <errno.h>
#include <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 buffer needs to be
* for a bsdconf_replaceall() series.
*/
unsigned int
bsdconf_strcount(const char *source, const char *find)
{
const char *p;
size_t flen;
unsigned int n = 0;
if (source == NULL || find == NULL || *source == '\0' || *find == '\0')
return (0);
flen = strlen(find);
for (p = source; (p = strstr(p, find)) != NULL; p += flen)
n++;
return (n);
}
/*
* Replaces all occurrences of `find' in `buf' with `replace'.
*
* `buf' must point to a mutable buffer of at least `buflen' bytes (including
* space for the terminating NUL). A string constant will not compile as the
* first argument without a cast; do not cast one in. A local or global
* non-const array is fine.
*
* The result is always built in a temporary buffer and copied back, so the
* same path is taken whether `replace' is longer or shorter than `find'.
* Pass a `buflen' large enough for the expanded result (bsdconf_strcount()
* can size it); if the result would not fit, -1 is returned with errno set
* to ENOSPC and `buf' is left unmodified. On success the return value is
* the length (in bytes) of the result, not counting the terminating NUL.
*
* When an error occurs, -1 is returned and the global variable errno is set
* accordingly.
*/
int
bsdconf_replaceall(char *buf, size_t buflen, const char *find,
const char *replace)
{
char *dst;
char *out;
const char *hit;
const char *src;
size_t flen, need, rlen, slen;
unsigned int n;
if (buf == NULL)
return (0);
if (find == NULL)
return ((int)strlen(buf));
slen = strlen(buf);
flen = strlen(find);
rlen = replace != NULL ? strlen(replace) : 0;
if (slen == 0 || flen == 0 || slen < flen)
return ((int)slen);
n = bsdconf_strcount(buf, find);
if (n == 0)
return ((int)slen);
if (rlen >= flen)
need = slen + (size_t)n * (rlen - flen) + 1;
else
need = slen - (size_t)n * (flen - rlen) + 1;
if (need > buflen) {
errno = ENOSPC;
return (-1);
}
if ((out = malloc(need)) == NULL)
return (-1);
dst = out;
src = buf;
while ((hit = strstr(src, find)) != NULL) {
memcpy(dst, src, (size_t)(hit - src));
dst += hit - src;
if (rlen > 0) {
memcpy(dst, replace, rlen);
dst += rlen;
}
src = hit + flen;
}
memcpy(dst, src, strlen(src) + 1);
memcpy(buf, out, need);
free(out);
return ((int)(need - 1));
}
/*
* Unexpands (collapses) C-style escape sequences in `src' into `dst'.
*
* The result is never longer than the input, so `dst' may be the same buffer
* as `src' (cf. strunvis(3)). Do not pass a string constant as `dst'; a local
* or global non-const array is fine.
*
* Interpreted sequences are:
*
* \NNN character with octal value NNN (1 to 3 digits)
* \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 '\#'). A trailing backslash
* or a `\x' with no following hex digits is emitted verbatim (the backslash
* is dropped for `\x', leaving `x').
*/
void
bsdconf_strunexpand(char *dst, const char *src)
{
char *d;
const char *s;
unsigned int n, v;
unsigned char c;
d = dst;
s = src;
/*
* Loop until we hit the end of the input. The stop condition must
* track the input cursor (s): collapsing an escape advances s ahead
* of the output cursor (d), so once the two diverge *d no longer
* reflects where the input terminates.
*/
while (*s != '\0') {
if (*s != '\\') {
*d++ = *s++;
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 (*(s + 1) == '\0') {
*d++ = *s++;
break;
}
/* Replace the backslash with the correct character */
s++;
switch (*s) {
case 'a': *d = '\a'; break; /* bell/alert (BEL) */
case 'b': *d = '\b'; break; /* backspace */
case 'f': *d = '\f'; break; /* form feed */
case 'n': *d = '\n'; break; /* new line */
case 'r': *d = '\r'; break; /* carriage return */
case 't': *d = '\t'; break; /* horizontal tab */
case 'v': *d = '\v'; break; /* vertical tab */
case 'x': /* hex value (1 to 2 digits)(\xNN) */
v = 0;
n = 0;
while (n < 2) {
c = (unsigned char)*(s + 1);
if (c >= '0' && c <= '9')
v = (v << 4) + (c - '0');
else if (c >= 'A' && c <= 'F')
v = (v << 4) + (c - 'A' + 10);
else if (c >= 'a' && c <= 'f')
v = (v << 4) + (c - 'a' + 10);
else
break;
s++;
n++;
}
/* \x with no digits: emit the 'x' (unknown escape) */
*d = (n == 0) ? 'x' : (char)v;
break;
default: /* octal (\NNN, 1 to 3 digits) or unknown sequence */
if (*s >= '0' && *s <= '7') {
v = (unsigned int)(*s - '0');
n = 1;
while (n < 3 && *(s + 1) >= '0' &&
*(s + 1) <= '7') {
s++;
v = (v << 3) +
(unsigned int)(*s - '0');
n++;
}
*d = (char)v;
} else
*d = *s;
break;
}
/* Increment to next offset, possible next escape sequence */
d++;
s++;
}
/*
* Terminate at the (possibly earlier) output cursor. When any
* escape was collapsed the string shrank, so the trailing bytes
* between d and s are now stale and must be cut off here.
*/
*d = '\0';
}
/*
* Convert a string to lower case. Pass a mutable buffer (a local or global
* non-const array is fine); do not pass a string constant.
*/
void
bsdconf_strtolower(char *buf)
{
char *p = buf;
if (buf == NULL)
return;
while (*p != '\0') {
*p = (char)tolower((unsigned char)*p);
p++;
}
}
+1
View File
@@ -30,6 +30,7 @@ LIBBLACKLIST?= ${LIBDESTDIR}${LIBDIR_BASE}/libblacklist.a
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
+1
View File
@@ -118,6 +118,7 @@ _LIBRARIES= \
be \
begemot \
bluetooth \
bsdconf \
bsdxml \
bsm \
bsnmp \
+1
View File
@@ -87,6 +87,7 @@ SUBDIR= adduser \
snapinfo \
spi \
spray \
sysconf \
syslogd \
sysrc \
tcpdrop \
+32
View File
@@ -0,0 +1,32 @@
.include <src.opts.mk>
PACKAGE= utilities
PROG= sysconf
MAN= sysconf.8 \
sysconf-generic.8 \
sysconf-loader.8 \
sysconf-make.8 \
sysconf-rc.8 \
sysconf-src.8 \
sysconf-sysctl.8 \
sysconf-targets.8
SRCS= sysconf.c \
sysconf_edit_assign.c \
sysconf_edit_make.c \
sysconf_edit_write.c \
sysconf_print.c \
sysconf_query_desc.c \
sysconf_query_sysctl.c \
sysconf_query_value.c \
sysconf_resolve.c \
sysconf_scan.c
CFLAGS+= -I${SRCTOP}/lib/libbsdconf -I${.CURDIR}
LIBADD= bsdconf jail
HAS_TESTS=
SUBDIR.${MK_TESTS}+= tests
.include <bsd.prog.mk>
+17
View File
@@ -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
+49
View File
@@ -0,0 +1,49 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF-GENERIC 8
.Os
.Sh NAME
.Nm "sysconf generic"
.Nd configure a file of no built-in format
.Sh SYNOPSIS
.Nm sysconf
.Cm generic
.Fl f Ar file
.Op Fl ceinNqvx
.Op Fl j Ar jail | Fl R Ar dir
.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
.Ar ...
.Sh DESCRIPTION
The
.Cm generic
target of
.Xr sysconf 8
exists for files matching no known format
.Pq quote values only when required to round-trip .
Having no default files of its own,
it always requires
.Fl f .
The format is never inferred from the path;
see
.Xr sysconf-targets 8 .
Shared options are in
.Xr sysconf 8 .
.Sh EXAMPLES
.Dl sysconf generic -f /usr/local/etc/myapp.conf loglevel=debug
.Sh SEE ALSO
.Xr bsdconf_format 3 ,
.Xr sysconf 8 ,
.Xr sysconf-targets 8
.Sh HISTORY
The
.Nm sysconf
utility first appeared in
.Fx 16.0 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+151
View File
@@ -0,0 +1,151 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF-LOADER 8
.Os
.Sh NAME
.Nm "sysconf loader"
.Nd configure boot loader directives
.Sh SYNOPSIS
.Nm sysconf
.Cm loader
.Op Fl AcDeFinNqvVx
.Op Fl j Ar jail | Fl R Ar dir
.Op Fl f Ar file
.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
.Ar ...
.Nm sysconf
.Cm loader
.Op Fl ADeFnNqvV
.Op Fl f Ar file
.Fl a
.Nm sysconf
.Cm loader
.Op Fl AaDinNq
.Fl d
.Op Ar name ...
.Nm sysconf
.Cm loader
.Op Fl E
.Fl l | L
.Sh DESCRIPTION
The
.Cm loader
target of
.Xr sysconf 8
reads and modifies
.Xr loader.conf 5
configuration.
Shared assignment syntax and options are documented in
.Xr sysconf 8 ;
multi-file write policy in
.Xr sysconf-targets 8 .
.Pp
The file list is not hardcoded:
just as the boot loader itself hardcodes only
.Pa /boot/defaults/loader.conf ,
.Nm sysconf
names that defaults file and the
.Va loader_conf_files ,
.Va loader_conf_dirs ,
and
.Va local_loader_conf_files
directives so that discovery matches the loader
.Pq see Xr bsdconf_format 3 and Xr loader.conf 5 .
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.
.Pp
Values written for this target are always enclosed in quotes
.Pq quoted or unquoted input is accepted
with no whitespace around the equals sign,
as demanded by the boot loader's reader.
.Pp
The defaults file
.Pa /boot/defaults/loader.conf
.Pq or Ev LOADER_DEFAULTS
additionally supports
.Fl d ,
.Fl D ,
and
.Fl A
.Pq see Xr sysconf-targets 8 .
Space-separated list directives such as
.Va kernels
use
.Ar name Ns += Ns Ar word
and
.Ar name Ns -= Ns Ar word
as described in
.Xr sysconf 8 .
.Sh ENVIRONMENT
.Bl -tag -width "LOADER_DEFAULTS"
.It Ev LOADER_DEFAULTS
Defaults file 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 and the authority on the file list.
.It Pa /boot/device.hints
First in the stock
.Va loader_conf_files
list.
.It Pa /boot/loader.conf
Stock write target.
.It Pa /boot/loader.conf.d/
Drop-in directory.
.It Pa /boot/loader.conf.local
Site-local overrides,
sourced last.
.El
.Sh EXAMPLES
.Dl sysconf loader zfs_load
.Dl sysconf loader zfs_load=\(dqYES\(dq
.Dl sysconf loader -l
.Dl sysconf loader -d verbose_loading
.Dl sysconf loader -Ad
.Dl sysconf loader -D verbose_loading
.Dl sysconf loader -aD
.Dl sysconf loader -A
.Dl sysconf loader kernels+=kernel_test
.Dl sysconf loader kernels-=kernel_test
.Dl sysconf -a loader
.Dl sysconf loader -f /tmp/loader.conf.new zfs_load=\(dqYES\(dq
.Sh SEE ALSO
.Xr bsdconf_format 3 ,
.Xr loader.conf 5 ,
.Xr sysconf 8 ,
.Xr sysconf-targets 8
.Sh HISTORY
The
.Nm sysconf
utility first appeared in
.Fx 16.0 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+231
View File
@@ -0,0 +1,231 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF-MAKE 8
.Os
.Sh NAME
.Nm "sysconf make"
.Nd configure make.conf directives
.Sh SYNOPSIS
.Nm sysconf
.Cm make
.Op Fl ceFinNqsvVx
.Op Fl j Ar jail | Fl R Ar dir
.Op Fl f Ar file
.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
.Ar ...
.Nm sysconf
.Cm make
.Op Fl eFnNqvV
.Op Fl f Ar file
.Fl a
.Nm sysconf
.Cm make
.Op Fl E
.Fl l | L
.Sh DESCRIPTION
The
.Cm make
target of
.Xr sysconf 8
reads and writes
.Xr make 1 Ns -syntax
configuration on
.Pa /etc/make.conf
by default,
or on any single file named with
.Fl f .
Shared options are in
.Xr sysconf 8 ;
make(1) limitations and multi-file policy for other targets in
.Xr sysconf-targets 8 .
.Pp
.Ql +=
is the
.Xr make 1
append operator:
a new
.Ql name+=value
line is appended
.Pq prior assignments for the same name are left intact ,
and reads report the cumulative effective value as
.Nm make Fl V
would.
Assignment modifiers may also be requested by suffixing the name,
as in
.Ql CFLAGS+=-DDEBUG .
.Pp
.Ql -=
is not a
.Xr make 1
operator;
it strikes each given word from the
.Em last
assignment whose value contains that word as a whole whitespace-separated
token
.Pq rewriting the statement, or deleting it when the value becomes empty ,
so successive strikes peel list fragments from the end of the assignment
chain without collapsing earlier
.Ql +=
lines.
.Pp
.Ql WITH_*
and
.Ql WITHOUT_*
presence knobs may be set with
.Fl s
.Pq preferred; writes Ql name=
or
.Ar name Ns = ;
reads report
.Ql name (present)
for any assigned value
.Po
.Fl e
prints the real assignment, e.g.\&
.Ql name=
or
.Ql name=t ;
.Fl n
prints that same value alone
.Pc .
.Fl s
is a no-op when the knob is already defined
.Pq any value ;
an explicit
.Ar name Ns =
.Pq including empty
always writes, and a change of placeholder echoes
.Ql name: old -> new .
.Fl s
does not clear an inverse
.Ql WITH_
or
.Ql WITHOUT_
for the same option
.Po
use
.Fl V
and clean up manually if needed
.Pc .
.Pp
Writing
.Ql WITH_ Ns Ar foo Ns =no
.Pq exact lowercase Ql no
still stores the assignment but warns to use
.Ql WITHOUT_ Ns Ar foo Ns =1 ,
matching the
.Ql .warning
in
.Pa share/mk/bsd.mkopt.mk .
.Nm
emits that advisory on write and on read
.Pq named queries and Fl a ,
with
.Ql file:line:
of the authoritative statement when known
.Pq suppressed by Fl q ,
because
.Xr make.conf 5
is read by
.Pa /usr/src
builds: a line such as
.Va WITH_MANCOMPRESS Ns =no
here is what produces
.Pp
.Dl warning: Use WITHOUT_MANCOMPRESS=1 instead of WITH_MANCOMPRESS=no
.Pp
when running
.Xr make 1
in
.Pa /usr/src
.Pq after Pa bsd.opts.mk reloads Pa bsd.mkopt.mk for common options .
The read-time form matters when the default presence display hides the
.Ql =no
value behind
.Ql (present) .
The same line stays silent under a bare
.Ql make -C /tmp
or ports, which never evaluate that option list.
Neither
.Nm
nor make warns for
.Ql WITHOUT_* Ns =no
or
.Ql WITH_* Ns =NO :
presence of
.Ql WITHOUT_
still disables, and
.Ql WITH_
with a value other than lowercase
.Ql no
does not match the check.
.Pp
.Va WITHOUT_MODULES
is not a presence knob:
its value is a module reject list, so
.Fl s
is rejected and reads print the module list rather than
.Ql (present) .
Set it with a normal assignment
.Pq e.g. Va WITHOUT_MODULES Ns = Ns Ar plip .
.Pp
This target has no defaults file;
.Fl d ,
.Fl D ,
and
.Fl A
are errors.
For the
.Pa /usr/src
build triad see
.Xr sysconf-src 8 .
.Sh FILES
.Bl -tag -width "/etc/make.conf" -compact
.It Pa /etc/make.conf
Default file for the
.Cm make
target.
.El
.Sh EXAMPLES
.Dl sysconf make CFLAGS+=-DDEBUG
.Dl sysconf make -s WITHOUT_MANCOMPRESS
.Dl sysconf make WITHOUT_MANCOMPRESS
.Pp
Strike a word from the last
.Ql +=
fragment that contains it
.Pq here removing Ql 3 from the final line only :
.Bd -literal -offset indent
bar+=1
bar+=1 2
bar+=1 2 3
.Ed
.Pp
.Dl sysconf make bar-=3
.Pp
leaves
.Ql bar+=1 2
on the last line.
Repeating with
.Ql bar-=2
and
.Ql bar-=1
peels that line further, then removes it when emptied.
.Sh SEE ALSO
.Xr make 1 ,
.Xr sysconf 8 ,
.Xr sysconf-src 8 ,
.Xr sysconf-targets 8
.Sh HISTORY
The
.Nm sysconf
utility first appeared in
.Fx 16.0 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+56
View File
@@ -0,0 +1,56 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF-RC 8
.Os
.Sh NAME
.Nm "sysconf rc"
.Nd pass configuration requests through to sysrc
.Sh SYNOPSIS
.Nm sysconf
.Cm rc
.Op Ar sysrc_argument ...
.Sh DESCRIPTION
The
.Cm rc
target of
.Xr sysconf 8
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 sysconf 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.
.Sh EXAMPLES
.Dl sysconf rc kld_list+=i915kms
.Sh SEE ALSO
.Xr rc.conf 5 ,
.Xr sysconf 8 ,
.Xr sysrc 8
.Sh HISTORY
The
.Nm sysconf
utility first appeared in
.Fx 16.0 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+211
View File
@@ -0,0 +1,211 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF-SRC 8
.Os
.Sh NAME
.Nm "sysconf src"
.Nd configure the src-env.conf / make.conf / src.conf triad
.Sh SYNOPSIS
.Nm sysconf
.Cm src
.Op Fl ceFinNqsvVx
.Op Fl j Ar jail | Fl R Ar dir
.Op Fl f Ar file
.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
.Ar ...
.Nm sysconf
.Cm src
.Op Fl eFnNqvV
.Op Fl f Ar file
.Fl a
.Nm sysconf
.Cm src
.Op Fl E
.Fl l | L
.Sh DESCRIPTION
The
.Cm src
target of
.Xr sysconf 8
is the
.Pa /usr/src
build audience:
it consults the same triad
.Xr make 1
uses when building the source tree
.Po
.Pa src-env.conf ,
then
.Pa make.conf ,
then
.Pa src.conf
.Pc ,
honoring
.Ev SRC_ENV_CONF ,
.Ev __MAKE_CONF ,
and
.Ev SRCCONF
when set.
See
.Xr bsdconf_format 3
and
.Xr src.conf 5 .
Shared options are in
.Xr sysconf 8 ;
authoritative-file and removal policy in
.Xr sysconf-targets 8 ;
.Ql +=
and
.Ql -=
semantics match
.Xr sysconf-make 8 .
.Pp
New directives are written to
.Pa src.conf
.Pq or the path in Ev SRCCONF ;
an existing definition is rewritten in the file that last set it.
There is no separate
.Ql src-env
keyword;
use
.Cm src
or
.Fl f
for surgical edits of
.Pa src-env.conf .
.Pp
.Ql WITH_*
and
.Ql WITHOUT_*
are presence knobs
.Po
.Xr src.conf 5
ignores the value; empty
.Ql name=
is the preferred spelling
.Pc .
Prefer
.Fl s
.Ar name
over
.Ar name Ns =
when only presence matters;
.Fl s
leaves an already-defined knob
.Pq any value
unchanged and echoes
.Ql (present) (unchanged) .
An explicit
.Ar name Ns =
.Pq including empty
always writes the requested value, and a change of placeholder echoes
.Ql name: old -> new .
.Fl s
appends to the preferred write file and does not remove an inverse
.Ql WITH_
or
.Ql WITHOUT_
for the same option.
Reads show
.Ql name (present)
for any assigned value;
with
.Fl e ,
the real assignment
.Po
e.g.
.Ql name=
or
.Ql name=t
.Pc ;
with
.Fl n ,
that same value alone
.Pq empty when the assignment is Ql name= .
Writing
.Ql WITH_ Ns Ar foo Ns =no
warns as on the
.Cm make
target
.Pq see Xr sysconf-make 8 ;
.Pa /usr/src
make evaluates such assignments via
.Pa bsd.opts.mk
and
.Pa src.opts.mk .
.Pp
.Va WITHOUT_MODULES
is not a presence knob:
its value is a module reject list, so
.Fl s
is rejected and reads print the module list rather than
.Ql (present) .
Set it with a normal assignment
.Pq e.g. Va WITHOUT_MODULES Ns = Ns Ar plip .
.Pp
This target has no defaults file;
.Fl d ,
.Fl D ,
and
.Fl A
are errors.
.Sh ENVIRONMENT
.Bl -tag -width "SRC_ENV_CONF"
.It Ev SRC_ENV_CONF
Path of
.Pa src-env.conf
.Pq default Pa /etc/src-env.conf ;
verbatim,
not subject to
.Fl R .
.It Ev __MAKE_CONF
Path of
.Pa make.conf
.Pq default Pa /etc/make.conf ;
verbatim,
not subject to
.Fl R .
.It Ev SRCCONF
Path of
.Pa src.conf
.Pq default Pa /etc/src.conf ;
verbatim,
not subject to
.Fl R .
Also the preferred write file for new directives.
.El
.Sh FILES
.Bl -tag -width "/etc/src-env.conf" -compact
.It Pa /etc/src-env.conf
First file of the triad.
.It Pa /etc/make.conf
Middle file of the triad.
.It Pa /etc/src.conf
Last file and preferred write target.
.El
.Sh EXAMPLES
.Dl sysconf src -s WITHOUT_LLDB
.Dl sysconf src WITHOUT_LLDB=
.Dl sysconf src WITHOUT_LLDB
.Dl sysconf src -x WITHOUT_LLDB
.Dl sysconf src -V CFLAGS
.Sh SEE ALSO
.Xr make 1 ,
.Xr bsdconf_format 3 ,
.Xr src.conf 5 ,
.Xr sysconf 8 ,
.Xr sysconf-make 8 ,
.Xr sysconf-targets 8
.Sh HISTORY
The
.Nm sysconf
utility first appeared in
.Fx 16.0 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+146
View File
@@ -0,0 +1,146 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF-SYSCTL 8
.Os
.Sh NAME
.Nm "sysconf sysctl"
.Nd configure boot-time sysctl.conf directives
.Sh SYNOPSIS
.Nm sysconf
.Cm sysctl
.Op Fl AcDeFinNqvVx
.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 sysconf
.Cm sysctl
.Op Fl ADeFnNqvV
.Op Fl f Ar file | Fl k Ar module
.Fl a
.Nm sysconf
.Cm sysctl
.Op Fl AaDinNq
.Fl d
.Op Ar name ...
.Nm sysconf
.Cm sysctl
.Op Fl E
.Op Fl k Ar module
.Fl l | L
.Sh DESCRIPTION
The
.Cm sysctl
target of
.Xr sysconf 8
reads and modifies
.Xr sysctl.conf 5
configuration.
Shared assignment syntax and options are documented in
.Xr sysconf 8 ;
multi-file write policy in
.Xr sysconf-targets 8 .
.Pp
The stock file list is
.Pa /etc/sysctl.conf ,
.Pa /etc/sysctl.conf.local ,
then
.Pa /etc/sysctl.kld.d/ Ns Ar module Ns Pa .conf
when
.Fl k Ar module
is given
.Pq applied by Xr rc.subr 8 when the module loads .
.Pp
Values are quoted on write only when the value requires it
.Pq embedded whitespace .
.Pp
When writing to the running system,
each
.Ar name Ns = Ns Ar value
is validated against the kernel's sysctl tree first.
An OID that cannot be set at run time
.Pq non-leaf, read-only, or loader-only tunable
is reported and skipped,
while the remaining assignments are still applied.
In particular,
a loader-only tunable is reported as requiring the
.Cm loader
target instead
.Pq see Xr sysconf-loader 8 .
Numeric values are also checked against the OID's
.Dv CTLTYPE
.Po
.Dv CTLTYPE_INT ,
.Dv CTLTYPE_U64 ,
and the other integer widths
.Pc
so an overflowing or non-numeric assignment cannot land in
.Xr sysctl.conf 5
and fail
.Pq or apply truncated
when
.Xr sysctl 8
loads the file at boot.
.Pp
An unknown OID
.Pq often from a module that is not yet loaded
is reported with a warning but still written,
matching how
.Xr init 8
treats such lines in
.Xr sysctl.conf 5 ;
.Fl i
and
.Fl q
suppress that warning.
Validation is skipped entirely under
.Fl R ,
where the target system's kernel is not the running kernel.
.Pp
.Fl d
descriptions for this target come from the running kernel
.Pq as with Xr sysctl 8 Fl d ,
not from a defaults file.
.Sh FILES
.Bl -tag -width "/etc/sysctl.conf.local" -compact
.It Pa /etc/sysctl.conf
Write target for new directives.
.It Pa /etc/sysctl.conf.local
Site-local overrides.
.It Pa /etc/sysctl.kld.d/
Module-specific drop-ins
.Pq see Fl k .
.El
.Sh EXAMPLES
.Dl sysconf sysctl kern.maxfiles=65536
.Dl sysconf sysctl -F vfs.usermount
.Dl sysconf sysctl -d kern.maxfiles
.Dl sysconf sysctl -L
.Dl sysconf sysctl -LE
.Dl sysconf sysctl -k ipfw net.inet.ip.fw.verbose=1
.Dl sysconf -x sysctl vfs.usermount
.Dl sysconf -j www sysctl kern.ipc.somaxconn=1024
.Pp
An overflowing assignment is refused before the file is touched
.Pq here an integer OID given a value beyond INT_MAX :
.Pp
.Dl sysconf sysctl security.bsd.see_other_uids=99999999999
.Sh SEE ALSO
.Xr sysctl.conf 5 ,
.Xr sysconf 8 ,
.Xr sysconf-loader 8 ,
.Xr sysconf-targets 8 ,
.Xr sysctl 8
.Sh HISTORY
The
.Nm sysconf
utility first appeared in
.Fx 16.0 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+174
View File
@@ -0,0 +1,174 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF-TARGETS 8
.Os
.Sh NAME
.Nm sysconf-targets
.Nd sysconf multi-file sourcing and write policy
.Sh SYNOPSIS
.Nm sysconf
.Ar target
.Op options
.Op Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value ...
.Sh DESCRIPTION
This page documents rules shared by every
.Xr sysconf 8
target that is backed by more than one configuration file.
Per-target file lists and syntax quirks are in
.Xr sysconf-loader 8 ,
.Xr sysconf-sysctl 8 ,
.Xr sysconf-make 8 ,
.Xr sysconf-src 8 ,
.Xr sysconf-rc 8 ,
and
.Xr sysconf-generic 8 .
Shared options remain in
.Xr sysconf 8 .
.Ss Authoritative file
Most targets consult several files in the deterministic order the system
sources them at boot.
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.
.Pp
Reads report that 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
.Pq Fl x
are applied to every file listing the directive,
lest deleting the authoritative definition merely unmask an earlier one.
.Ss Defaults files
For a target with a defaults file
.Po
presently
.Ql loader ;
see
.Xr sysconf-loader 8
.Pc ,
the boot-time consumer sources the defaults before everything else,
and so does
.Nm sysconf :
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 sysconf may modify .
.Pp
Those targets additionally support querying defaults:
.Fl d
reports directive descriptions,
.Fl D
consults the defaults alone,
and
.Fl A
widens dumps to include directives still at their system default
.Po
a named
.Ar name
read always reflects the defaults already;
.Fl A
changes only
.Fl a
scope,
or dumps everything when given with no
.Ar name
arguments
.Pc .
Attempting any of these against a target with no defaults
.Po
.Cm make
and
.Cm src
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
.Pq see Xr sysconf-sysctl 8 .
.Ss Explicit file selection
An arbitrary configuration file may 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.
.Pp
How each built-in format discovers or lists its backing files is
implemented by
.Xr bsdconf_format 3 .
.Sh LIMITATIONS
.Nm sysconf
does not evaluate
.Xr make 1
conditionals
.Pq Ql .if , .elif , .else , .endif
or other makefile control flow that may appear in
.Pa make.conf ,
.Pa src.conf ,
or
.Pa src-env.conf .
.Pp
It also does not consult
.Pa sys.mk ,
.Pa local.sys.mk ,
or other
.Xr make 1
defaults under
.Pa /usr/share/mk .
.Pp
Effective values from
.Nm sysconf
therefore match
.Nm make Fl V
for straight-line assignment and
.Ql +=
chains in those conf files,
but may differ when the value depends on conditionals or mk defaults.
.Sh SEE ALSO
.Xr bsdconf_format 3 ,
.Xr sysconf 8 ,
.Xr sysconf-generic 8 ,
.Xr sysconf-loader 8 ,
.Xr sysconf-make 8 ,
.Xr sysconf-rc 8 ,
.Xr sysconf-src 8 ,
.Xr sysconf-sysctl 8
.Sh HISTORY
The
.Nm sysconf
utility first appeared in
.Fx 16.0 .
.Sh AUTHORS
.An Devin Teske Aq Mt [email protected]
.An Faraz Vahedi Aq Mt [email protected]
+704
View File
@@ -0,0 +1,704 @@
.\" Copyright (c) 2013-2026 Devin Teske <[email protected]>
.\" Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd August 2, 2026
\
.Dt SYSCONF 8
.Os
.Sh NAME
.Nm sysconf
.Nd read and modify system configuration files
.Sh SYNOPSIS
.Nm
.Ar target
.Op Fl AcDeFinNqsvVx
.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 ADeFnNqvV
.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 .
The first positional argument selects a
.Ar target
keyword;
options are equally accepted before it.
Each target is documented on its own page
.Pq see Sx TARGETS ;
shared options and assignment syntax are documented here.
.Ss Assignment syntax
A
.Ar name
alone reads the current value of the directive,
while
.Ar name Ns = Ns Ar value
atomically writes it.
Writes echo
.Ql name: old -> new
when the stored value changes,
or
.Ql name: value (unchanged)
when it is already set as requested
.Pq the file is left untouched in that case .
With
.Fl e
those echoes use
.Ql name=...
instead of
.Ql name: ...
.Pq matching read output .
The echo is suppressed by
.Fl q ;
.Fl c
writes nothing to stdout.
A directive not previously set anywhere shows an empty old value.
.Pp
On the
.Cm make
and
.Cm src
targets, a
.Ql WITH_*
or
.Ql WITHOUT_*
build knob
.Pq presence matters; Xr src.conf 5 ignores the value
is shown as
.Ql name (present)
when defined
.Po
any value, including
.Ql =
or
.Ql =t
.Pc ,
rather than printing the value after the colon;
.Fl e
prints the real assignment
.Po
e.g.
.Ql name=
or
.Ql name=t
.Pc ,
and
.Fl n
prints that same value alone
.Pq empty when the assignment is Ql name= .
Setting such a knob with
.Fl s
echoes
.Ql name: (not present) -> (present)
.Po
or
.Ql (present) (unchanged)
when it is already defined
.Pc .
An explicit
.Ar name Ns =
.Pq including empty
always writes the requested value;
a change of placeholder while the knob stays defined echoes
.Ql name: old -> new
like other assignments.
A write of
.Ql WITH_ Ns Ar foo Ns =no
.Pq exact lowercase Ql no
is still performed but warns to use
.Ql WITHOUT_ Ns Ar foo Ns =1
instead.
That message matches
.Pa share/mk/bsd.mkopt.mk
and is emitted here as a precursor so a bad
.Xr make.conf 5
or
.Xr src.conf 5
assignment is caught at write time.
The same advisory is printed on reads
.Po
named queries and
.Fl a
dump
.Pc ,
with
.Ql file:line:
when the authoritative statement is known, so an existing
.Ql WITH_* Ns =no
is not overlooked when presence display shows only
.Ql (present) .
.Fl q
suppresses the read-time form.
.Xr make 1
itself typically warns later during a
.Pa /usr/src
build, when
.Pa bsd.opts.mk
.Pq or Pa src.opts.mk
reloads
.Pa bsd.mkopt.mk
for recognized options such as
.Va MANCOMPRESS .
The same
.Xr make.conf 5
line stays silent for a bare
.Xr make 1
.Pq e.g. Fl C Pa /tmp
or for ports, which do not run that option pass.
.Ql WITHOUT_* Ns =no ,
.Ql WITH_* Ns =NO ,
and other values do not warn here or there.
.Va WITHOUT_MODULES
is not a presence knob
.Pq its value is a module list
and is excluded from
.Fl s
and presence display.
.Pp
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.
.Pp
For
.Xr make 1 Ns -syntax
targets
.Po
.Cm make
and
.Cm src ;
see
.Xr sysconf-make 8
and
.Xr sysconf-src 8
.Pc ,
.Ql +=
is instead the
.Xr make 1
append operator and
.Ql -=
strikes words from the last matching assignment fragment.
.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
.Pq see Xr bsdconf_put 3 .
How multi-file targets choose the file to read or rewrite is documented in
.Xr sysconf-targets 8 .
.Sh TARGETS
The following target keywords are supported:
.Bl -tag -width indent
.It Cm loader
Boot loader configuration
.Pq Xr loader.conf 5 .
See
.Xr sysconf-loader 8 .
.It Cm sysctl
Kernel state set at boot
.Pq Xr sysctl.conf 5 .
See
.Xr sysconf-sysctl 8 .
.It Cm make
System-wide
.Xr make 1
settings
.Pq Pa /etc/make.conf .
See
.Xr sysconf-make 8 .
.It Cm src
The
.Pa /usr/src
build triad
.Pq Pa src-env.conf , make.conf , src.conf .
See
.Xr sysconf-src 8 .
.It Cm rc
Pass-through to
.Xr sysrc 8 .
See
.Xr sysconf-rc 8 .
.It Cm generic
Foreign or unknown format;
requires
.Fl f .
See
.Xr sysconf-generic 8 .
.El
.Pp
Cross-cutting rules for multi-file sourcing,
defaults,
and where writes versus removals land are in
.Xr sysconf-targets 8 .
Built-in format descriptors and file-list discovery are in
.Xr bsdconf_format 3 .
.Sh OPTIONS
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
.Xr sysconf-targets 8
.Pc .
With no
.Ar name
arguments,
dumps everything
.Po
as
.Fl a ,
defaults included
.Pc .
.Pp
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
.Pq see Xr sysconf-loader 8 .
.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
.Po
with
.Fl x ,
check that
.Ar name
is absent
.Pc .
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
.Po
or, with
.Fl a ,
dumping every default
.Pc .
Defaults are read-only;
combining
.Fl D
with an assignment is an error.
Only valid for targets with a defaults file
.Pq see Xr sysconf-loader 8 .
.It Fl d
Show the description of each
.Ar name
instead of its value.
.Pp
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
.Po
as with
.Xr sysctl 8
.Fl d ;
see
.Xr sysconf-sysctl 8
.Pc .
.Pp
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.
Applies to both reads and the informational echo from assignments
.Po
replacing the default
.Ql name: value
form;
changed writes use
.Ql name=old # -> new ,
matching
.Xr sysrc 8
.Pc .
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 .
See
.Xr sysconf-targets 8
and
.Xr sysconf-generic 8 .
.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 suppress the warning when writing an unknown sysctl OID .
.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
.Pq see Xr sysconf-sysctl 8 .
.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
.Po
.Fl l
enumerates only files
.Nm sysconf
may modify;
see
.Xr sysconf-targets 8
.Pc .
.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 s
Set empty
.Ql WITH_*
and
.Ql WITHOUT_*
build knobs on the
.Cm make
and
.Cm src
targets
.Po
equivalent to
.Ar name Ns =
with no value;
see
.Xr sysconf-make 8
and
.Xr sysconf-src 8
.Pc .
Each bare
.Ar name
must begin with
.Ql WITH_
or
.Ql WITHOUT_ .
An already-present knob
.Pq any value
is left unchanged by
.Fl s .
An explicit
.Ar name Ns =
including empty always writes.
Mutually exclusive with
.Fl x .
.It Fl v
Verbose.
Print the pathname of the configuration file holding the final effective
value
.Pq the last file in sourcing order that assigned the directive .
.It Fl V
Trail.
For each named read
.Pq and for Fl a ,
print every assignment step that contributed to the effective value:
.Ql file:line: nameopfragment ,
and when the running effective value differs from the fragment,
.Ql -> effective .
.Pp
For writes, print the same
.Ql file:line:
shape for the change:
.Ql file:line: name=old -> name=new ,
.Ql name=value (unchanged) ,
.Ql name=value (added)
when a new statement is created,
or
.Ql nameopfragment (removed)
when a statement is deleted,
using the statement operator
.Ql = / += / ...
as appropriate .
.Pp
Uses the same accumulation model as ordinary reads
.Pq make(1) `+=' et al. ,
so the last effective value matches
.Fl v
and,
for linear conf files without
.Ql .if ,
.Nm make Fl V
.Pq see Xr sysconf-make 8 and Xr sysconf-src 8 .
.It Fl x
Remove the named directive(s) from every file of the target listing them
.Pq see Xr sysconf-targets 8 .
With
.Fl v
or
.Fl V ,
a successful removal echoes
.Ql name (removed)
.Pq or a trail form with Fl V .
.El
.Sh ENVIRONMENT
.Bl -tag -width "LOADER_DEFAULTS"
.It Ev LOADER_DEFAULTS
Defaults file for the
.Ql loader
target;
see
.Xr sysconf-loader 8 .
.It Ev SRC_ENV_CONF , Ev __MAKE_CONF , Ev SRCCONF
Paths for the
.Cm src
triad;
see
.Xr sysconf-src 8 .
.El
.Sh FILES
See the per-target manuals
.Pq Xr sysconf-loader 8 , Xr sysconf-sysctl 8 , Xr sysconf-make 8 , Xr sysconf-src 8
for the files each keyword consults.
Common paths include
.Pa /boot/loader.conf ,
.Pa /etc/sysctl.conf ,
.Pa /etc/make.conf ,
and
.Pa /etc/src.conf .
.Sh EXIT STATUS
.Ex -std
In check mode
.Pq Fl c ,
an exit status of zero means no changes are required.
.Sh EXAMPLES
Read and set a boot loader directive:
.Pp
.Dl sysconf loader zfs_load
.Dl sysconf loader zfs_load=\(dqYES\(dq
.Pp
Raise a sysctl applied at boot:
.Pp
.Dl sysconf sysctl kern.maxfiles=65536
.Pp
List files backing the loader target:
.Pp
.Dl sysconf loader -l
.Pp
Append to a space-separated list:
.Pp
.Dl sysconf loader kernels+=kernel_test
.Pp
Append a
.Xr make 1
flag and set a
.Pa src.conf
knob:
.Pp
.Dl sysconf make CFLAGS+=-DDEBUG
.Dl sysconf src -s WITHOUT_LLDB
.Dl sysconf src WITHOUT_LLDB=
.Pp
Further examples for each target appear in
.Xr sysconf-loader 8 ,
.Xr sysconf-sysctl 8 ,
.Xr sysconf-make 8 ,
.Xr sysconf-src 8 ,
.Xr sysconf-rc 8 ,
and
.Xr sysconf-generic 8 .
.Sh SEE ALSO
.Xr bsdconf 3 ,
.Xr bsdconf_format 3 ,
.Xr bsdconf_put 3 ,
.Xr loader.conf 5 ,
.Xr src.conf 5 ,
.Xr sysctl.conf 5 ,
.Xr jail 8 ,
.Xr sysconf-generic 8 ,
.Xr sysconf-loader 8 ,
.Xr sysconf-make 8 ,
.Xr sysconf-rc 8 ,
.Xr sysconf-src 8 ,
.Xr sysconf-sysctl 8 ,
.Xr sysconf-targets 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 [email protected]
.An Faraz Vahedi Aq Mt [email protected]
.Sh SECURITY CONSIDERATIONS
Read-only invocations
.Po
including
.Fl c
checks, which never modify files
.Pc
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.
.Pp
Write operations replace files atomically and never in place;
see the
.Sx SECURITY CONSIDERATIONS
section of
.Xr bsdconf_put 3
for the properties of the write transaction.
+761
View File
@@ -0,0 +1,761 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* sysconf(8): CLI entry, option parsing, and program state.
*/
#include "sysconf_priv.h"
/* Requests to process (one per name argument) */
struct request *reqs; /* set by main() */
unsigned int nreqs; /* set by main() */
/* Union of all directives, for `-a' against multi-file targets */
struct dumpent *dumps; /* set by scan_cb() */
size_t ndumps; /* set by scan_cb() */
size_t dumpsize; /* set by scan_cb() */
/* Resolved target state */
char **conf_files; /* ordered backing files */
size_t nconf_files;
size_t conf_scanidx; /* file being parsed (scan_pass) */
size_t write_idx; /* default write target index */
int defaults_idx = -1; /* defaults file index in conf_files
* (read-only, never listed); -1 if none */
enum bsdconf_format format = BSDCONF_FORMAT_GENERIC;
/* Extra display information */
const char *pgm; /* set to getprogname() by main() */
bool check = false; /* `-c' */
bool defaults_only = false; /* `-D' */
bool dump_all = false; /* `-a' */
bool existing_only = false; /* `-E' */
bool ignore_unknown = false; /* `-i' */
bool list_all = false; /* `-L' */
bool list_files = false; /* `-l' */
bool name_only = false; /* `-N' */
bool quiet = false; /* `-q' */
bool remove_mode = false; /* `-x' */
bool set_knobs = false; /* `-s' WITH_/WITHOUT_ presence */
bool show_desc = false; /* `-d' */
bool show_equals = false; /* `-e' */
bool show_file = false; /* `-F' */
bool value_only = false; /* `-n' */
bool verbose = false; /* `-v' */
bool verbose_trail = false; /* `-V' */
bool with_defaults = false; /* `-A' */
/* Option arguments */
const char *file = NULL; /* `-f file' */
bool file_stdin = false; /* `-f -' given */
const char *jailname = NULL; /* `-j jail' */
const char *module = NULL; /* `-k module' */
const char *rootdir = ""; /* `-R dir' */
/*
* 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.
*
* Attached arguments (`-fPATH', `-R/altroot') are self-contained: the rest
* of the cluster is the option's argument, so the following argv element is
* not consumed. Only a trailing option letter that takes an argument and
* has nothing after it in the cluster (`-f PATH') skips the next element.
*/
static int
find_target(int argc, char *argv[])
{
int n;
const char *o;
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') {
for (p = argv[n] + 1; *p != '\0'; p++) {
o = strchr(OPTSTRING, *p);
if (o != NULL && o[1] == ':') {
/* Takes an argument */
if (p[1] != '\0') {
;
/* attached: rest of argv[n] */
} else if (n + 1 < argc) {
n++;
/* separate argv */
}
break;
}
}
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 's': /* set empty WITH_/WITHOUT_ knobs (make/src) */
set_knobs = 1;
break;
case 'v': /* verbose (last file + final value) */
verbose = 1;
break;
case 'V': /* verbose trail (every assignment step) */
verbose_trail = 1;
break;
case 'x': /* remove */
remove_mode = 1;
break;
case '?': /* unknown argument (based on optstring) */
default: /* unhandled argument (based on switch) */
usage();
}
}
return (optind);
}
/*
* 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 = getprogname();
/*
* 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();
}
if (set_knobs && remove_mode) {
warnx("-s and -x are mutually exclusive");
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 ||
set_knobs) {
warnx("-d cannot be combined with "
"-c/-l/-L/-s/-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);
if (set_knobs) {
if (format != BSDCONF_FORMAT_MAKE &&
format != BSDCONF_FORMAT_SRC) {
warnx("-s is only valid for make and src");
usage();
}
if (argc == 0) {
warnx("-s requires at least one WITH_/WITHOUT_ name");
usage();
}
}
for (n = 0; n < argc; n++) {
split_request(argv[n], &reqs[n], remove_mode);
if (set_knobs) {
if (reqs[n].newvalue != NULL) {
warnx("-s does not take a value "
"('%s')", argv[n]);
usage();
}
if (!make_knob_name_p(reqs[n].name)) {
if (strcmp(reqs[n].name,
"WITHOUT_MODULES") == 0)
warnx("WITHOUT_MODULES takes a "
"module list; use "
"WITHOUT_MODULES=...");
else
warnx("-s requires a WITH_ or "
"WITHOUT_ name ('%s')",
reqs[n].name);
usage();
}
reqs[n].newvalue = "";
}
if (reqs[n].newvalue != NULL && !reqs[n].remove)
warn_with_equals_no(reqs[n].name,
reqs[n].newvalue, NULL, 0);
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 or -s name");
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) (non-leaf, read-only, loader-only tunables)
* or whose values cannot fit the OID's CTLTYPE (so an overflowing
* assignment cannot surprise sysctl(8) at boot). Unknown OIDs are
* warned about but still written -- the module may not be loaded
* yet, matching init(8)'s handling of sysctl.conf(5). Only
* meaningful against the running system's sysctl tree, so skipped
* under -R. -i/-q quiet the unknown-OID warning; -k remains the
* module drop-in selector.
*/
if (format == BSDCONF_FORMAT_SYSCTL && have_writes && !check &&
*rootdir == '\0') {
int quiet_unknown = ignore_unknown || quiet;
unsigned int u;
for (u = 0; u < nreqs; u++) {
if (reqs[u].newvalue == NULL)
continue;
if (sysctl_writable(reqs[u].name, reqs[u].newvalue,
quiet_unknown) != 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 reads and -c checks (do_checks() never calls
* put) run the scan inside a Capsicum sandbox -- except a sysctl
* description dump, whose kernel queries the sandbox would deny.
* Classify by whether we will actually write, not by whether the
* argv looked like name=value (`-c' sets have_writes for checks).
*/
scan_pass((check || !have_writes) &&
!(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_make_strikes(1) != EXIT_SUCCESS)
rv = EXIT_FAILURE;
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_make_strikes(0)) != EXIT_SUCCESS)
rv = 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++) {
size_t ai;
trail_free(reqs[n].trail, reqs[n].ntrail);
reqs[n].trail = NULL;
reqs[n].ntrail = 0;
for (ai = 0; ai < reqs[n].nassigns; ai++)
free(reqs[n].assigns[ai].piece);
free(reqs[n].assigns);
reqs[n].assigns = NULL;
reqs[n].nassigns = 0;
reqs[n].found = 0;
reqs[n].append_present = 0;
reqs[n].srcidx = -1;
reqs[n].line = 0;
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.
*/
void
usage(void)
{
fprintf(stderr,
"usage: %s target [-AcDeFinNqsvVx] [-j jail | -R dir]"
" [-f file | -k module]\n"
" name[[+|-]=value] ...\n", pgm);
fprintf(stderr,
" %s target [-ADeFnNqvV] [-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.
*/
void
help(void)
{
fprintf(stderr,
"usage: %s target [-AcDeFinNqsvVx] [-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 (or -f for any make-syntax file)");
fprintf(stderr, TGTFMT, "src",
"/usr/src build triad: src-env.conf, make.conf, src.conf");
fprintf(stderr, TGTFMT, "",
"(new writes prefer src.conf; env: SRC_ENV_CONF,");
fprintf(stderr, TGTFMT, "",
"__MAKE_CONF, SRCCONF)");
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 name and value with `=' (reads and write echoes).");
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 quiet unknown sysctl OID warnings).");
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, "-s",
"Set empty WITH_/WITHOUT_ knobs (make and src only).");
fprintf(stderr, OPTFMT, "-v",
"Verbose. Print the pathname of the configuration file");
fprintf(stderr, OPTFMT, "",
"holding the final effective value.");
fprintf(stderr, OPTFMT, "-V",
"Trail. Reads: each assignment step (file:line, operator,");
fprintf(stderr, OPTFMT, "",
"fragment, running effective). Writes: file:line:");
fprintf(stderr, OPTFMT, "",
"name=old -> name=new (or name=value (unchanged)).");
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).");
fprintf(stderr, OPTFMT, "SRC_ENV_CONF",
"src-env.conf for the src target (default /etc/src-env.conf).");
fprintf(stderr, OPTFMT, "__MAKE_CONF",
"make.conf for the src target (default /etc/make.conf).");
fprintf(stderr, OPTFMT, "SRCCONF",
"src.conf for the src target (default /etc/src.conf).");
exit(EXIT_FAILURE);
}
+221
View File
@@ -0,0 +1,221 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* CLI assignment parsing and sysrc(8)-style list word edits.
*/
#include <ctype.h>
#include "sysconf_priv.h"
/*
* 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: `+=' appends a new `name+=value' line
* (make accumulates; we never rewrite a prior assignment), while `=', `?=',
* `:=', and `!=' rewrite or add a single assignment as today. `name-=word'
* is a list strike: the last assignment whose value contains each word is
* rewritten (or removed if emptied); see do_make_strikes(). For every other
* target, `name+=word' and `name-=word' are sysrc(8)-style list edits
* against the effective value (see merge_list_requests() below).
*/
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->append_present = 0;
req->remove = remove;
req->srcidx = -1;
req->line = 0;
req->trail = NULL;
req->ntrail = 0;
req->assigns = NULL;
req->nassigns = 0;
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 '-':
/* make(1) has no `-='; treat as a list strike */
req->listop = '-';
arg[n - 1] = '\0';
break;
}
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");
}
/* True when `req' is a make/src `name-=word' strike (not a sysrc list edit). */
/*
* Test whether the whitespace-separated `list' contains `word' (of length
* `wlen') as a whole word.
*/
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.
*/
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 sysrc(8)-style 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. Make/src `name-=word'
* strikes are handled separately by do_make_strikes(). 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.
*/
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;
/* make/src `-=` is a per-statement strike, not a list merge */
if (make_strike_p(&reqs[n]))
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);
}
+274
View File
@@ -0,0 +1,274 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* make(1)/src triad word-removal across += assignment fragments.
*/
#include <ctype.h>
#include "sysconf_priv.h"
int
make_strike_p(const struct request *req)
{
return ((bsdconf_format_processing(format) &
BSDCONF_OPERATOR_EQUALS) != 0 &&
req->listop == '-' && req->newvalue != NULL);
}
int
make_assign_push(struct request *req, enum bsdconf_op op,
const char *piece, uint32_t line)
{
struct make_assign *na;
char *pcopy;
if (piece == NULL)
piece = "";
if ((pcopy = strdup(piece)) == NULL)
return (-1);
na = realloc(req->assigns, (req->nassigns + 1) * sizeof(*na));
if (na == NULL) {
free(pcopy);
return (-1);
}
req->assigns = na;
na[req->nassigns].fileidx = conf_scanidx;
na[req->nassigns].line = line;
na[req->nassigns].op = op;
na[req->nassigns].piece = pcopy;
req->nassigns++;
return (0);
}
/*
* Apply make/src `name-=word' strikes: for each whitespace-separated word,
* find the last assignment whose piece contains it and remove the word from
* that piece (delete the statement if the piece becomes empty). With
* `check_only', report whether any change would be required without writing.
* Echoes effective old/new (or `(unchanged)') unless -q. Neutralizes each
* handled request so do_writes() skips it. Returns EXIT_SUCCESS or
* EXIT_FAILURE.
*/
int
do_make_strikes(int check_only)
{
char **work;
char *new_eff;
char *token;
char *wordscopy;
char *wp;
const char *old_eff;
int differs;
int rv = EXIT_SUCCESS;
size_t i;
size_t j;
size_t wlen;
unsigned int n;
for (n = 0; n < nreqs; n++) {
if (!make_strike_p(&reqs[n]))
continue;
if (!reqs[n].found || reqs[n].nassigns == 0) {
if (!ignore_unknown) {
if (!quiet)
warnx("unknown directive '%s'",
reqs[n].name);
rv = EXIT_FAILURE;
}
reqs[n].newvalue = NULL;
reqs[n].name = "";
continue;
}
if ((work = calloc(reqs[n].nassigns, sizeof(*work))) == NULL)
err(EXIT_FAILURE, NULL);
for (i = 0; i < reqs[n].nassigns; i++) {
work[i] = strdup(reqs[n].assigns[i].piece);
if (work[i] == NULL)
err(EXIT_FAILURE, NULL);
}
differs = 0;
if ((wordscopy = strdup(reqs[n].newvalue)) == NULL)
err(EXIT_FAILURE, NULL);
wp = wordscopy;
while ((token = strsep(&wp, " \t")) != NULL) {
if (*token == '\0')
continue;
wlen = strlen(token);
for (i = reqs[n].nassigns; i > 0; i--) {
j = i - 1;
if (!list_has(work[j], token, wlen))
continue;
{
char *merged;
merged = list_merge(work[j], token,
'-');
free(work[j]);
work[j] = merged;
differs = 1;
}
break;
}
}
free(wordscopy);
if (check_only) {
if (differs) {
if (!quiet)
warnx("%s: value differs",
reqs[n].name);
rv = EXIT_FAILURE;
}
goto neutralize;
}
old_eff = reqs[n].value != NULL ? reqs[n].value : "";
if (!differs) {
if (!quiet)
print_write_echo(
reqs[n].srcidx >= 0 ?
conf_files[reqs[n].srcidx] : NULL,
0, reqs[n].name, BSDCONF_OP_ASSIGN,
old_eff, old_eff, 0, 0, 0);
goto neutralize;
}
/* Effective value after applying the struck pieces */
new_eff = NULL;
for (i = 0; i < reqs[n].nassigns; i++) {
char *merged;
if (work[i][0] == '\0')
continue;
merged = make_apply(reqs[n].assigns[i].op, new_eff,
work[i]);
if (merged == NULL)
err(EXIT_FAILURE, NULL);
free(new_eff);
new_eff = merged;
}
if (new_eff == NULL) {
if ((new_eff = strdup("")) == NULL)
err(EXIT_FAILURE, NULL);
}
/* Put each changed statement (line-targeted) */
for (i = 0; i < nconf_files; i++) {
struct bsdconf_option *options;
unsigned int nopts = 0;
size_t k;
for (k = 0; k < reqs[n].nassigns; k++)
if (reqs[n].assigns[k].fileidx == i &&
strcmp(work[k],
reqs[n].assigns[k].piece) != 0)
nopts++;
if (nopts == 0)
continue;
options = calloc(nopts + 1, sizeof(*options));
if (options == NULL)
err(EXIT_FAILURE, NULL);
nopts = 0;
for (k = 0; k < reqs[n].nassigns; k++) {
struct bsdconf_option *opt;
if (reqs[n].assigns[k].fileidx != i ||
strcmp(work[k],
reqs[n].assigns[k].piece) == 0)
continue;
opt = &options[nopts++];
memset(opt, 0, sizeof(*opt));
opt->type = BSDCONF_TYPE_STR;
opt->directive = reqs[n].name;
opt->match_line = reqs[n].assigns[k].line;
opt->op = reqs[n].assigns[k].op;
if (work[k][0] == '\0') {
opt->action = BSDCONF_ACTION_REMOVE;
opt->value.str = NULL;
} else {
opt->action = BSDCONF_ACTION_SET_VALUE;
opt->value.str = work[k];
}
}
{
int fd;
if ((fd = open(conf_files[i],
O_WRONLY | O_CREAT | O_EXCL, 0666)) >= 0)
close(fd);
else if (errno != EEXIST)
err(EXIT_FAILURE, "%s", conf_files[i]);
}
if (bsdconf_put(options, conf_files[i],
bsdconf_format_processing(format),
bsdconf_format_put(format)) != 0)
err(EXIT_FAILURE, "%s", conf_files[i]);
if (!quiet && verbose_trail) {
for (k = 0; k < nopts; k++) {
struct bsdconf_option *opt;
const char *oval;
const char *nval;
opt = &options[k];
/* Recover old piece from assigns */
oval = "";
nval = opt->value.str != NULL ?
opt->value.str : "";
for (j = 0; j < reqs[n].nassigns;
j++) {
if (reqs[n].assigns[j].fileidx
!= i ||
reqs[n].assigns[j].line !=
opt->match_line)
continue;
oval =
reqs[n].assigns[j].piece;
break;
}
if (opt->action ==
BSDCONF_ACTION_REMOVE)
printf("%s:%u: %s%s%s "
"(removed)\n",
conf_files[i],
opt->match_line,
reqs[n].name,
op_str(opt->op), oval);
else
print_write_echo(conf_files[i],
opt->match_line,
reqs[n].name, opt->op,
oval, nval, 1, 0, 0);
}
}
free(options);
}
if (!quiet && !verbose_trail)
print_write_echo(
reqs[n].srcidx >= 0 ?
conf_files[reqs[n].srcidx] : NULL,
0, reqs[n].name, BSDCONF_OP_ASSIGN, old_eff,
new_eff, 1, 0, 0);
free(reqs[n].value);
reqs[n].value = new_eff;
neutralize:
for (i = 0; i < reqs[n].nassigns; i++)
free(work[i]);
free(work);
reqs[n].newvalue = NULL;
reqs[n].listop = '\0';
/* Keep name for any trailing read of the same argv slot */
}
return (rv);
}
+264
View File
@@ -0,0 +1,264 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Apply or check assignments against conf files (bsdconf_put path).
*/
#include <ctype.h>
#include "sysconf_priv.h"
/*
* 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, a make(1) `+=value' request differs when no identical `+=value'
* line is already present, and a removal request differs when the
* directive is still present. Returns EXIT_SUCCESS when no changes would
* be required.
*/
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 if (reqs[n].op == BSDCONF_OP_APPEND)
differs = !reqs[n].append_present;
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);
}
/*
* 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.
*/
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;
/*
* An identical make(1) `+=value' line is
* already present: nothing to write, but
* still report the effective value unless
* -q (see report loop for the SET case).
*/
if (reqs[n].op == BSDCONF_OP_APPEND &&
reqs[n].append_present) {
if (!quiet)
print_write_echo(conf_files[f],
0, reqs[n].name,
BSDCONF_OP_APPEND,
reqs[n].value,
reqs[n].value, 0, 0, 0);
continue;
}
/*
* -s only ensures presence: if the
* WITH_/WITHOUT_ knob is already defined
* (any value), leave the file alone.
* An explicit name= (including empty)
* always writes the requested value.
*/
if (set_knobs && make_knob_p(reqs[n].name) &&
reqs[n].found) {
if (!quiet)
print_write_echo(conf_files[f],
0, reqs[n].name,
reqs[n].op,
reqs[n].value,
reqs[n].value, 0, 0,
0);
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,
0666)) >= 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;
/*
* Removals are quiet unless -v/-V
* (or -q which is already silent).
*/
if (!quiet &&
(verbose || verbose_trail))
print_write_echo(
conf_files[f],
opt->line,
opt->directive,
opt->op, "", "", 0, 0,
1);
}
continue;
}
if (!quiet) {
const char *old = owner[n]->value;
char *neweff = NULL;
const char *shown;
/*
* Gate the echo on whether bsdconf_put()
* actually edited the file
* (BSDCONF_VALUE_CHANGED). With -V, shape the
* echo like a read trail
* (`file:line: name=old -> name=new'). For
* make(1) `+=', show the effective value
* before and after the append rather than
* the fragment alone.
*/
if (opt->op == BSDCONF_OP_APPEND) {
neweff = make_apply(BSDCONF_OP_APPEND,
old, opt->value.str);
if (neweff == NULL)
err(EXIT_FAILURE, NULL);
shown = neweff;
} else
shown = opt->value.str;
/*
* -V shows statement shape at file:line; a
* newly added statement is `(added)' rather
* than `op -> op+value'. -v keeps effective
* old -> new for `+='.
*/
if (verbose_trail &&
(opt->result &
BSDCONF_DIRECTIVE_ADDED) != 0)
print_write_echo(conf_files[f],
opt->line, opt->directive, opt->op,
"", opt->value.str,
(opt->result &
BSDCONF_VALUE_CHANGED) != 0, 1, 0);
else if (verbose_trail)
print_write_echo(conf_files[f],
opt->line, opt->directive, opt->op,
old == NULL ? "" : old, shown,
(opt->result &
BSDCONF_VALUE_CHANGED) != 0, 0, 0);
else
print_write_echo(conf_files[f],
opt->line, opt->directive, opt->op,
old, shown,
(opt->result &
BSDCONF_VALUE_CHANGED) != 0, 0, 0);
if (neweff != NULL) {
free(owner[n]->value);
owner[n]->value = neweff;
}
}
}
}
/* 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);
}
+320
View File
@@ -0,0 +1,320 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Display helpers: name/value pairs, `-V' trails, write echoes.
*/
#include <ctype.h>
#include "sysconf_priv.h"
/*
* True when `name' is a presence-only WITH_/WITHOUT_ build knob
* (share/mk/bsd.mkopt.mk tests defined(), and src.conf(5) ignores the
* value). WITHOUT_MODULES is excluded: its value is a module reject list.
*/
int
make_knob_name_p(const char *name)
{
if (strcmp(name, "WITHOUT_MODULES") == 0)
return (0);
return (strncmp(name, "WITH_", 5) == 0 ||
strncmp(name, "WITHOUT_", 8) == 0);
}
/*
* Precursor to share/mk/bsd.mkopt.mk (seen under /usr/src after
* bsd.opts.mk / src.opts.mk, not bare make or ports): WITH_FOO=no
* does not disable FOO (presence still counts). Warn only for WITH_*
* with the exact value "no" (not WITHOUT_*, not "NO"). On reads,
* `path'/`line' locate the authoritative assignment when known.
*/
void
warn_with_equals_no(const char *name, const char *value, const char *path,
uint32_t line)
{
const char *opt;
if (format != BSDCONF_FORMAT_MAKE && format != BSDCONF_FORMAT_SRC)
return;
if (name == NULL || value == NULL || strcmp(value, "no") != 0)
return;
/* WITH_ but not WITHOUT_ */
if (strncmp(name, "WITH_", 5) != 0 ||
strncmp(name, "WITHOUT_", 8) == 0)
return;
opt = name + 5;
if (path != NULL && line != 0)
warnx("%s:%u: Use WITHOUT_%s=1 instead of WITH_%s=no",
path, line, opt, opt);
else
warnx("Use WITHOUT_%s=1 instead of WITH_%s=no", opt, opt);
}
/*
* True when `name' is a presence-only WITH_/WITHOUT_ knob on make/src
* (bsd.mkopt.mk tests defined(); src.conf(5) ignores the value).
*/
int
make_knob_p(const char *name)
{
if (format != BSDCONF_FORMAT_MAKE && format != BSDCONF_FORMAT_SRC)
return (0);
return (make_knob_name_p(name));
}
/*
* Print a directive/value pair according to the display flags. `path' is
* the configuration file holding the authoritative definition (or NULL).
*/
void
print_pair(const char *path, const char *directive, const char *value)
{
int knob;
if (show_file)
value = path != NULL ? path : "(none)";
if (verbose && path != NULL && !show_file)
printf("%s: ", path);
if (name_only) {
puts(directive);
return;
}
/* Presence knobs: any assigned value means present (defined()). */
knob = !show_file && make_knob_p(directive);
if (value_only) {
puts(value != NULL ? value : "");
return;
}
/*
* Presence display is for the human-readable form only. With -e,
* emit the real assignment (`NAME=') so the spelling is valid make
* and matches what -s writes.
*/
if (knob && !show_equals) {
printf("%s (present)\n", directive);
return;
}
if (show_equals && !show_file) {
if ((bsdconf_format_put(format) &
BSDCONF_PUT_QUOTE_ALWAYS) != 0)
printf("%s=\"%s\"\n", directive,
value != NULL ? value : "");
else
printf("%s=%s\n", directive,
value != NULL ? value : "");
return;
}
printf("%s: %s\n", directive, value != NULL ? value : "");
}
/*
* Spelling of a make(1) (or plain) assignment operator for `-V' trails.
*/
const char *
op_str(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_ASSIGN:
case BSDCONF_OP_DEFAULT:
default:
return ("=");
}
}
/*
* Append one assignment step to a `-V' trail. No-op when `-V' is unset.
* Returns zero on success; -1 (errno set) on allocation failure.
*/
int
trail_push(struct trail_step **trailp, size_t *ntrailp,
size_t fileidx, uint32_t line, enum bsdconf_op op,
const char *piece, const char *effective)
{
struct trail_step *nt;
char *ecopy;
char *pcopy;
if (!verbose_trail)
return (0);
if (piece == NULL)
piece = "";
if (effective == NULL)
effective = "";
if ((pcopy = strdup(piece)) == NULL)
return (-1);
if ((ecopy = strdup(effective)) == NULL) {
free(pcopy);
return (-1);
}
nt = realloc(*trailp, (*ntrailp + 1) * sizeof(*nt));
if (nt == NULL) {
free(pcopy);
free(ecopy);
return (-1);
}
*trailp = nt;
nt[*ntrailp].fileidx = fileidx;
nt[*ntrailp].line = line;
nt[*ntrailp].op = op;
nt[*ntrailp].piece = pcopy;
nt[*ntrailp].effective = ecopy;
(*ntrailp)++;
return (0);
}
void
trail_free(struct trail_step *trail, size_t ntrail)
{
size_t i;
for (i = 0; i < ntrail; i++) {
free(trail[i].piece);
free(trail[i].effective);
}
free(trail);
}
/*
* Print every recorded assignment step for `name'. Each line is
* `file:line: nameoppiece' and, when the running effective value differs
* from the fragment, ` -> effective'.
*/
void
print_trail(const char *name, const struct trail_step *trail, size_t ntrail)
{
const char *path;
size_t i;
for (i = 0; i < ntrail; i++) {
path = conf_files[trail[i].fileidx];
if (strcmp(trail[i].piece, trail[i].effective) != 0)
printf("%s:%u: %s%s%s -> %s\n", path, trail[i].line,
name, op_str(trail[i].op), trail[i].piece,
trail[i].effective);
else
printf("%s:%u: %s%s%s\n", path, trail[i].line, name,
op_str(trail[i].op), trail[i].piece);
}
}
/*
* Echo a write result. With -V: `file:line: name=old -> name=new',
* `name=value (unchanged)', or `name=value (added)', matching read-trail
* shape. With -v alone: prefix the pathname. Otherwise: `name: old -> new'
* / `name: value (unchanged)' (additions still use old -> new with an
* empty old value). With `-e', the non-trail form uses `=' (and quotes
* for formats that always quote), matching print_pair() on reads --
* except make/src WITH_/WITHOUT_ presence knobs, which use
* `(not present) -> (present)' when newly defined,
* `(present) (unchanged)' when `-s' finds them already defined or
* an explicit write leaves the value unchanged, and a normal `old ->
* new' echo when the placeholder value changes while still present
* (also for `-e'). Removals are only echoed when the caller passes
* `-v'/`-V' (not bare `-x'), using the same `name (removed)' / trail
* form as other directives.
*/
void
print_write_echo(const char *path, uint32_t line, const char *name,
enum bsdconf_op op, const char *oldv, const char *newv, int changed,
int added, int removed)
{
const char *ops;
const char *shown;
int knob;
int quote;
int was_present;
/* NULL oldv: absent; "" : present with empty assignment */
was_present = (oldv != NULL);
if (oldv == NULL)
oldv = "";
if (newv == NULL)
newv = "";
ops = op_str(op);
shown = oldv[0] != '\0' ? oldv : newv;
knob = make_knob_p(name) && op != BSDCONF_OP_APPEND;
if (verbose_trail && path != NULL) {
if (removed) {
printf("%s:%u: %s (removed)\n", path, line, name);
return;
}
if (knob) {
if (!was_present || added) {
printf("%s:%u: %s: (not present) -> "
"(present)\n", path, line, name);
return;
}
if (strcmp(oldv, newv) == 0) {
printf("%s:%u: %s: (present) (unchanged)\n",
path, line, name);
return;
}
/* value change while present: fall through */
}
if (added)
printf("%s:%u: %s%s%s (added)\n", path, line, name,
ops, newv);
else if (changed)
printf("%s:%u: %s%s%s -> %s%s%s\n", path, line, name,
ops, oldv, name, ops, newv);
else
printf("%s:%u: %s%s%s (unchanged)\n", path, line, name,
ops, shown);
return;
}
if (verbose && path != NULL)
printf("%s: ", path);
if (removed) {
printf("%s (removed)\n", name);
return;
}
if (knob) {
if (!was_present || added) {
printf("%s: (not present) -> (present)\n", name);
return;
}
if (strcmp(oldv, newv) == 0) {
printf("%s: (present) (unchanged)\n", name);
return;
}
/* value change while present: fall through */
}
quote = show_equals && !show_file &&
(bsdconf_format_put(format) & BSDCONF_PUT_QUOTE_ALWAYS) != 0;
if (show_equals && !show_file) {
if (changed || added) {
/* sysrc(8)-style: `name=old # -> new' with `-e' */
if (quote)
printf("%s=\"%s\" # -> \"%s\"\n", name, oldv,
newv);
else
printf("%s=%s # -> %s\n", name, oldv, newv);
} else if (quote)
printf("%s=\"%s\" (unchanged)\n", name, shown);
else
printf("%s=%s (unchanged)\n", name, shown);
return;
}
if (changed || added)
printf("%s: %s -> %s\n", name, oldv, newv);
else
printf("%s: %s (unchanged)\n", name, shown);
}
+203
View File
@@ -0,0 +1,203 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#ifndef _SYSCONF_PRIV_H_
#define _SYSCONF_PRIV_H_
#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 <inttypes.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.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 2026-08-16"
/* getopt(3) optstring; shared by parse_options() and find_target() */
#define OPTSTRING "AacdDEeFf:hij:k:lLnNqR:svVx"
/* One assignment step recorded for `-V' trails */
struct trail_step {
size_t fileidx; /* index into conf_files */
uint32_t line; /* 1-based line in that file */
enum bsdconf_op op; /* operator of this statement */
char *piece; /* unquoted fragment */
char *effective; /* value after applying the op */
};
/*
* One make(1) assignment statement for `name-=word' strikes: the last
* statement whose piece contains the word is rewritten (or removed if
* the piece becomes empty).
*/
struct make_assign {
size_t fileidx;
uint32_t line;
enum bsdconf_op op;
char *piece;
};
/* 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 append_present; /* identical make `+=value' line */
int remove; /* nonzero for `-x' removals */
int srcidx; /* conf file that last set the value
(authoritative); -1 if unset */
uint32_t line; /* 1-based line of that definition */
unsigned char *infile; /* per-file presence map */
struct trail_step *trail; /* `-V' assignment steps */
size_t ntrail;
struct make_assign *assigns; /* make `-=': per-statement pieces */
size_t nassigns;
};
/* 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 */
uint32_t line; /* 1-based line of that definition */
struct trail_step *trail; /* `-V' assignment steps */
size_t ntrail;
};
/* Request / dump state (defined in sysconf.c) */
extern struct request *reqs;
extern unsigned int nreqs;
extern struct dumpent *dumps;
extern size_t ndumps;
extern size_t dumpsize;
/* Resolved target state */
extern char **conf_files;
extern size_t nconf_files;
extern size_t conf_scanidx;
extern size_t write_idx;
extern int defaults_idx;
extern enum bsdconf_format format;
/* Display / mode flags */
extern const char *pgm;
extern bool check;
extern bool defaults_only;
extern bool dump_all;
extern bool existing_only;
extern bool ignore_unknown;
extern bool list_all;
extern bool list_files;
extern bool name_only;
extern bool quiet;
extern bool remove_mode;
extern bool set_knobs;
extern bool show_desc;
extern bool show_equals;
extern bool show_file;
extern bool value_only;
extern bool verbose;
extern bool verbose_trail;
extern bool with_defaults;
/* Option arguments */
extern const char *file;
extern bool file_stdin;
extern const char *jailname;
extern const char *module;
extern const char *rootdir;
/* sysconf_print.c */
int make_knob_name_p(const char *_name);
int make_knob_p(const char *_name);
void warn_with_equals_no(const char *_name, const char *_value,
const char *_path, uint32_t _line);
void print_pair(const char *_path, const char *_directive,
const char *_value);
const char *op_str(enum bsdconf_op _op);
int trail_push(struct trail_step **_trailp, size_t *_ntrailp,
size_t _fileidx, uint32_t _line, enum bsdconf_op _op,
const char *_piece, const char *_effective);
void trail_free(struct trail_step *_trail, size_t _ntrail);
void print_trail(const char *_name,
const struct trail_step *_trail, size_t _ntrail);
void print_write_echo(const char *_path, uint32_t _line,
const char *_name, enum bsdconf_op _op, const char *_oldv,
const char *_newv, int _changed, int _added, int _removed);
/* sysconf_resolve.c */
char *defaults_path(void);
void resolve_target(const char *_target);
int do_list(void);
/* sysconf_scan.c */
char *make_apply(enum bsdconf_op _op, const char *_old,
const char *_piece);
int scan_pass(int _sandbox);
/* sysconf_edit_assign.c */
void split_request(char *_arg, struct request *_req, int _remove);
int list_has(const char *_list, const char *_word, size_t _wlen);
char *list_merge(const char *_current, const char *_words, int _op);
int merge_list_requests(void);
/* sysconf_edit_make.c */
int make_strike_p(const struct request *_req);
int make_assign_push(struct request *_req, enum bsdconf_op _op,
const char *_piece, uint32_t _line);
int do_make_strikes(int _check_only);
/* sysconf_edit_write.c */
int do_checks(void);
int do_writes(void);
/* sysconf_query_value.c */
int do_reads(void);
/* sysconf_query_desc.c */
void load_descriptions(void);
void print_desc(const char *_directive);
int describe_defaults(void);
/* sysconf_query_sysctl.c */
#if defined(__FreeBSD__)
int sysctl_descr(const char *_name, char *_buf, size_t _bufsize);
int describe_sysctl(void);
int sysctl_writable(const char *_name, const char *_value,
int _quiet_unknown);
#endif
/* sysconf.c */
void usage(void);
void help(void);
#endif /* !_SYSCONF_PRIV_H_ */
+209
View File
@@ -0,0 +1,209 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Directive descriptions harvested from defaults-file comments (-d).
*/
#include "sysconf_priv.h"
/*
* 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 `='.
*/
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);
}
/*
* 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).
*/
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.
*/
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);
}
+285
View File
@@ -0,0 +1,285 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Running-kernel sysctl OID descriptions and writability checks.
*/
#include "sysconf_priv.h"
#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.
*/
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';
len = CTL_MAXNAME;
if (sysctlnametomib(name, mib, &len) != 0)
return (-1);
qoid[0] = CTL_SYSCTL;
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);
}
/*
* 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.
*/
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);
}
/*
* Return a short name for a CTLTYPE_* value (for warnings).
*/
static const char *
sysctl_typename(u_int kind)
{
switch (kind & CTLTYPE) {
case CTLTYPE_INT: return ("integer");
case CTLTYPE_UINT: return ("unsigned integer");
case CTLTYPE_LONG: return ("long integer");
case CTLTYPE_ULONG: return ("unsigned long");
case CTLTYPE_S8: return ("int8_t");
case CTLTYPE_S16: return ("int16_t");
case CTLTYPE_S32: return ("int32_t");
case CTLTYPE_S64: return ("int64_t");
case CTLTYPE_U8: return ("uint8_t");
case CTLTYPE_U16: return ("uint16_t");
case CTLTYPE_U32: return ("uint32_t");
case CTLTYPE_U64: return ("uint64_t");
case CTLTYPE_STRING: return ("string");
case CTLTYPE_OPAQUE: return ("opaque");
default: return ("unknown");
}
}
/*
* True if `value' can be represented as a signed integer of the given
* inclusive [min, max] range. Requires a full-string numeric parse.
*/
static int
sysctl_signed_ok(const char *value, intmax_t min, intmax_t max)
{
char *end;
intmax_t v;
errno = 0;
v = strtoimax(value, &end, 0);
if (errno != 0 || end == value || *end != '\0')
return (0);
return (v >= min && v <= max);
}
/*
* True if `value' can be represented as an unsigned integer <= max.
* Leading whitespace is not accepted (sysctl.conf values are trimmed by
* the caller before we see them); a leading minus is rejected so that
* strtoumax() cannot wrap a negative into a large positive.
*/
static int
sysctl_unsigned_ok(const char *value, uintmax_t max)
{
char *end;
uintmax_t v;
if (value[0] == '\0' || value[0] == '-')
return (0);
errno = 0;
v = strtoumax(value, &end, 0);
if (errno != 0 || end == value || *end != '\0')
return (0);
return (v <= max);
}
/*
* Validate that `value' fits the numeric CTLTYPE described by `kind'.
* `fmt' is the OIDFMT format string (may start with "IK" for Kelvin
* temperatures); those are left to sysctl(8) at apply time. String and
* opaque OIDs are accepted as-is. Returns zero if acceptable; -1 if not.
*/
static int
sysctl_value_ok(u_int kind, const char *fmt, const char *value)
{
if (value == NULL)
return (0);
switch (kind & CTLTYPE) {
case CTLTYPE_STRING:
case CTLTYPE_OPAQUE:
return (0);
case CTLTYPE_INT:
/*
* Temperature OIDs use an "IK" / "IK<digit>" display format
* with a specialized parser in sysctl(8); do not apply a
* plain integer range here.
*/
if (fmt != NULL && strncmp(fmt, "IK", 2) == 0)
return (0);
if (sysctl_signed_ok(value, INT_MIN, INT_MAX))
return (0);
break;
case CTLTYPE_UINT:
if (sysctl_unsigned_ok(value, UINT_MAX))
return (0);
break;
case CTLTYPE_LONG:
if (sysctl_signed_ok(value, LONG_MIN, LONG_MAX))
return (0);
break;
case CTLTYPE_ULONG:
if (sysctl_unsigned_ok(value, ULONG_MAX))
return (0);
break;
case CTLTYPE_S8:
if (sysctl_signed_ok(value, INT8_MIN, INT8_MAX))
return (0);
break;
case CTLTYPE_S16:
if (sysctl_signed_ok(value, INT16_MIN, INT16_MAX))
return (0);
break;
case CTLTYPE_S32:
if (sysctl_signed_ok(value, INT32_MIN, INT32_MAX))
return (0);
break;
case CTLTYPE_S64:
if (sysctl_signed_ok(value, INT64_MIN, INT64_MAX))
return (0);
break;
case CTLTYPE_U8:
if (sysctl_unsigned_ok(value, UINT8_MAX))
return (0);
break;
case CTLTYPE_U16:
if (sysctl_unsigned_ok(value, UINT16_MAX))
return (0);
break;
case CTLTYPE_U32:
if (sysctl_unsigned_ok(value, UINT32_MAX))
return (0);
break;
case CTLTYPE_U64:
if (sysctl_unsigned_ok(value, UINT64_MAX))
return (0);
break;
default:
/* NODE already rejected; unknown types: do not block */
return (0);
}
return (-1);
}
/*
* Determine whether the sysctl OID `name' can take effect when set from
* sysctl.conf(5) to `value': if the OID resolves it must be a leaf and
* writable at run time, and `value' must fit the OID's CTLTYPE (so an
* overflowing assignment cannot land in the conf file and surprise
* sysctl(8) at boot). An OID that is only tunable from the boot loader is
* reported as such (pointing at the loader target). An OID that does not
* resolve is still written -- sysctl.conf(5) commonly names OIDs from
* modules that are not yet loaded, and init(8) only warns -- so we warn
* (unless `quiet_unknown') and allow the assignment through; flag and
* range checks are only possible against a resolvable OID. Returns zero
* when the value should be written; -1 (after warning) otherwise.
*/
int
sysctl_writable(const char *name, const char *value, int quiet_unknown)
{
int mib[CTL_MAXNAME];
int qoid[CTL_MAXNAME + 2];
size_t len;
size_t size;
u_int kind;
u_char buf[BUFSIZ];
const char *fmt;
len = CTL_MAXNAME;
if (sysctlnametomib(name, mib, &len) != 0) {
if (!quiet_unknown)
warnx("WARNING: unknown oid '%s'", name);
return (0);
}
qoid[0] = CTL_SYSCTL;
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;
fmt = (const char *)(buf + sizeof(u_int));
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);
}
if (sysctl_value_ok(kind, fmt, value) != 0) {
warnx("oid '%s' value '%s' is invalid or out of range for %s",
name, value, sysctl_typename(kind));
return (-1);
}
return (0);
}
#endif /* __FreeBSD__ */
+79
View File
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Report effective directive values (named reads and -a).
*/
#include "sysconf_priv.h"
/*
* Report read requests (and `-a') from the results of a completed
* scan_pass(). Returns EXIT_SUCCESS or EXIT_FAILURE.
*/
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 (!quiet)
warn_with_equals_no(dumps[d].name,
dumps[d].value,
conf_files[dumps[d].srcidx],
dumps[d].line);
if (show_desc)
print_desc(dumps[d].name);
else if (verbose_trail)
print_trail(dumps[d].name, dumps[d].trail,
dumps[d].ntrail);
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;
}
if (!quiet)
warn_with_equals_no(reqs[n].name, reqs[n].value,
conf_files[reqs[n].srcidx], reqs[n].line);
if (verbose_trail)
print_trail(reqs[n].name, reqs[n].trail,
reqs[n].ntrail);
else
print_pair(conf_files[reqs[n].srcidx], reqs[n].name,
reqs[n].value);
}
return (rv);
}
+337
View File
@@ -0,0 +1,337 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Backing-file discovery and file-list reporting (-l/-L).
*/
#include "sysconf_priv.h"
static void apply_src_env_overrides(void);
/*
* 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.
*/
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).
*/
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) {
if (strcmp(target, "src-env") == 0)
warnx("unknown target `src-env'; use `src' "
"(includes src-env.conf in the build triad) "
"or `-f' for a specific file");
else
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);
/*
* The src triad honors the same environment overrides make(1)
* does when building /usr/src (verbatim, not subject to -R).
*/
if (format == BSDCONF_FORMAT_SRC)
apply_src_env_overrides();
/*
* 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++;
}
}
/*
* Replace src triad paths with SRC_ENV_CONF / __MAKE_CONF / SRCCONF when
* set and non-empty (same variables share/mk consults). Values are used
* verbatim, matching LOADER_DEFAULTS.
*/
static void
apply_src_env_overrides(void)
{
static const char *const envs[] = {
"SRC_ENV_CONF",
"__MAKE_CONF",
"SRCCONF",
};
const char *env;
char *path;
size_t i;
if (nconf_files != sizeof(envs) / sizeof(envs[0]))
return;
for (i = 0; i < nconf_files; i++) {
if ((env = getenv(envs[i])) == NULL || *env == '\0')
continue;
if ((path = strdup(env)) == NULL)
err(EXIT_FAILURE, NULL);
free(conf_files[i]);
conf_files[i] = path;
}
}
/*
* 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.
*/
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);
}
+288
View File
@@ -0,0 +1,288 @@
/*
* Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
* Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Ordered conf-file scanning into effective values, trails, and dumps.
*/
#include <ctype.h>
#include "sysconf_priv.h"
/*
* Apply a make(1)-style assignment operator to the running effective value
* of a directive. `+=' appends with a single space (as make(1) does);
* `?=' keeps the existing value when already defined; `=', `:=', and `!='
* replace. Returns newly allocated storage, or NULL on allocation failure.
* `old' may be NULL when the directive has not been seen yet.
*/
char *
make_apply(enum bsdconf_op op, const char *old, const char *piece)
{
size_t len;
char *out;
if (piece == NULL)
piece = "";
switch (op) {
case BSDCONF_OP_APPEND:
if (old == NULL || *old == '\0')
return (strdup(piece));
if (*piece == '\0')
return (strdup(old));
len = strlen(old) + 1 + strlen(piece) + 1;
if ((out = malloc(len)) == NULL)
return (NULL);
snprintf(out, len, "%s %s", old, piece);
return (out);
case BSDCONF_OP_COND:
if (old != NULL)
return (strdup(old));
return (strdup(piece));
case BSDCONF_OP_EXPAND:
case BSDCONF_OP_SHELL:
case BSDCONF_OP_ASSIGN:
case BSDCONF_OP_DEFAULT:
default:
return (strdup(piece));
}
}
/*
* Record `directive'/`value' in the dump union (for `-a'). For formats
* without make(1) operators a later assignment overrides an earlier one
* while retaining its original position. With BSDCONF_OPERATOR_EQUALS the
* operator is honored: `+=' accumulates, `?=' keeps a prior definition,
* and `=' / `:=' / `!=' replace -- matching make(1)'s effective value.
* Returns zero on success; -1 (errno set) on allocation failure.
*/
static int
dump_record(const char *directive, const char *value, enum bsdconf_op op,
uint32_t line)
{
char *merged;
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;
if ((bsdconf_format_processing(format) &
BSDCONF_OPERATOR_EQUALS) != 0) {
merged = make_apply(op, dumps[n].value, vcopy);
if (merged == NULL) {
free(vcopy);
return (-1);
}
free(dumps[n].value);
dumps[n].value = merged;
} else {
free(dumps[n].value);
dumps[n].value = vcopy;
vcopy = NULL; /* owned by dumps[n].value */
}
dumps[n].srcidx = (int)conf_scanidx;
dumps[n].line = line;
if (trail_push(&dumps[n].trail, &dumps[n].ntrail,
conf_scanidx, line, op,
vcopy != NULL ? vcopy : dumps[n].value,
dumps[n].value) != 0) {
free(vcopy);
return (-1);
}
free(vcopy);
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;
dumps[ndumps].line = line;
dumps[ndumps].trail = NULL;
dumps[ndumps].ntrail = 0;
if (trail_push(&dumps[ndumps].trail, &dumps[ndumps].ntrail,
conf_scanidx, line, op, vcopy, vcopy) != 0)
return (-1);
ndumps++;
return (0);
}
/*
* bsdconf_fparse() call-back; record each statement against the requests
* (and, with `-a', the dump union). For loader/sysctl/generic a later
* assignment overrides an earlier one -- both within a file and across the
* ordered file list. For make(1)-syntax targets the statement's operator
* (passed via option->op) drives accumulation so `foo=1' followed by
* `foo+=2' yields the effective value `1 2', as make -V reports.
*/
static int
scan_cb(struct bsdconf_option *option, uint32_t line,
char *directive, char *value)
{
char *copy;
char *merged;
enum bsdconf_op file_op;
unsigned int n;
file_op = (option != NULL) ? option->op : BSDCONF_OP_ASSIGN;
if (file_op == BSDCONF_OP_DEFAULT)
file_op = BSDCONF_OP_ASSIGN;
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);
bsdconf_unquote(copy);
/*
* An identical make(1) `+=value' line already in the file
* satisfies a pending `name+=value' write or check.
*/
if (reqs[n].op == BSDCONF_OP_APPEND &&
reqs[n].newvalue != NULL &&
file_op == BSDCONF_OP_APPEND &&
strcmp(copy, reqs[n].newvalue) == 0)
reqs[n].append_present = 1;
if ((bsdconf_format_processing(format) &
BSDCONF_OPERATOR_EQUALS) != 0) {
if (make_strike_p(&reqs[n]) &&
make_assign_push(&reqs[n], file_op, copy,
line) != 0) {
free(copy);
return (-1);
}
merged = make_apply(file_op, reqs[n].value, copy);
if (merged == NULL) {
free(copy);
return (-1);
}
free(reqs[n].value);
reqs[n].value = merged;
if (trail_push(&reqs[n].trail, &reqs[n].ntrail,
conf_scanidx, line, file_op, copy,
reqs[n].value) != 0) {
free(copy);
return (-1);
}
free(copy);
} else {
free(reqs[n].value);
reqs[n].value = copy;
if (trail_push(&reqs[n].trail, &reqs[n].ntrail,
conf_scanidx, line, file_op, copy, copy) != 0)
return (-1);
}
reqs[n].found = 1;
reqs[n].srcidx = (int)conf_scanidx;
reqs[n].line = line;
reqs[n].infile[conf_scanidx] = 1;
}
if (dump_all && dump_record(directive, value, file_op, line) != 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.
*/
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);
}
+11
View File
@@ -0,0 +1,11 @@
ATF_TESTS_SH+= sysconf_test
.include <bsd.test.mk>
#
# SCRIPTS install before DIRS in bsd.prog.mk; create TESTSDIR first so a
# leaf `make install' works before installworld has applied BSD.tests.dist.
#
beforeinstall: _sysconf_testdir
_sysconf_testdir: .PHONY
${INSTALL} -d -o ${BINOWN} -g ${BINGRP} -m 0755 ${DESTDIR}${TESTSDIR}
+431
View File
@@ -0,0 +1,431 @@
#
# Copyright (c) 2026 Devin Teske <[email protected]>
#
# SPDX-License-Identifier: BSD-2-Clause
#
#
# Resolve the sysconf binary: honor SYSCONF if set, else the sibling
# build product (objdir layout: .../sysconf/tests next to .../sysconf/sysconf),
# else PATH. Require a regular file: when installed under
# /usr/tests/usr.sbin/sysconf, `../sysconf' is this directory itself and
# `[ -x dir ]' is true for searchable directories.
#
find_sysconf()
{
if [ -n "$SYSCONF" ] && [ -f "$SYSCONF" ] && [ -x "$SYSCONF" ]; then
return
fi
if [ -f "$( atf_get_srcdir )/../sysconf" ] &&
[ -x "$( atf_get_srcdir )/../sysconf" ]
then
SYSCONF="$( atf_get_srcdir )/../sysconf"
elif [ -f "$( pwd )/../sysconf" ] && [ -x "$( pwd )/../sysconf" ]; then
SYSCONF="$( pwd )/../sysconf"
else
SYSCONF=$( command -v sysconf ) ||
atf_skip "sysconf binary not found"
fi
}
setup_root()
{
find_sysconf
ROOT="$( pwd )/root"
mkdir -p "$ROOT/etc" "$ROOT/boot/defaults"
}
cleanup_root()
{
rm -rf "$( pwd )/root"
}
atf_test_case make_append_accumulate cleanup
make_append_accumulate_head()
{
atf_set "descr" \
"make += accumulates on read and appends a line on write"
}
make_append_accumulate_body()
{
setup_root
printf 'foo=1\n' > "$ROOT/etc/make.conf"
atf_check -o inline:'foo: 1\n' "$SYSCONF" make -R "$ROOT" foo
printf 'foo+=2\n' >> "$ROOT/etc/make.conf"
atf_check -o inline:'foo: 1 2\n' "$SYSCONF" make -R "$ROOT" foo
atf_check -o inline:'foo: 1 2 -> 1 2 -O2\n' \
"$SYSCONF" make -R "$ROOT" 'foo+=-O2'
atf_check -o inline:'foo: 1 2 -O2\n' "$SYSCONF" make -R "$ROOT" foo
atf_check -o match:'foo\+=-O2' cat "$ROOT/etc/make.conf"
# -V on a new += statement reports (added), not `+= -> +=value'
atf_check -o match:'make.conf:[0-9]*: foo\+=-O3 \(added\)' \
"$SYSCONF" make -R "$ROOT" -V 'foo+=-O3'
atf_check -o inline:'foo: 1 2 -O2 -O3\n' "$SYSCONF" make -R "$ROOT" foo
# Identical += is a no-op (append_present)
before=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check "$SYSCONF" make -R "$ROOT" -c 'foo+=-O2'
after=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check_equal "$before" "$after"
}
make_append_accumulate_cleanup()
{
cleanup_root
}
atf_test_case src_triad_and_trail cleanup
src_triad_and_trail_head()
{
atf_set "descr" "src triad order, -V trail, writes prefer src.conf"
}
src_triad_and_trail_body()
{
setup_root
printf 'CFLAGS=-O0\n' > "$ROOT/etc/src-env.conf"
printf 'CFLAGS+=-g\n' > "$ROOT/etc/make.conf"
printf 'CFLAGS+=-pipe\n' > "$ROOT/etc/src.conf"
atf_check -o inline:'CFLAGS: -O0 -g -pipe\n' \
"$SYSCONF" src -R "$ROOT" CFLAGS
atf_check -o match:'src-env.conf:1: CFLAGS=-O0' \
-o match:'make.conf:1: CFLAGS\+=-g -> -O0 -g' \
-o match:'src.conf:1: CFLAGS\+=-pipe -> -O0 -g -pipe' \
"$SYSCONF" src -R "$ROOT" -V CFLAGS
atf_check -o match:'src.conf' \
"$SYSCONF" src -R "$ROOT" -v CFLAGS
# New directive lands in src.conf
atf_check "$SYSCONF" src -R "$ROOT" -q WITHOUT_FOO=
atf_check -o match:'WITHOUT_FOO=' cat "$ROOT/etc/src.conf"
atf_check -o not-match:'WITHOUT_FOO' cat "$ROOT/etc/make.conf"
}
src_triad_and_trail_cleanup()
{
cleanup_root
}
atf_test_case src_env_rejected
src_env_rejected_head()
{
atf_set "descr" "src-env keyword is rejected with a hint toward src"
}
src_env_rejected_body()
{
find_sysconf
atf_check -s not-exit:0 -e match:'src-env' -e match:'src' \
"$SYSCONF" src-env foo
}
atf_test_case env_overrides cleanup
env_overrides_head()
{
atf_set "descr" "SRC_ENV_CONF / __MAKE_CONF / SRCCONF remap the triad"
}
env_overrides_body()
{
setup_root
mkdir -p "$ROOT/alt"
printf 'X=from-env\n' > "$ROOT/alt/se.conf"
printf 'X+=from-make\n' > "$ROOT/alt/mk.conf"
printf 'X+=from-src\n' > "$ROOT/alt/sc.conf"
# Empty stock files so -R still resolves the triad slots
:> "$ROOT/etc/src-env.conf"
:> "$ROOT/etc/make.conf"
:> "$ROOT/etc/src.conf"
export SRC_ENV_CONF="$ROOT/alt/se.conf"
export __MAKE_CONF="$ROOT/alt/mk.conf"
export SRCCONF="$ROOT/alt/sc.conf"
atf_check -o inline:'X: from-env from-make from-src\n' \
"$SYSCONF" src -R "$ROOT" X
unset SRC_ENV_CONF __MAKE_CONF SRCCONF
}
env_overrides_cleanup()
{
cleanup_root
}
atf_test_case attached_f_passthrough cleanup
attached_f_passthrough_head()
{
atf_set "descr" \
"Attached -fPATH does not steal the next argv (rc as name)"
}
attached_f_passthrough_body()
{
setup_root
mkdir -p "$ROOT/boot"
printf 'zfs_load="YES"\n' > "$ROOT/boot/loader.conf"
#
# If find_target consumed 'loader' as -f's argument, 'rc' would become
# the target and exec sysrc(8). Correctly, target is loader and 'rc'
# is an unknown directive name.
#
atf_check -s not-exit:0 -e match:'unknown directive' \
"$SYSCONF" -R "$ROOT" -f/boot/loader.conf loader rc
}
attached_f_passthrough_cleanup()
{
cleanup_root
}
atf_test_case verbatim_backslash cleanup
verbatim_backslash_head()
{
atf_set "descr" \
"verbatim_ok re-examines the byte after an even \\\\ run"
}
verbatim_backslash_body()
{
setup_root
printf 'foo=1\n' > "$ROOT/etc/make.conf"
# Even-length \\ before # must not skip #: put must reject (Faraz)
atf_check -s not-exit:0 -e match:'Invalid argument' \
"$SYSCONF" make -R "$ROOT" 'foo=a\\#b'
# A value without a comment marker still writes.
atf_check -o match:'foo:' \
"$SYSCONF" make -R "$ROOT" 'foo=a\\b'
atf_check -o match:'foo=a\\\\b' cat "$ROOT/etc/make.conf"
}
verbatim_backslash_cleanup()
{
cleanup_root
}
atf_test_case make_strike_last_assign cleanup
make_strike_last_assign_head()
{
atf_set "descr" \
"make -= strikes the last assignment containing the word"
}
make_strike_last_assign_body()
{
setup_root
printf 'bar+=1\nbar+=1 2\nbar+=1 2 3\n' > "$ROOT/etc/make.conf"
atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=3'
atf_check -o inline:'bar+=1\nbar+=1 2\nbar+=1 2\n' \
cat "$ROOT/etc/make.conf"
atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=2'
atf_check -o inline:'bar+=1\nbar+=1 2\nbar+=1\n' \
cat "$ROOT/etc/make.conf"
atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=1'
atf_check -o inline:'bar+=1\nbar+=1 2\n' cat "$ROOT/etc/make.conf"
atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=1'
atf_check -o inline:'bar+=1\nbar+=2\n' cat "$ROOT/etc/make.conf"
atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=2'
atf_check -o inline:'bar+=1\n' cat "$ROOT/etc/make.conf"
atf_check -o inline:'bar: 1 (unchanged)\n' \
"$SYSCONF" make -R "$ROOT" 'bar-=9'
# Emptying the last word deletes the statement: (removed), like (added)
printf 'z+=only\n' >>"$ROOT/etc/make.conf"
atf_check -o match:'make.conf:[0-9]*: z\+=only \(removed\)' \
"$SYSCONF" make -R "$ROOT" -V 'z-=only'
atf_check -o not-match:'^z\+=' cat "$ROOT/etc/make.conf"
}
make_strike_last_assign_cleanup()
{
cleanup_root
}
atf_test_case equal_value_nocheck_mtime cleanup
equal_value_nocheck_mtime_head()
{
atf_set "descr" "Equal-value write and -c leave mtime alone"
}
equal_value_nocheck_mtime_body()
{
setup_root
printf 'bar=1\n' > "$ROOT/etc/make.conf"
sleep 1
before=$( stat -f '%m' "$ROOT/etc/make.conf" )
# Equal-value write: informational echo, file untouched
atf_check -o inline:'bar: 1 (unchanged)\n' \
"$SYSCONF" make -R "$ROOT" bar=1
# -e uses `=' for write echoes the same as for reads
atf_check -o inline:'bar=1 (unchanged)\n' \
"$SYSCONF" make -R "$ROOT" -e bar=1
after=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check_equal "$before" "$after"
# -q suppresses the echo; -c is silent and does not write
atf_check -o empty "$SYSCONF" make -R "$ROOT" -q bar=1
atf_check -o empty "$SYSCONF" make -R "$ROOT" -c bar=1
after2=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check_equal "$before" "$after2"
atf_check -o inline:'bar=1 # -> 2\n' \
"$SYSCONF" make -R "$ROOT" -e bar=2
atf_check -o inline:'bar=2\n' \
"$SYSCONF" make -R "$ROOT" -e bar
}
equal_value_nocheck_mtime_cleanup()
{
cleanup_root
}
atf_test_case sysctl_oid_range cleanup
sysctl_oid_range_head()
{
atf_set "descr" \
"sysctl target rejects values that overflow the OID CTLTYPE"
}
sysctl_oid_range_body()
{
find_sysconf
case "$( uname -s )" in
FreeBSD) ;;
*) atf_skip "requires FreeBSD sysctl OIDFMT" ;;
esac
# -f alone (no -R) still validates against the running kernel.
conf="$( pwd )/sysctl.conf"
:> "$conf"
atf_check -s exit:1 -e match:'out of range for integer' \
"$SYSCONF" sysctl -f "$conf" \
security.bsd.see_other_uids=99999999999
atf_check -o empty cat "$conf"
atf_check -s exit:1 -e match:'out of range for unsigned integer' \
"$SYSCONF" sysctl -f "$conf" kern.ipc.somaxconn=-1
atf_check -s exit:1 -e match:'out of range for unsigned integer' \
"$SYSCONF" sysctl -f "$conf" kern.ipc.somaxconn=abc
atf_check -o match:'see_other_uids' \
"$SYSCONF" sysctl -f "$conf" security.bsd.see_other_uids=0
atf_check -o match:'see_other_uids=0' cat "$conf"
}
sysctl_oid_range_cleanup()
{
rm -f "$( pwd )/sysctl.conf"
}
atf_test_case make_src_knobs cleanup
make_src_knobs_head()
{
atf_set "descr" \
"-s WITH_/WITHOUT_ presence, query display, unchanged, -x"
}
make_src_knobs_body()
{
setup_root
:> "$ROOT/etc/make.conf"
:> "$ROOT/etc/sysctl.conf"
:> "$ROOT/etc/src-env.conf"
:> "$ROOT/etc/src.conf"
# -s only on make/src
atf_check -s exit:1 -e match:'only valid for make and src' \
"$SYSCONF" sysctl -R "$ROOT" -s WITHOUT_FOO
# name must be WITH_/WITHOUT_
atf_check -s exit:1 -e match:'WITH_ or WITHOUT_' \
"$SYSCONF" make -R "$ROOT" -s CFLAGS
# no value with -s
atf_check -s exit:1 -e match:'does not take a value' \
"$SYSCONF" make -R "$ROOT" -s 'WITHOUT_FOO='
atf_check -o inline:'WITHOUT_FOO: (not present) -> (present)\n' \
"$SYSCONF" make -R "$ROOT" -s WITHOUT_FOO
atf_check -o match:'^WITHOUT_FOO=$' cat "$ROOT/etc/make.conf"
atf_check -o inline:'WITHOUT_FOO (present)\n' \
"$SYSCONF" make -R "$ROOT" WITHOUT_FOO
atf_check -o inline:'WITHOUT_FOO=\n' \
"$SYSCONF" make -R "$ROOT" -e WITHOUT_FOO
atf_check -o inline:'\n' \
"$SYSCONF" make -R "$ROOT" -n WITHOUT_FOO
before=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check -o inline:'WITHOUT_FOO: (present) (unchanged)\n' \
"$SYSCONF" make -R "$ROOT" -s WITHOUT_FOO
after=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check_equal "$before" "$after"
# Non-empty placeholder (=t) still means present (defined())
atf_check -o inline:'WITHOUT_BAR: (not present) -> (present)\n' \
"$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=t'
atf_check -o match:'^WITHOUT_BAR=t$' cat "$ROOT/etc/make.conf"
atf_check -o inline:'WITHOUT_BAR (present)\n' \
"$SYSCONF" make -R "$ROOT" WITHOUT_BAR
atf_check -o inline:'WITHOUT_BAR=t\n' \
"$SYSCONF" make -R "$ROOT" -e WITHOUT_BAR
atf_check -o inline:'t\n' \
"$SYSCONF" make -R "$ROOT" -n WITHOUT_BAR
before=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check -o inline:'WITHOUT_BAR: (present) (unchanged)\n' \
"$SYSCONF" make -R "$ROOT" -s WITHOUT_BAR
after=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check_equal "$before" "$after"
atf_check -o match:'^WITHOUT_BAR=t$' cat "$ROOT/etc/make.conf"
# Explicit name= always writes (incl. empty); echo value changes
atf_check -o inline:'WITHOUT_BAR: t -> \n' \
"$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR='
atf_check -o match:'^WITHOUT_BAR=$' cat "$ROOT/etc/make.conf"
atf_check -o inline:'WITHOUT_BAR: -> 123\n' \
"$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=123'
atf_check -o match:'^WITHOUT_BAR=123$' cat "$ROOT/etc/make.conf"
atf_check -o inline:'WITHOUT_BAR: 123 -> 456\n' \
"$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=456'
atf_check -o match:'^WITHOUT_BAR=456$' cat "$ROOT/etc/make.conf"
# -s still no-op while present with a value
before=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check -o inline:'WITHOUT_BAR: (present) (unchanged)\n' \
"$SYSCONF" make -R "$ROOT" -s WITHOUT_BAR
after=$( stat -f '%m' "$ROOT/etc/make.conf" )
atf_check_equal "$before" "$after"
atf_check -o match:'^WITHOUT_BAR=456$' cat "$ROOT/etc/make.conf"
# src prefers src.conf
atf_check -o inline:'WITHOUT_LLDB: (not present) -> (present)\n' \
"$SYSCONF" src -R "$ROOT" -s WITHOUT_LLDB
atf_check -o match:'^WITHOUT_LLDB=$' cat "$ROOT/etc/src.conf"
# -x is quiet unless -v/-V (same as non-knob removals)
atf_check -o empty \
"$SYSCONF" src -R "$ROOT" -x WITHOUT_LLDB
atf_check -o empty cat "$ROOT/etc/src.conf"
atf_check -o inline:'WITHOUT_LLDB: (not present) -> (present)\n' \
"$SYSCONF" src -R "$ROOT" -s WITHOUT_LLDB
atf_check -o match:'WITHOUT_LLDB \(removed\)' \
"$SYSCONF" src -R "$ROOT" -vx WITHOUT_LLDB
atf_check -o empty cat "$ROOT/etc/src.conf"
# WITH_*=no (exact lowercase) warns like bsd.mkopt.mk; WITHOUT_*=no
# and WITH_*=NO do not
atf_check -e match:'Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
-o match:'WITH_FOO' \
"$SYSCONF" make -R "$ROOT" 'WITH_FOO=no'
atf_check -o match:'^WITH_FOO=no$' cat "$ROOT/etc/make.conf"
atf_check -e empty -o match:'WITH_BAR' \
"$SYSCONF" make -R "$ROOT" 'WITH_BAR=NO'
atf_check -e empty -o match:'WITHOUT_BAZ' \
"$SYSCONF" make -R "$ROOT" 'WITHOUT_BAZ=no'
# Reads remind with file:line (presence display hides =no)
atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
-o inline:'WITH_FOO (present)\n' \
"$SYSCONF" make -R "$ROOT" WITH_FOO
atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
-o match:'WITH_FOO \(present\)' \
"$SYSCONF" make -R "$ROOT" -a
atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
-o inline:'WITH_FOO=no\n' \
"$SYSCONF" make -R "$ROOT" -e WITH_FOO
atf_check -e empty -o inline:'WITH_FOO (present)\n' \
"$SYSCONF" make -R "$ROOT" -q WITH_FOO
# WITHOUT_MODULES is not a presence knob
atf_check -s exit:1 -e match:'WITHOUT_MODULES takes a module list' \
"$SYSCONF" make -R "$ROOT" -s WITHOUT_MODULES
atf_check -o match:'WITHOUT_MODULES: -> plip' \
"$SYSCONF" make -R "$ROOT" 'WITHOUT_MODULES=plip'
atf_check -o inline:'WITHOUT_MODULES: plip\n' \
"$SYSCONF" make -R "$ROOT" WITHOUT_MODULES
}
make_src_knobs_cleanup()
{
cleanup_root
}
atf_init_test_cases()
{
atf_add_test_case make_src_knobs
atf_add_test_case make_append_accumulate
atf_add_test_case make_strike_last_assign
atf_add_test_case src_triad_and_trail
atf_add_test_case src_env_rejected
atf_add_test_case env_overrides
atf_add_test_case attached_f_passthrough
atf_add_test_case verbatim_backslash
atf_add_test_case equal_value_nocheck_mtime
atf_add_test_case sysctl_oid_range
}