diff --git a/.github/workflows/build-test-musl.sh b/.github/workflows/build-test-musl.sh new file mode 100755 index 0000000000..f0f72d6e03 --- /dev/null +++ b/.github/workflows/build-test-musl.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +set -eux + +if ! command -v musl-gcc >/dev/null; then + echo "musl-gcc is not installed, skipping the test." + exit 77 +fi + +TMPDIR=$(mktemp -d) + +cleanup() ( + set +e + + if [[ -d "$TMPDIR" ]]; then + rm -rf "$TMPDIR" + fi +) + +trap cleanup EXIT ERR INT TERM + +mkdir -p "${TMPDIR}/build" +mkdir -p "${TMPDIR}/usr/include" +mkdir -p "${TMPDIR}/usr/lib64/pkgconfig" + +CFLAGS="-idirafter ${TMPDIR}/usr/include" +export PKG_CONFIG_PATH="${TMPDIR}"/usr/lib64/pkgconfig + +LINKS=( + acl + archive.h + archive_entry.h + asm + asm-generic + audit-records.h + audit_logging.h + bpf + bzlib.h + curl + dwarf.h + elfutils + fido.h + gcrypt.h + gelf.h + gnutls + gpg-error.h + idn2.h + libaudit.h + libcryptsetup.h + libelf.h + libkmod.h + linux + lz4.h + lz4frame.h + lz4hc.h + lzma + lzma.h + microhttpd.h + mtd + openssl + pcre2.h + pwquality.h + qrencode.h + seccomp-syscalls.h + seccomp.h + security + selinux + sys/acl.h + sys/capability.h + tss2 + xen + xkbcommon + zconf.h + zlib.h + zstd.h + zstd_errors.h +) + +for t in "${LINKS[@]}"; do + [[ -e /usr/include/"$t" ]] + link="${TMPDIR}"/usr/include/"${t}" + mkdir -p "${link%/*}" + ln -s /usr/include/"$t" "$link" +done + +env \ + CC=musl-gcc \ + CXX=musl-gcc \ + CFLAGS="$CFLAGS" \ + CXXFLAGS="$CFLAGS" \ + meson setup --werror -Ddbus-interfaces-dir=no -Dlibc=musl "${TMPDIR}"/build + +ninja -v -C "${TMPDIR}"/build diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index c107b2da1e..c99d34705a 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,6 +49,10 @@ jobs: [Build] ToolsTreeDistribution=fedora ToolsTreeRelease=rawhide + ToolsTreePackages= + libgcrypt-devel + libgpg-error-devel + musl-gcc EOF mkosi -f box -- true @@ -77,3 +81,6 @@ jobs: - name: Run clang-tidy run: mkosi box -- meson test -C build --suite=clang-tidy --print-errorlogs --no-stdsplit + + - name: Build with musl + run: mkosi box -- .github/workflows/build-test-musl.sh diff --git a/.github/workflows/unit-tests-musl.sh b/.github/workflows/unit-tests-musl.sh new file mode 100755 index 0000000000..c3307a62b3 --- /dev/null +++ b/.github/workflows/unit-tests-musl.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +# shellcheck disable=SC2206 +PHASES=(${@:-SETUP BUILD RUN CLEANUP}) + +function info() { + echo -e "\033[33;1m$1\033[0m" +} + +function run_meson() { + if ! meson "$@"; then + find . -type f -name meson-log.txt -exec cat '{}' + + return 1 + fi +} + +set -ex + +for phase in "${PHASES[@]}"; do + case $phase in + SETUP) + info "Setup phase" + + # Alpine still uses split-usr. + for i in /bin/* /sbin/*; do + ln -rs "$i" "/usr/$i"; + done + ;; + BUILD) + info "Build systemd phase" + + run_meson setup --werror -Dtests=unsafe -Dslow-tests=true -Dfuzz-tests=true -Dlibc=musl build + ninja -v -C build + ;; + RUN) + info "Run phase" + + # Create dummy machine ID. + echo '052e58f661f94bd080e258b96aea3f7b' > /etc/machine-id + + # Start dbus for several unit tests. + mkdir -p /var/run/dbus + /usr/bin/dbus-daemon --system || : + + # Here, we explicitly set SYSTEMD_IN_CHROOT=yes as unfortunately runnin_in_chroot() does not + # correctly detect the environment. + env \ + SYSTEMD_IN_CHROOT=yes \ + meson test -C build --print-errorlogs + ;; + CLEANUP) + info "Cleanup phase" + ;; + *) + echo >&2 "Unknown phase '$phase'" + exit 1 + esac +done diff --git a/.github/workflows/unit-tests-musl.yml b/.github/workflows/unit-tests-musl.yml new file mode 100644 index 0000000000..fca3eef019 --- /dev/null +++ b/.github/workflows/unit-tests-musl.yml @@ -0,0 +1,113 @@ +--- +# vi: ts=2 sw=2 et: +# SPDX-License-Identifier: LGPL-2.1-or-later +# +name: Unit tests (musl) +on: + pull_request: + paths: + - '**/meson.build' + - '.github/workflows/**' + - 'meson_options.txt' + - 'src/**' + - 'test/fuzz/**' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Repository checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Install build dependencies + uses: jirutka/setup-alpine@v1 + with: + arch: x86_64 + branch: edge + packages: > + acl + acl-dev + audit-dev + bash + bash-completion-dev + bpftool + build-base + bzip2-dev + coreutils + cryptsetup-dev + curl-dev + dbus + dbus-dev + elfutils-dev + gettext-dev + git + glib-dev + gnutls-dev + gperf + grep + iproute2 + iptables-dev + kbd + kexec-tools + kmod + kmod-dev + libapparmor-dev + libarchive-dev + libbpf-dev + libcap-dev + libcap-utils + libfido2-dev + libgcrypt-dev + libidn2-dev + libmicrohttpd-dev + libpwquality-dev + libqrencode-dev + libseccomp-dev + libselinux-dev + libxkbcommon-dev + linux-pam-dev + lz4-dev + meson + openssl + openssl-dev + p11-kit-dev + pcre2-dev + pkgconf + polkit-dev + py3-elftools + py3-jinja2 + py3-pefile + py3-pytest + py3-lxml + quota-tools + rsync + sfdisk + tpm2-tss-dev + tpm2-tss-esys + tpm2-tss-rc + tpm2-tss-tcti-device + tzdata + util-linux-dev + util-linux-login + util-linux-misc + utmps-dev + valgrind-dev + xen-dev + zlib-dev + zstd-dev + + - name: Setup + run: .github/workflows/unit-tests-musl.sh SETUP + shell: alpine.sh --root {0} + - name: Build + run: .github/workflows/unit-tests-musl.sh BUILD + shell: alpine.sh {0} + - name: Run + run: .github/workflows/unit-tests-musl.sh RUN + shell: alpine.sh --root {0} + - name: Cleanup + run: .github/workflows/unit-tests-musl.sh CLEANUP + shell: alpine.sh --root {0} diff --git a/meson.build b/meson.build index 055ee31ee4..fc305a8565 100644 --- a/meson.build +++ b/meson.build @@ -1028,6 +1028,7 @@ threads = dependency('threads') librt = cc.find_library('rt') libm = cc.find_library('m') libdl = cc.find_library('dl') +libutmps = dependency('libutmps', required : false) # On some distributions that use musl (e.g. Alpine), libintl.h may be provided by gettext rather than musl. # In that case, we need to explicitly link with libintl.so. diff --git a/src/basic/glob-util.c b/src/basic/glob-util.c index 5843ef088f..eda7bcb6c5 100644 --- a/src/basic/glob-util.c +++ b/src/basic/glob-util.c @@ -9,6 +9,78 @@ #include "string-util.h" #include "strv.h" +#ifndef __GLIBC__ +static bool safe_glob_verify(const char *p, const char *prefix) { + if (isempty(p)) + return false; /* should not happen, but for safey. */ + + if (prefix) { + /* Skip the prefix, as we allow dots in prefix. + * Note, glob() does not normalize paths, hence do not use path_startswith(). */ + p = startswith(p, prefix); + if (!p) + return false; /* should not happen, but for safety. */ + } + + for (;;) { + p += strspn(p, "/"); + if (*p == '\0') + return true; + if (*p == '.') { + p++; + if (IN_SET(*p, '/', '\0')) + return false; /* refuse dot */ + if (*p == '.') { + p++; + if (IN_SET(*p, '/', '\0')) + return false; /* refuse dot dot */ + } + } + + p += strcspn(p, "/"); + if (*p == '\0') + return true; + } +} + +static int filter_glob_result(char * const *result, const char *path, char ***ret) { + int r; + + assert(path); + + if (strv_isempty(result)) + return -ENOENT; + + _cleanup_free_ char *prefix = NULL; + r = glob_non_glob_prefix(path, &prefix); + if (r < 0 && r != -ENOENT) + return r; + + _cleanup_strv_free_ char **filtered = NULL; + size_t n_filtered = 0; + STRV_FOREACH(p, result) { + if (!safe_glob_verify(*p, prefix)) + continue; + + if (!ret) + return 0; /* Found at least one entry, let's return earlier. */ + + /* When musl is used, each entry is not a head of allocated memory. Hence, it is + * necessary to copy the string. */ + r = strv_extend_with_size(&filtered, &n_filtered, *p); + if (r < 0) + return r; + } + + if (n_filtered == 0) + return -ENOENT; + + assert(ret); + *ret = TAKE_PTR(filtered); + return 0; +} +#endif + DEFINE_TRIVIAL_DESTRUCTOR(closedir_wrapper, void, closedir); int safe_glob_full(const char *path, int flags, opendir_t opendir_func, char ***ret) { @@ -32,6 +104,10 @@ int safe_glob_full(const char *path, int flags, opendir_t opendir_func, char *** if (r != 0) return errno_or_else(EIO); +#ifndef __GLIBC__ + return filter_glob_result(g.gl_pathv, path, ret); +#endif + if (strv_isempty(g.gl_pathv)) return -ENOENT; diff --git a/src/include/musl/glob.h b/src/include/musl/glob.h new file mode 100644 index 0000000000..12faa8ebfc --- /dev/null +++ b/src/include/musl/glob.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include_next + +#ifndef GLOB_ALTDIRFUNC +#define GLOB_ALTDIRFUNC (1 << 9) +#define gl_flags __dummy1 +#define gl_closedir __dummy2[0] +#define gl_readdir __dummy2[1] +#define gl_opendir __dummy2[2] +#define gl_lstat __dummy2[3] +#define gl_stat __dummy2[4] +#endif + +#ifndef GLOB_BRACE +#define GLOB_BRACE (1 << 10) +#endif diff --git a/src/include/musl/printf.h b/src/include/musl/printf.h new file mode 100644 index 0000000000..aa5382613a --- /dev/null +++ b/src/include/musl/printf.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* Copyright 2014 Emil Renner Berthing */ +#pragma once + +#include + +enum { /* C type: */ + PA_INT, /* int */ + PA_CHAR, /* int, cast to char */ + PA_WCHAR, /* wide char */ + PA_STRING, /* const char *, a '\0'-terminated string */ + PA_WSTRING, /* const wchar_t *, wide character string */ + PA_POINTER, /* void * */ + PA_FLOAT, /* float */ + PA_DOUBLE, /* double */ + PA_LAST, +}; + +/* Flag bits that can be set in a type returned by `parse_printf_format'. */ +#define PA_FLAG_MASK 0xff00 +#define PA_FLAG_LONG_LONG (1 << 8) +#define PA_FLAG_LONG_DOUBLE PA_FLAG_LONG_LONG +#define PA_FLAG_LONG (1 << 9) +#define PA_FLAG_SHORT (1 << 10) +#define PA_FLAG_PTR (1 << 11) + +size_t parse_printf_format(const char *fmt, size_t n, int *types); diff --git a/src/include/musl/utmpx.h b/src/include/musl/utmpx.h new file mode 100644 index 0000000000..d95fc8f25e --- /dev/null +++ b/src/include/musl/utmpx.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include_next + +#ifndef UTMPX_FILE +#define UTMPX_FILE "/run/utmp" +#endif + +#ifndef WTMPX_FILE +#define WTMPX_FILE "/var/log/wtmp" +#endif + +#ifndef ACCOUNTING +#define ACCOUNTING 9 +#endif diff --git a/src/libc/musl/meson.build b/src/libc/musl/meson.build index 54866a8b61..387f04dac4 100644 --- a/src/libc/musl/meson.build +++ b/src/libc/musl/meson.build @@ -5,6 +5,7 @@ if get_option('libc') != 'musl' endif libc_wrapper_sources += files( + 'printf.c', 'stdio.c', 'stdlib.c', 'string.c', diff --git a/src/libc/musl/printf.c b/src/libc/musl/printf.c new file mode 100644 index 0000000000..6be5733f39 --- /dev/null +++ b/src/libc/musl/printf.c @@ -0,0 +1,263 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* Copyright 2014 Emil Renner Berthing */ + +#include +#include +#include +#include + +static const char* consume_nonarg(const char *fmt) { + do { + if (*fmt == '\0') + return fmt; + } while (*fmt++ != '%'); + return fmt; +} + +static const char* consume_num(const char *fmt) { + for (;*fmt >= '0' && *fmt <= '9'; fmt++) + /* do nothing */; + return fmt; +} + +static const char* consume_argn(const char *fmt, size_t *arg) { + const char *p = fmt; + size_t val = 0; + + if (*p < '1' || *p > '9') + return fmt; + do { + val = 10*val + (*p++ - '0'); + } while (*p >= '0' && *p <= '9'); + + if (*p != '$') + return fmt; + *arg = val; + return p+1; +} + +static const char* consume_flags(const char *fmt) { + for (;;) + switch (*fmt) { + case '#': + case '0': + case '-': + case ' ': + case '+': + case '\'': + case 'I': + fmt++; + continue; + default: + return fmt; + } +} + +enum state { + BARE, + LPRE, + LLPRE, + HPRE, + HHPRE, + BIGLPRE, + ZTPRE, + JPRE, + STOP, +}; + +enum type { + NONE, + PTR, + STR, + WSTR, + INT, + SHORT, + LONG, + LLONG, + IMAX, + SIZET, + CHAR, + WCHAR, + DBL, + LDBL, + NPTR, + _TYPE_MAX, +}; + +static const short pa_types[_TYPE_MAX] = { + [NONE] = PA_INT, + [PTR] = PA_POINTER, + [STR] = PA_STRING, + [WSTR] = PA_WSTRING, + [INT] = PA_INT, + [SHORT] = PA_INT | PA_FLAG_SHORT, + [LONG] = PA_INT | PA_FLAG_LONG, +#if ULLONG_MAX > ULONG_MAX + [LLONG] = PA_INT | PA_FLAG_LONG_LONG, +#else + [LLONG] = PA_INT | PA_FLAG_LONG, +#endif +#if UINTMAX_MAX > ULONG_MAX + [IMAX] = PA_INT | PA_FLAG_LONG_LONG, +#elif UINTMAX_MAX > UINT_MAX + [IMAX] = PA_INT | PA_FLAG_LONG, +#else + [IMAX] = PA_INT, +#endif +#if SIZE_MAX > ULONG_MAX + [SIZET] = PA_INT | PA_FLAG_LONG_LONG, +#elif SIZE_MAX > UINT_MAX + [SIZET] = PA_INT | PA_FLAG_LONG, +#else + [SIZET] = PA_INT, +#endif + [CHAR] = PA_CHAR, + [WCHAR] = PA_WCHAR, + [DBL] = PA_DOUBLE, + [LDBL] = PA_DOUBLE | PA_FLAG_LONG_DOUBLE, + [NPTR] = PA_FLAG_PTR, +}; + +#define S(x) [(x)-'A'] +#define E(x) (STOP + (x)) + +static const unsigned char states[]['z'-'A'+1] = { + { /* 0: bare types */ + S('d') = E(INT), S('i') = E(INT), + S('o') = E(INT), S('u') = E(INT), S('x') = E(INT), S('X') = E(INT), + S('e') = E(DBL), S('f') = E(DBL), S('g') = E(DBL), S('a') = E(DBL), + S('E') = E(DBL), S('F') = E(DBL), S('G') = E(DBL), S('A') = E(DBL), + S('c') = E(CHAR), S('C') = E(WCHAR), + S('s') = E(STR), S('S') = E(WSTR), S('p') = E(PTR), + S('n') = E(NPTR), + S('m') = E(NONE), + S('l') = LPRE, S('q') = LLPRE, S('h') = HPRE, S('L') = BIGLPRE, + S('z') = ZTPRE, S('Z') = ZTPRE, S('j') = JPRE, S('t') = ZTPRE, + }, + { /* 1: l-prefixed */ + S('d') = E(LONG), S('i') = E(LONG), + S('o') = E(LONG), S('u') = E(LONG), S('x') = E(LONG), S('X') = E(LONG), + S('e') = E(DBL), S('f') = E(DBL), S('g') = E(DBL), S('a') = E(DBL), + S('E') = E(DBL), S('F') = E(DBL), S('G') = E(DBL), S('A') = E(DBL), + S('c') = E(CHAR), S('s') = E(STR), + S('n') = E(NPTR), + S('l') = LLPRE, + }, + { /* 2: ll-prefixed */ + S('d') = E(LLONG), S('i') = E(LLONG), + S('o') = E(LLONG), S('u') = E(LLONG), S('x') = E(LLONG), S('X') = E(LLONG), + S('n') = E(NPTR), + }, + { /* 3: h-prefixed */ + S('d') = E(SHORT), S('i') = E(SHORT), + S('o') = E(SHORT), S('u') = E(SHORT), S('x') = E(SHORT), S('X') = E(SHORT), + S('n') = E(NPTR), + S('h') = HHPRE, + }, + { /* 4: hh-prefixed */ + S('d') = E(CHAR), S('i') = E(CHAR), + S('o') = E(CHAR), S('u') = E(CHAR), S('x') = E(CHAR), S('X') = E(CHAR), + S('n') = E(NPTR), + }, + { /* 5: L-prefixed */ + S('e') = E(LDBL), S('f') = E(LDBL), S('g') = E(LDBL), S('a') = E(LDBL), + S('E') = E(LDBL), S('F') = E(LDBL), S('G') = E(LDBL), S('A') = E(LDBL), + }, + { /* 6: z- or t-prefixed (assumed to be same size) */ + S('d') = E(SIZET), S('i') = E(SIZET), + S('o') = E(SIZET), S('u') = E(SIZET), S('x') = E(SIZET), S('X') = E(SIZET), + S('n') = E(NPTR), + }, + { /* 7: j-prefixed */ + S('d') = E(IMAX), S('i') = E(IMAX), + S('o') = E(IMAX), S('u') = E(IMAX), S('x') = E(IMAX), S('X') = E(IMAX), + S('n') = E(NPTR), + }, +}; + +size_t parse_printf_format(const char *fmt, size_t n, int *types) { + size_t i = 0; + size_t last = 0; + + memset(types, 0, n); + + for (;;) { + size_t arg; + + fmt = consume_nonarg(fmt); + if (*fmt == '\0') + break; + if (*fmt == '%') { + fmt++; + continue; + } + arg = 0; + fmt = consume_argn(fmt, &arg); + /* flags */ + fmt = consume_flags(fmt); + /* width */ + if (*fmt == '*') { + size_t warg = 0; + fmt = consume_argn(fmt+1, &warg); + if (warg == 0) + warg = ++i; + if (warg > last) + last = warg; + if (warg <= n && types[warg-1] == NONE) + types[warg-1] = INT; + } else + fmt = consume_num(fmt); + /* precision */ + if (*fmt == '.') { + fmt++; + if (*fmt == '*') { + size_t parg = 0; + fmt = consume_argn(fmt+1, &parg); + if (parg == 0) + parg = ++i; + if (parg > last) + last = parg; + if (parg <= n && types[parg-1] == NONE) + types[parg-1] = INT; + } else { + if (*fmt == '-') + fmt++; + fmt = consume_num(fmt); + } + } + /* length modifier and conversion specifier */ + unsigned state = BARE; + for (;;) { + unsigned char c = *fmt; + + if (c == '\0') + break; + + fmt++; + + if (c < 'A' || c > 'z') + break; + + state = states[state]S(c); + if (state == 0 || state >= STOP) + break; + } + + if (state <= STOP) /* %m or invalid format */ + continue; + + if (arg == 0) + arg = ++i; + if (arg > last) + last = arg; + if (arg <= n) + types[arg-1] = state - STOP; + } + + if (last > n) + last = n; + for (i = 0; i < last; i++) + types[i] = pa_types[types[i]]; + + return last; +} diff --git a/src/login/meson.build b/src/login/meson.build index 0ad9beb167..e6cd002c6c 100644 --- a/src/login/meson.build +++ b/src/login/meson.build @@ -49,6 +49,7 @@ executables += [ 'include_directories' : [libexec_template['include_directories'], include_directories('.')], 'extract' : systemd_logind_extract_sources, 'dependencies' : [ + libutmps, threads, ], }, diff --git a/src/shared/meson.build b/src/shared/meson.build index acc80d3e34..eebadf6d3f 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -385,6 +385,7 @@ libshared_deps = [threads, librt, libseccomp_cflags, libselinux_cflags, + libutmps, libxenctrl_cflags, libxz_cflags, libzstd_cflags, diff --git a/src/test/meson.build b/src/test/meson.build index 9f89ce8e58..d51911bd5b 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -164,6 +164,7 @@ simple_tests += files( 'test-percent-util.c', 'test-pidref.c', 'test-pretty-print.c', + 'test-printf.c', 'test-prioq.c', 'test-proc-cmdline.c', 'test-procfs-util.c', @@ -472,6 +473,7 @@ executables += [ test_template + { 'sources' : files('test-utmp.c'), 'conditions' : ['ENABLE_UTMP'], + 'dependencies' : libutmps, }, test_template + { 'sources' : files('test-varlink.c'), diff --git a/src/test/test-printf.c b/src/test/test-printf.c new file mode 100644 index 0000000000..8a105c26bb --- /dev/null +++ b/src/test/test-printf.c @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "memory-util.h" +#include "strv.h" +#include "tests.h" + +static void test_parse_printf_format_one(const char *fmt, size_t m, const int *expected) { + log_debug("/* %s(%s) */", __func__, fmt); + + int types[128] = {}; + size_t n = parse_printf_format(fmt, ELEMENTSOF(types), types); + + for (size_t i = 0; i < MAX(n, m); i++) + if (i < MIN(n, m)) + log_debug("types[%zu]=%i, expected[%zu]=%i", i, types[i], i, expected[i]); + else if (i < n) + log_debug("types[%zu]=%i, expected[%zu]=n/a", i, types[i], i); + else + log_debug("types[%zu]=n/a, expected[%zu]=%i", i, i, expected[i]); + + ASSERT_EQ(memcmp_nn(types, n * sizeof(int), expected, m * sizeof(int)), 0); +} + +TEST(parse_printf_format) { + static struct { + const char *prefix; + int expected; + } integer_table[] = { + { "", PA_INT }, + { "hh", PA_CHAR }, + { "h", PA_INT | PA_FLAG_SHORT }, + { "l", PA_INT | PA_FLAG_LONG }, +#if ULLONG_MAX > ULONG_MAX + { "ll", PA_INT | PA_FLAG_LONG_LONG }, +#else + { "ll", PA_INT | PA_FLAG_LONG }, +#endif +#if UINTMAX_MAX > ULONG_MAX + { "j", PA_INT | PA_FLAG_LONG_LONG }, +#elif UINTMAX_MAX > UINT_MAX + { "j", PA_INT | PA_FLAG_LONG }, +#else + { "j", PA_INT }, +#endif +#if SIZE_MAX > ULONG_MAX + { "z", PA_INT | PA_FLAG_LONG_LONG }, + { "Z", PA_INT | PA_FLAG_LONG_LONG }, + { "t", PA_INT | PA_FLAG_LONG_LONG }, +#elif SIZE_MAX > UINT_MAX + { "z", PA_INT | PA_FLAG_LONG }, + { "Z", PA_INT | PA_FLAG_LONG }, + { "t", PA_INT | PA_FLAG_LONG }, +#else + { "z", PA_INT }, + { "Z", PA_INT }, + { "t", PA_INT }, +#endif + }, float_table[] = { + { "", PA_DOUBLE }, + { "L", PA_DOUBLE | PA_FLAG_LONG_DOUBLE }, + }; + + FOREACH_ELEMENT(i, integer_table) { + _cleanup_free_ char *fmt = NULL; + + FOREACH_STRING(s, "d", "i", "o", "u", "x", "X") { + ASSERT_NOT_NULL(fmt = strjoin("%", i->prefix, s)); + test_parse_printf_format_one(fmt, 1, &i->expected); + fmt = mfree(fmt); + } + + ASSERT_NOT_NULL(fmt = strjoin("%", i->prefix, "n")); + test_parse_printf_format_one(fmt, 1, (int[]){ PA_INT | PA_FLAG_PTR }); + + fmt = mfree(fmt); + + ASSERT_NOT_NULL(fmt = strjoin("%", i->prefix)); + test_parse_printf_format_one(fmt, 0, NULL); + } + + FOREACH_ELEMENT(i, float_table) { + _cleanup_free_ char *fmt = NULL; + + FOREACH_STRING(s, "e", "E", "f", "F", "g", "G", "a", "A") { + ASSERT_NOT_NULL(fmt = strjoin("%", i->prefix, s)); + test_parse_printf_format_one(fmt, 1, &i->expected); + fmt = mfree(fmt); + } + + ASSERT_NOT_NULL(fmt = strjoin("%", i->prefix)); + test_parse_printf_format_one(fmt, 0, NULL); + } + + test_parse_printf_format_one("%c", 1, (int[]) { PA_CHAR }); + test_parse_printf_format_one("%lc", 1, (int[]) { PA_CHAR }); + test_parse_printf_format_one("%C", 1, (int[]) { PA_WCHAR }); + + test_parse_printf_format_one("%s", 1, (int[]) { PA_STRING }); + test_parse_printf_format_one("%ls", 1, (int[]) { PA_STRING }); + test_parse_printf_format_one("%S", 1, (int[]) { PA_WSTRING }); + + test_parse_printf_format_one("%p", 1, (int[]) { PA_POINTER }); + + test_parse_printf_format_one("%m", 0, NULL); + test_parse_printf_format_one("%%", 0, NULL); + + test_parse_printf_format_one("asfhghejmlahpgakdmsalc", 0, NULL); + test_parse_printf_format_one( + "%d%i%o%u%x%X", 6, + (int[]) { PA_INT, PA_INT, PA_INT, PA_INT, PA_INT, PA_INT }); + test_parse_printf_format_one( + "%e%E%f%F%g%G%a%A", 8, + (int[]) { PA_DOUBLE, PA_DOUBLE, PA_DOUBLE, PA_DOUBLE, PA_DOUBLE, PA_DOUBLE, PA_DOUBLE, PA_DOUBLE }); + test_parse_printf_format_one( + "%c%s%C%S%p%n%m%%", 6, + (int[]) { PA_CHAR, PA_STRING, PA_WCHAR, PA_WSTRING, PA_POINTER, PA_INT | PA_FLAG_PTR }); + test_parse_printf_format_one( + "%03d%-05d%+i%hhu%hu%hx%lx", 7, + (int[]) { PA_INT, PA_INT, PA_INT, PA_CHAR, PA_INT | PA_FLAG_SHORT, PA_INT | PA_FLAG_SHORT, PA_INT | PA_FLAG_LONG }); +} + +DEFINE_TEST_MAIN(LOG_DEBUG);