mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-28 11:25:21 +01:00
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:
1 parent
f308d6d484
commit
3fe5961a0b
47 files changed
+10120
No files matched your search
@@ -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)")
|
||||
|
||||
@@ -1323,6 +1323,8 @@
|
||||
..
|
||||
sa
|
||||
..
|
||||
sysconf
|
||||
..
|
||||
syslogd
|
||||
..
|
||||
sysrc
|
||||
|
||||
@@ -36,6 +36,7 @@ SUBDIR= ${SUBDIR_BOOTSTRAP} \
|
||||
libarchive \
|
||||
libbegemot \
|
||||
libblocksruntime \
|
||||
libbsdconf \
|
||||
libbsddialog \
|
||||
libbsdstat \
|
||||
libbsm \
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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 .
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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]
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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_ */
|
||||
@@ -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_ */
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -118,6 +118,7 @@ _LIBRARIES= \
|
||||
be \
|
||||
begemot \
|
||||
bluetooth \
|
||||
bsdconf \
|
||||
bsdxml \
|
||||
bsm \
|
||||
bsnmp \
|
||||
|
||||
@@ -87,6 +87,7 @@ SUBDIR= adduser \
|
||||
snapinfo \
|
||||
spi \
|
||||
spray \
|
||||
sysconf \
|
||||
syslogd \
|
||||
sysrc \
|
||||
tcpdrop \
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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__ */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user