diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f72e0..2dde838 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,9 +55,6 @@ jobs: - name: "Configure" run: | tests/configure.sh --datadir="$PWD/data" --enable-memcheck - - name: "Check sparse" - run: | - make check-sparse V=1 - name: "Clean" run: | make clean @@ -87,28 +84,27 @@ jobs: matrix: include: - os: ubuntu-latest - cc: gcc + cflags: compiler: gcc libc: glibc configure: check: unittest e2e - os: ubuntu-latest - cc: clang + cflags: compiler: clang libc: glibc configure: check: unittest e2e - os: ubuntu-latest - cc: musl-gcc -static -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/ + cflags: -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/ compiler: gcc libc: musl - configure: --disable-libkeymap --disable-vlock + configure: --host=x86_64-linux-musl --disable-vlock --without-zlib --without-bzip2 --without-lzma --without-zstd check: unittest e2e fail-fast: false runs-on: ${{ matrix.os }} needs: [ distcheck_job ] env: - CC: ${{ matrix.cc }} CHECK_KEYWORDS: ${{ matrix.check }} SANDBOX: priviliged TTY: /dev/tty60 @@ -126,7 +122,7 @@ jobs: tests/configure.sh --datadir="$PWD/tests/data" --enable-memcheck ${{ matrix.configure }} - name: "Build" run: | - make V=1 CFLAGS+="-g -O0" + make V=1 CFLAGS+="-g -O0 ${{ matrix.cflags }}" - name: "Check" run: | sudo -E tests/check.sh diff --git a/configure.ac b/configure.ac index d468370..ac4a512 100644 --- a/configure.ac +++ b/configure.ac @@ -220,6 +220,66 @@ if test "$VLOCK_PROG" = "yes"; then fi fi +AC_ARG_WITH([zlib], + [AS_HELP_STRING([--with-zlib], + [support zlib compression @<:@default=auto@:>@])], + [], + [: m4_divert_text([DEFAULTS], [with_zlib=yes])]) + +AS_IF([test "x$with_zlib" != xno], + [PKG_CHECK_MODULES(ZLIB, zlib, [HAVE_ZLIB=yes], [HAVE_ZLIB=no])], + [HAVE_ZLIB=no]) +AS_IF([test "x$HAVE_ZLIB" = "xyes"], + [AC_DEFINE([HAVE_ZLIB], [1], [support zlib compression])]) +AM_CONDITIONAL(USE_ZLIB, test "x$HAVE_ZLIB" = "xyes") + +AC_ARG_WITH([bzip2], + [AS_HELP_STRING([--with-bzip2], + [support bzip2 compression @<:@default=auto@:>@])], + [], + [: m4_divert_text([DEFAULTS], [with_bzip2=yes])]) + +AS_IF([test "x$with_bzip2" != xno], + [PKG_CHECK_MODULES(BZIP2, bzip2, [HAVE_BZIP2=yes], [HAVE_BZIP2=no])], + [HAVE_BZIP2=no]) + +if test "x$HAVE_BZIP2" = xno; then + AC_CHECK_LIB(bz2, BZ2_bzDecompressInit, [ + HAVE_BZIP2=yes + BZIP2_LIBS=-lbz2 + BZIP2_CFLAGS='' + ], [HAVE_BZIP2=no]) +fi +AS_IF([test "x$HAVE_BZIP2" = "xyes"], + [AC_DEFINE([HAVE_BZIP2], [1], [support bzip2 compression])]) +AM_CONDITIONAL(USE_BZIP2, test "$HAVE_BZIP2" = "yes") + +AC_ARG_WITH([lzma], + [AS_HELP_STRING([--with-lzma], + [support lzma compression @<:@default=auto@:>@])], + [], + [: m4_divert_text([DEFAULTS], [with_lzma=yes])]) + +AS_IF([test "x$with_lzma" != xno], + [PKG_CHECK_MODULES(LZMA, liblzma, [HAVE_LZMA=yes], [HAVE_LZMA=no])], + [HAVE_LZMA=no]) +AS_IF([test "x$HAVE_LZMA" = "xyes"], + [AC_DEFINE([HAVE_LZMA], [1], [support lzma compression])]) +AM_CONDITIONAL(USE_LZMA, test "$HAVE_LZMA" = "yes") + +AC_ARG_WITH([zstd], + [AS_HELP_STRING([--with-zstd], + [support zstd compression @<:@default=auto@:>@])], + [], + [: m4_divert_text([DEFAULTS], [with_zstd=yes])]) + +AS_IF([test "x$with_zstd" != xno], + [PKG_CHECK_MODULES(ZSTD, libzstd, [HAVE_ZSTD=yes], [HAVE_ZSTD=no])], + [HAVE_ZSTD=no]) +AS_IF([test "x$HAVE_ZSTD" = "xyes"], + [AC_DEFINE([HAVE_ZSTD], [1], [support zstd compression])]) +AM_CONDITIONAL(USE_ZSTD, test "$HAVE_ZSTD" = "yes") + AC_ARG_ENABLE(tests, [AS_HELP_STRING([--disable-tests], [do not build tests])], [build_tests=$enableval], [build_tests=auto]) diff --git a/src/libkbdfile/Makefile.am b/src/libkbdfile/Makefile.am index 06bac46..2511064 100644 --- a/src/libkbdfile/Makefile.am +++ b/src/libkbdfile/Makefile.am @@ -10,9 +10,34 @@ headers = \ libkbdfile_la_SOURCES = \ $(headers) \ contextP.h \ + elf-note.h \ + elf-note.c \ init.c \ kbdfile.c +libkbdfile_la_LIBADD = +libkbdfile_la_CFLAGS = + +if USE_ZLIB +libkbdfile_la_SOURCES += kbdfile-zlib.c +libkbdfile_la_CFLAGS += $(ZLIB_CFLAGS) +endif + +if USE_BZIP2 +libkbdfile_la_SOURCES += kbdfile-bzip2.c +libkbdfile_la_CFLAGS += $(BZIP2_CFLAGS) +endif + +if USE_LZMA +libkbdfile_la_SOURCES += kbdfile-lzma.c +libkbdfile_la_CFLAGS += $(LZMA_CFLAGS) +endif + +if USE_ZSTD +libkbdfile_la_SOURCES += kbdfile-zstd.c +libkbdfile_la_CFLAGS += $(ZSTD_CFLAGS) +endif + KBDFILE_CURRENT = 1 KBDFILE_REVISION = 0 KBDFILE_AGE = 0 diff --git a/src/libkbdfile/contextP.h b/src/libkbdfile/contextP.h index 59e9e84..95ba7c7 100644 --- a/src/libkbdfile/contextP.h +++ b/src/libkbdfile/contextP.h @@ -33,6 +33,7 @@ struct kbdfile { #define KBDFILE_CTX_INITIALIZED 0x01 #define KBDFILE_PIPE 0x02 +#define KBDFILE_COMPRESSED 0x04 #define kbdfile_log_cond(ctx, level, arg...) \ do { \ @@ -68,4 +69,36 @@ struct kbdfile { */ #define ERR(ctx, arg...) kbdfile_log_cond(ctx, LOG_ERR, ##arg) +char *kbd_strerror(int errnum, char *buf, size_t buflen); + +static inline FILE *kbdfile_decompressor_dummy(struct kbdfile *file KBD_ATTR_UNUSED) +{ + return NULL; +} + +#define kbdfile_decompressor_zlib kbdfile_decompressor_dummy +#define kbdfile_decompressor_bzip2 kbdfile_decompressor_dummy +#define kbdfile_decompressor_lzma kbdfile_decompressor_dummy +#define kbdfile_decompressor_zstd kbdfile_decompressor_dummy + +#ifdef HAVE_ZLIB +#undef kbdfile_decompressor_zlib +FILE *kbdfile_decompressor_zlib(struct kbdfile *file) KBD_ATTR_MUST_CHECK; +#endif + +#ifdef HAVE_BZIP2 +#undef kbdfile_decompressor_bzip2 +FILE *kbdfile_decompressor_bzip2(struct kbdfile *file) KBD_ATTR_MUST_CHECK; +#endif + +#ifdef HAVE_LZMA +#undef kbdfile_decompressor_lzma +FILE *kbdfile_decompressor_lzma(struct kbdfile *file) KBD_ATTR_MUST_CHECK; +#endif + +#ifdef HAVE_ZSTD +#undef kbdfile_decompressor_zstd +FILE *kbdfile_decompressor_zstd(struct kbdfile *file) KBD_ATTR_MUST_CHECK; +#endif + #endif /* KBDFILE_CONTEXTP_H */ diff --git a/src/libkbdfile/elf-note.c b/src/libkbdfile/elf-note.c new file mode 100644 index 0000000..842be9d --- /dev/null +++ b/src/libkbdfile/elf-note.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "elf-note.h" + +static int dlsym_manyv(void *dl, va_list ap) +{ + void (**fn)(void); + + while ((fn = va_arg(ap, typeof(fn)))) { + const char *symbol; + + symbol = va_arg(ap, typeof(symbol)); + *fn = dlsym(dl, symbol); + if (!*fn) + return -ENXIO; + } + + return 0; +} + +int dlsym_many(void **dlp, const char *filename, ...) +{ + va_list ap; + void *dl; + int r; + + if (*dlp) + return 0; + + dl = dlopen(filename, RTLD_LAZY); + if (!dl) + return -ENOENT; + + va_start(ap, filename); + r = dlsym_manyv(dl, ap); + va_end(ap); + + if (r < 0) { + dlclose(dl); + return r; + } + + *dlp = dl; + + return 1; +} diff --git a/src/libkbdfile/elf-note.h b/src/libkbdfile/elf-note.h new file mode 100644 index 0000000..a0fd9e2 --- /dev/null +++ b/src/libkbdfile/elf-note.h @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#define XSTRINGIFY(x) #x +#define STRINGIFY(x) XSTRINGIFY(x) + +#define XCONCATENATE(x, y) x##y +#define CONCATENATE(x, y) XCONCATENATE(x, y) +#define UNIQ(x) CONCATENATE(x, __COUNTER__) +#define UNIQ_T(x, uniq) CONCATENATE(__unique_prefix_, CONCATENATE(x, uniq)) + +/* + * Load many various symbols from @filename. + * @dlp: pointer to the previous results of this call: it's set when it succeeds + * @filename: the library to dlopen() and look for symbols + * @...: or 1 more tuples created by DLSYM_ARG() with ( &var, "symbol name" ). + */ +int dlsym_many(void **dlp, const char *filename, ...); + +/* + * Helper to create tuples passed as arguments to dlsym_many(). + * @symbol__: symbol to create arguments for. Example: DLSYM_ARG(foo) expands to + * `&sym_foo, "foo"` + */ +#define DLSYM_ARG(symbol__) &sym_##symbol__, STRINGIFY(symbol__), + +/* For symbols being dynamically loaded */ +#define DECLARE_DLSYM(symbol) static typeof(symbol) *sym_##symbol + +/* + * Helper defines, to be done locally before including this header to switch between + * implementations + */ +#define DECLARE_SYM(sym__) DECLARE_DLSYM(sym__); + +/* + * Originally from systemd codebase. + * + * Reference: https://systemd.io/ELF_PACKAGE_METADATA/ + */ + +#define ELF_NOTE_DLOPEN_VENDOR "FDO" +#define ELF_NOTE_DLOPEN_TYPE UINT32_C(0x407c0c0a) +#define ELF_NOTE_DLOPEN_PRIORITY_REQUIRED "required" +#define ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED "recommended" +#define ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED "suggested" + +/* + * Add an ".note.dlopen" ELF note to our binary that declares our weak dlopen() + * dependency. This information can be read from an ELF file via + * "readelf -p .note.dlopen" or an equivalent command. + */ +#define _ELF_NOTE_DLOPEN(json, variable_name) \ + __attribute__((used, section(".note.dlopen"))) _Alignas(sizeof(uint32_t)) \ + static const struct { \ + struct { \ + uint32_t n_namesz, n_descsz, n_type; \ + } nhdr; \ + char name[sizeof(ELF_NOTE_DLOPEN_VENDOR)]; \ + _Alignas(sizeof(uint32_t)) char dlopen_json[sizeof(json)]; \ + } variable_name = { \ + .nhdr = { \ + .n_namesz = sizeof(ELF_NOTE_DLOPEN_VENDOR), \ + .n_descsz = sizeof(json), \ + .n_type = ELF_NOTE_DLOPEN_TYPE, \ + }, \ + .name = ELF_NOTE_DLOPEN_VENDOR, \ + .dlopen_json = json, \ + } + +#define _SONAME_ARRAY1(a) "[\""a"\"]" +#define _SONAME_ARRAY2(a, b) "[\""a"\",\""b"\"]" +#define _SONAME_ARRAY3(a, b, c) "[\""a"\",\""b"\",\""c"\"]" +#define _SONAME_ARRAY4(a, b, c, d) "[\""a"\",\""b"\",\""c"\"",\""d"\"]" +#define _SONAME_ARRAY5(a, b, c, d, e) "[\""a"\",\""b"\",\""c"\"",\""d"\",\""e"\"]" +#define _SONAME_ARRAY_GET(_1,_2,_3,_4,_5,NAME,...) NAME +#define _SONAME_ARRAY(...) _SONAME_ARRAY_GET(__VA_ARGS__, _SONAME_ARRAY5, _SONAME_ARRAY4, _SONAME_ARRAY3, _SONAME_ARRAY2, _SONAME_ARRAY1)(__VA_ARGS__) + +/* + * The 'priority' must be one of 'required', 'recommended' or 'suggested' as per + * specification, use the macro defined above to specify it. + * Multiple sonames can be passed and they will be automatically constructed + * into a json array (but note that due to preprocessor language limitations if + * more than the limit defined above is used, a new _SONAME_ARRAY will need + * to be added). + */ +#define ELF_NOTE_DLOPEN(feature, description, priority, ...) \ + _ELF_NOTE_DLOPEN("[{\"feature\":\"" feature "\",\"description\":\"" description "\",\"priority\":\"" priority "\",\"soname\":" _SONAME_ARRAY(__VA_ARGS__) "}]", UNIQ_T(s, UNIQ)) diff --git a/src/libkbdfile/kbdfile-bzip2.c b/src/libkbdfile/kbdfile-bzip2.c new file mode 100644 index 0000000..5ab3e6a --- /dev/null +++ b/src/libkbdfile/kbdfile-bzip2.c @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "config.h" + +#include + +#include +#include +#include +#include + +#include + +#include "contextP.h" +#include "elf-note.h" + +#define DL_SYMBOL_TABLE(M) \ + M(BZ2_bzopen) \ + M(BZ2_bzclose) \ + M(BZ2_bzread) \ + M(BZ2_bzerror) + +DL_SYMBOL_TABLE(DECLARE_SYM) + +static int dlopen_note(void) +{ + static void *dl; + + ELF_NOTE_DLOPEN("bzip2", + "Support for uncompressing bzip2-compressed files", + ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED, + "libbz2.so.1"); + + return dlsym_many(&dl, "libbz2.so.1", DL_SYMBOL_TABLE(DLSYM_ARG) NULL); +} + +FILE *kbdfile_decompressor_bzip2(struct kbdfile *file) +{ + char errbuf[200]; + int retcode; + BZFILE *zf = NULL; + FILE *outf = NULL; + int memfd = -1; + + retcode = dlopen_note(); + if (retcode < 0) { + ERR(file->ctx, "bzip2: can't load and resolve symbols: %s", + kbd_strerror(-retcode, errbuf, sizeof(errbuf))); + return NULL; + } + + retcode = -1; + + memfd = memfd_create(file->pathname, MFD_CLOEXEC); + if (memfd < 0) { + ERR(file->ctx, "unable to open in-memory file: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + zf = sym_BZ2_bzopen(file->pathname, "rb"); + if (!zf) { + ERR(file->ctx, "bzip2: unable to open archive: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + while (1) { + int read_bytes; + char outbuf[BUFSIZ]; + + read_bytes = sym_BZ2_bzread(zf, outbuf, sizeof(outbuf)); + if (read_bytes < 0) { + int zerrno; + ERR(file->ctx, "bzip2: read error: %s", + sym_BZ2_bzerror(zf, &zerrno)); + goto cleanup; + } + if (read_bytes == 0) { + retcode = 0; + break; + } + + size_t to_write = (size_t) read_bytes; + size_t written = 0; + + while (written < to_write) { + ssize_t w = write(memfd, outbuf + written, to_write - written); + + if (w < 0) { + if (errno == EINTR) + continue; + + ERR(file->ctx, "unable to write data: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + goto cleanup; + } + written += (size_t) w; + } + } + +cleanup: + if (zf) + sym_BZ2_bzclose(zf); + + if (retcode == 0) { + lseek(memfd, 0L, SEEK_SET); + + outf = fdopen(memfd, "r"); + if (!outf) { + ERR(file->ctx, "unable to create file stream from file descriptor: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + if (memfd >= 0) + close(memfd); + } + } else if (memfd >= 0) { + close(memfd); + } + + return outf; +} diff --git a/src/libkbdfile/kbdfile-lzma.c b/src/libkbdfile/kbdfile-lzma.c new file mode 100644 index 0000000..fee2e6b --- /dev/null +++ b/src/libkbdfile/kbdfile-lzma.c @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "config.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#include "contextP.h" +#include "elf-note.h" + +#define DL_SYMBOL_TABLE(M) \ + M(lzma_stream_decoder) \ + M(lzma_code) \ + M(lzma_end) + +DL_SYMBOL_TABLE(DECLARE_SYM) + +static int dlopen_lzma(void) +{ + static void *dl; + + ELF_NOTE_DLOPEN("lzma", + "Support for uncompressing xz-compressed files", + ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED, + "liblzma.so.5"); + + return dlsym_many(&dl, "liblzma.so.5", DL_SYMBOL_TABLE(DLSYM_ARG) NULL); +} + +FILE *kbdfile_decompressor_lzma(struct kbdfile *file) +{ + char errbuf[200]; + int retcode; + FILE *outf = NULL; + int infd = -1; + int memfd = -1; + + lzma_stream strm = LZMA_STREAM_INIT; + lzma_action action = LZMA_RUN; + + retcode = dlopen_lzma(); + if (retcode < 0) { + ERR(file->ctx, "lzma: can't load and resolve symbols: %s", + kbd_strerror(-retcode, errbuf, sizeof(errbuf))); + return NULL; + } + + retcode = -1; + + if (sym_lzma_stream_decoder(&strm, UINT64_MAX, LZMA_CONCATENATED) != LZMA_OK) + goto cleanup; + + infd = open(file->pathname, O_RDONLY); + if (infd < 0) { + ERR(file->ctx, "unable to open xz-archive file: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + memfd = memfd_create(file->pathname, MFD_CLOEXEC); + if (memfd < 0) { + ERR(file->ctx, "unable to open in-memory file: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + uint8_t inpbuf[BUFSIZ]; + uint8_t outbuf[BUFSIZ * 4]; + int eof = 0; + + while (1) { + if (!eof && strm.avail_in == 0) { + ssize_t read_bytes = read(infd, inpbuf, sizeof(inpbuf)); + + if (read_bytes < 0) { + ERR(file->ctx, "unable to read xz-archive: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } else if (read_bytes == 0) { + eof = 1; + action = LZMA_FINISH; + } else { + strm.next_in = inpbuf; + strm.avail_in = (size_t) read_bytes; + } + } + + strm.next_out = outbuf; + strm.avail_out = sizeof(outbuf); + + lzma_ret code_ret = sym_lzma_code(&strm, action); + + if (code_ret != LZMA_OK && code_ret != LZMA_STREAM_END) { + ERR(file->ctx, "lzma: decode error (return code %d)", code_ret); + goto cleanup; + } + + size_t to_write = sizeof(outbuf) - strm.avail_out; + size_t written = 0; + + while (written < to_write) { + ssize_t w = write(memfd, outbuf + written, to_write - written); + if (w < 0) { + if (errno == EINTR) + continue; + + ERR(file->ctx, "unable to write data: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + goto cleanup; + } + written += (size_t) w; + } + + if (code_ret == LZMA_STREAM_END) { + retcode = 0; + goto cleanup; + } + } + +cleanup: + if (infd >= 0) + close(infd); + + sym_lzma_end(&strm); + + if (retcode == 0) { + lseek(memfd, 0L, SEEK_SET); + + outf = fdopen(memfd, "r"); + if (!outf) { + ERR(file->ctx, "unable to create file stream from file descriptor: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + if (memfd >= 0) + close(memfd); + } + } else if (memfd >= 0) { + close(memfd); + } + + return outf; +} diff --git a/src/libkbdfile/kbdfile-zlib.c b/src/libkbdfile/kbdfile-zlib.c new file mode 100644 index 0000000..f495003 --- /dev/null +++ b/src/libkbdfile/kbdfile-zlib.c @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "config.h" + +#include + +#include +#include +#include +#include + +#include + +#include "contextP.h" +#include "elf-note.h" + +#define DL_SYMBOL_TABLE(M) \ + M(gzclose) \ + M(gzopen) \ + M(gzerror) \ + M(gzread) + +DL_SYMBOL_TABLE(DECLARE_SYM) + +static int dlopen_note(void) +{ + static void *dl; + + ELF_NOTE_DLOPEN("zlib", + "Support for uncompressing zlib-compressed files", + ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED, + "libz.so.1"); + + return dlsym_many(&dl, "libz.so.1", DL_SYMBOL_TABLE(DLSYM_ARG) NULL); +} + +FILE *kbdfile_decompressor_zlib(struct kbdfile *file) +{ + char errbuf[200]; + int retcode; + gzFile gzf = NULL; + FILE *outf = NULL; + int memfd = -1; + + retcode = dlopen_note(); + if (retcode < 0) { + ERR(file->ctx, "zlib: can't load and resolve symbols: %s", + kbd_strerror(-retcode, errbuf, sizeof(errbuf))); + return NULL; + } + + retcode = -1; + + memfd = memfd_create(file->pathname, MFD_CLOEXEC); + if (memfd < 0) { + ERR(file->ctx, "unable to open in-memory file: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + gzf = sym_gzopen(file->pathname, "rb"); + if (!gzf) { + ERR(file->ctx, "zlib: unable to open archive: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + while (1) { + int read_bytes; + char outbuf[BUFSIZ]; + + read_bytes = sym_gzread(gzf, outbuf, sizeof(outbuf)); + + if (read_bytes < 0) { + int gzerr; + ERR(file->ctx, "zlib: read error: %s", + sym_gzerror(gzf, &gzerr)); + goto cleanup; + } + if (read_bytes == 0) { + retcode = 0; + break; + } + + size_t to_write = (size_t) read_bytes; + size_t written = 0; + + while (written < to_write) { + ssize_t w = write(memfd, outbuf + written, to_write - written); + + if (w < 0) { + if (errno == EINTR) + continue; + + ERR(file->ctx, "unable to write data: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + goto cleanup; + } + written += (size_t) w; + } + } + +cleanup: + sym_gzclose(gzf); + + if (retcode == 0) { + lseek(memfd, 0L, SEEK_SET); + + outf = fdopen(memfd, "r"); + if (!outf) { + ERR(file->ctx, "unable to create file stream from file descriptor: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + if (memfd >= 0) + close(memfd); + } + } else if (memfd >= 0) { + close(memfd); + } + + return outf; +} diff --git a/src/libkbdfile/kbdfile-zstd.c b/src/libkbdfile/kbdfile-zstd.c new file mode 100644 index 0000000..806199c --- /dev/null +++ b/src/libkbdfile/kbdfile-zstd.c @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "config.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#include "contextP.h" +#include "elf-note.h" + +#define DL_SYMBOL_TABLE(M) \ + M(ZSTD_createDStream) \ + M(ZSTD_decompressStream) \ + M(ZSTD_isError) \ + M(ZSTD_getErrorName) \ + M(ZSTD_freeDStream) + +DL_SYMBOL_TABLE(DECLARE_SYM) + +static int dlopen_note(void) +{ + static void *dl; + + ELF_NOTE_DLOPEN("zstd", + "Support for uncompressing zstd-compressed files", + ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED, + "libzstd.so.1"); + + return dlsym_many(&dl, "libzstd.so.1", DL_SYMBOL_TABLE(DLSYM_ARG) NULL); +} + +FILE *kbdfile_decompressor_zstd(struct kbdfile *file) +{ + char errbuf[200]; + int retcode; + FILE *outf = NULL; + int infd = -1; + int memfd = -1; + + ZSTD_DStream *dstream = NULL; + + retcode = dlopen_note(); + if (retcode < 0) { + ERR(file->ctx, "zstd: can't load and resolve symbols: %s", + kbd_strerror(-retcode, errbuf, sizeof(errbuf))); + return NULL; + } + + retcode = -1; + + infd = open(file->pathname, O_RDONLY); + if (infd < 0) { + ERR(file->ctx, "unable to open xz-archive file: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + memfd = memfd_create(file->pathname, MFD_CLOEXEC); + if (memfd < 0) { + ERR(file->ctx, "unable to open in-memory file: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } + + dstream = sym_ZSTD_createDStream(); + if (!dstream) { + ERR(file->ctx, "prepare zstd streaming decompressor failed"); + goto cleanup; + } + + char inpbuf[BUFSIZ]; + char outbuf[BUFSIZ * 4]; + int eof = 0; + + ZSTD_inBuffer input = { inpbuf, 0, 0 }; + ZSTD_outBuffer output = { outbuf, sizeof(outbuf), 0 }; + + while (!eof || input.pos < input.size) { + if (!eof && input.pos == input.size) { + ssize_t r = read(infd, inpbuf, sizeof(inpbuf)); + + if (r < 0) { + ERR(file->ctx, "unable to read zstd-archive: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + goto cleanup; + } else if (r == 0) { + eof = 1; + input.src = inpbuf; + input.size = 0; + input.pos = 0; + } else { + input.src = inpbuf; + input.size = (size_t) r; + input.pos = 0; + } + } + + output.dst = outbuf; + output.size = sizeof(outbuf); + output.pos = 0; + + size_t res_decompress = sym_ZSTD_decompressStream(dstream, &output, &input); + + if (sym_ZSTD_isError(res_decompress)) { + ERR(file->ctx, "zstd: unable to decompress stream: %s", + sym_ZSTD_getErrorName(res_decompress)); + goto cleanup; + } + + size_t to_write = output.pos; + size_t written = 0; + + while (written < to_write) { + ssize_t w = write(memfd, outbuf + written, to_write - written); + + if (w < 0) { + if (errno == EINTR) + continue; + + ERR(file->ctx, "unable to write data: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + goto cleanup; + } + written += (size_t) w; + } + } + + if (eof && input.pos == input.size) + retcode = 0; + +cleanup: + if (infd >= 0) + close(infd); + + if (dstream) + sym_ZSTD_freeDStream(dstream); + + if (retcode == 0) { + lseek(memfd, 0, SEEK_SET); + + outf = fdopen(memfd, "r"); + if (!outf) { + ERR(file->ctx, "unable to create file stream from file descriptor: %s", + kbd_strerror(errno, errbuf, sizeof(errbuf))); + + if (memfd >= 0) + close(memfd); + } + } else if (memfd >= 0) { + close(memfd); + } + + return outf; +} diff --git a/src/libkbdfile/kbdfile.c b/src/libkbdfile/kbdfile.c index 9a0ce05..5b1ee6d 100644 --- a/src/libkbdfile/kbdfile.c +++ b/src/libkbdfile/kbdfile.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -16,14 +17,17 @@ #include "contextP.h" static struct decompressor { + unsigned char magic[2]; + FILE *(*decompressor)(struct kbdfile *fp); const char *ext; /* starts with `.', has no other dots */ const char *cmd; } decompressors[] = { - { ".gz", "gzip -d -c" }, - { ".bz2", "bzip2 -d -c" }, - { ".xz", "xz -d -c" }, - { ".zst", "zstd -d -q -c" }, - { NULL, NULL } + { { 0x1f, 0x8b }, kbdfile_decompressor_zlib, ".gz", "gzip -d -c" }, + { { 0x1f, 0x9e }, kbdfile_decompressor_zlib, ".gz", "gzip -d -c" }, + { { 0x42, 0x5a }, kbdfile_decompressor_bzip2, ".bz2", "bzip2 -d -c" }, + { { 0xfd, 0x37 }, kbdfile_decompressor_lzma, ".xz", "xz -d -c" }, + { { 0x28, 0xb5 }, kbdfile_decompressor_zstd, ".zst", "zstd -d -q -c" }, + { { 0, 0 }, NULL, NULL, NULL } }; struct kbdfile * @@ -74,6 +78,29 @@ kbdfile_set_pathname(struct kbdfile *fp, const char *pathname) return 0; } +static int KBD_ATTR_PRINTF(2, 3) +kbdfile_pathname_sprintf(struct kbdfile *fp, const char *fmt, ...) +{ + ssize_t size; + va_list ap; + + if (fp == NULL || fmt == NULL) + return -1; + + va_start(ap, fmt); + size = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + if (size < 0 || (size_t) size >= sizeof(fp->pathname)) + return -1; + + va_start(ap, fmt); + vsnprintf(fp->pathname, sizeof(fp->pathname), fmt, ap); + va_end(ap); + + return 0; +} + FILE * kbdfile_get_file(struct kbdfile *fp) { @@ -102,7 +129,7 @@ kbdfile_close(struct kbdfile *fp) fp->pathname[0] = '\0'; } -static char * +char * kbd_strerror(int errnum, char *buf, size_t buflen) { *buf = '\0'; @@ -131,7 +158,7 @@ pipe_open(const struct decompressor *dc, struct kbdfile *fp) sprintf(pipe_cmd, "%s %s", dc->cmd, fp->pathname); fp->fd = popen(pipe_cmd, "r"); - fp->flags |= KBDFILE_PIPE; + fp->flags |= KBDFILE_PIPE | KBDFILE_COMPRESSED; if (!(fp->fd)) { char buf[200]; @@ -144,78 +171,130 @@ pipe_open(const struct decompressor *dc, struct kbdfile *fp) return 0; } -/* If a file PATHNAME exists, then open it. - If is has a `compressed' extension, then open a pipe reading it */ static int -maybe_pipe_open(struct kbdfile *fp) +open_pathname(struct kbdfile *fp) { - char *t; + FILE *f; + char buf[200]; + unsigned char magic[2]; struct stat st; - struct decompressor *dc; - if (stat(fp->pathname, &st) == -1 || !S_ISREG(st.st_mode) || access(fp->pathname, R_OK) == -1) + if (!fp || stat(fp->pathname, &st) < 0 || !S_ISREG(st.st_mode)) return -1; - t = strrchr(fp->pathname, '.'); - if (t) { - for (dc = &decompressors[0]; dc->cmd; dc++) { - if (strcmp(t, dc->ext) == 0) - return pipe_open(dc, fp); - } - } + errno = 0; + f = fopen(fp->pathname, "r"); - fp->flags &= ~KBDFILE_PIPE; - - if ((fp->fd = fopen(fp->pathname, "r")) == NULL) { - char buf[200]; + if (!f) { ERR(fp->ctx, "fopen: %s: %s", fp->pathname, kbd_strerror(errno, buf, sizeof(buf))); return -1; } + fp->flags &= ~KBDFILE_PIPE; + fp->flags &= ~KBDFILE_COMPRESSED; + + if ((size_t) st.st_size > sizeof(magic)) { + struct decompressor *dc; + + errno = 0; + + if (fread(magic, sizeof(magic), 1, f) != 1) { + ERR(fp->ctx, "fread: %s: %s", fp->pathname, kbd_strerror(errno, buf, sizeof(buf))); + fclose(f); + return -1; + } + + /* + * We ignore the suffix and use archive magics to avoid problems + * with incorrect file naming. + */ + for (dc = &decompressors[0]; dc->cmd; dc++) { + if (memcmp(magic, dc->magic, sizeof(magic)) != 0) + continue; + fclose(f); + + if (dc->decompressor && (f = dc->decompressor(fp)) != NULL) { + fp->flags |= KBDFILE_COMPRESSED; + goto uncompressed; + } + + if (getenv("KBDFILE_IGNORE_DECOMP_UTILS") != NULL) + return -1; + + return pipe_open(dc, fp); + } + + rewind(f); + } + +uncompressed: + fp->fd = f; + return 0; } +/* + * If a file PATHNAME exists, then open it. + * If is has a `compressed' extension, then open a pipe reading it. + */ +static int +maybe_pipe_open(struct kbdfile *fp) +{ + size_t len; + struct decompressor *dc; + + if (!fp) + return -1; + + if (!open_pathname(fp)) + return 0; + + len = strlen(fp->pathname); + + /* + * We no longer rely on suffixes to select a decompressor, but we still + * need to check the suffix for backward compatibility. + */ + for (dc = &decompressors[0]; dc->cmd; dc++) { + if (len + strlen(dc->ext) >= sizeof(fp->pathname)) + continue; + + fp->pathname[len] = '\0'; + strcat(fp->pathname, dc->ext); + + if (!open_pathname(fp)) + return 0; + } + + fp->pathname[len] = '\0'; + + return -1; +} + static int findfile_by_fullname(const char *fnam, const char *const *suffixes, struct kbdfile *fp) { int i; - struct stat st; - struct decompressor *dc; - size_t fnam_len, sp_len; fp->flags &= ~KBDFILE_PIPE; - fnam_len = strlen(fnam); + fp->flags &= ~KBDFILE_COMPRESSED; for (i = 0; suffixes[i]; i++) { if (suffixes[i] == NULL) continue; /* we tried it already */ - sp_len = strlen(suffixes[i]); - - if (fnam_len + sp_len + 1 > sizeof(fp->pathname)) + if (kbdfile_pathname_sprintf(fp, "%s%s", fnam, suffixes[i]) < 0) continue; - snprintf(fp->pathname, sizeof(fp->pathname), "%s%s", fnam, suffixes[i]); - - if (stat(fp->pathname, &st) == 0 && S_ISREG(st.st_mode) && (fp->fd = fopen(fp->pathname, "r")) != NULL) + if (!maybe_pipe_open(fp)) return 0; - - for (dc = &decompressors[0]; dc->cmd; dc++) { - if (fnam_len + sp_len + strlen(dc->ext) + 1 > sizeof(fp->pathname)) - continue; - - snprintf(fp->pathname, sizeof(fp->pathname), "%s%s%s", fnam, suffixes[i], dc->ext); - - if (stat(fp->pathname, &st) == 0 && S_ISREG(st.st_mode) && access(fp->pathname, R_OK) == 0) - return pipe_open(dc, fp); - } } return -1; } static int -filecmp(const char *fname, const char *name, const char *const *suf, unsigned int *index, struct decompressor **d) +filecmp(const char *fname, const char *name, const char *const *suf, unsigned int *index) { /* Does d_name start right? */ const char *p = name; @@ -232,7 +311,6 @@ filecmp(const char *fname, const char *name, const char *const *suf, unsigned in if (!strcmp(p, suf[i])) { if (i < *index) { *index = i; - *d = NULL; } return 0; } @@ -246,7 +324,6 @@ filecmp(const char *fname, const char *name, const char *const *suf, unsigned in if (!strcmp(p + l, dc->ext)) { if (i < *index) { *index = i; - *d = dc; } return 0; } @@ -266,6 +343,7 @@ findfile_in_dir(const char *fnam, const char *dir, const int recdepth, const cha fp->fd = NULL; fp->flags &= ~KBDFILE_PIPE; + fp->flags &= ~KBDFILE_COMPRESSED; dir_len = strlen(dir); @@ -291,7 +369,6 @@ findfile_in_dir(const char *fnam, const char *dir, const int recdepth, const cha goto EndScan; } - struct decompressor *dc = NULL; unsigned int index = UINT_MAX; // Scan the directory twice: first for files, then @@ -343,35 +420,35 @@ StartScan: if (secondpass || ff) continue; - snprintf(fp->pathname, sizeof(fp->pathname), "%s/%s", dir, namelist[n]->d_name); + if (kbdfile_pathname_sprintf(fp, "%s/%s", dir, namelist[n]->d_name) < 0) + continue; if (stat(fp->pathname, &st) || !S_ISREG(st.st_mode)) continue; - if (!filecmp(fnam, namelist[n]->d_name, suf, &index, &dc)) { + if (!filecmp(fnam, namelist[n]->d_name, suf, &index)) { + /* + * We cannot immediately try to open the file because + * the suffixes are specified in order of priority. We + * need to find the lowest index. + */ rc = 0; } } - if (!secondpass && index != UINT_MAX) { - snprintf(fp->pathname, sizeof(fp->pathname), "%s/%s%s%s", dir, fnam, suf[index], (dc ? dc->ext : "")); - - if (!dc) { - rc = maybe_pipe_open(fp); + if (!secondpass) { + if (index != UINT_MAX) { + rc = rc ?: kbdfile_pathname_sprintf(fp, "%s/%s%s", dir, fnam, suf[index]); + rc = rc ?: maybe_pipe_open(fp); goto EndScan; } - if (pipe_open(dc, fp) < 0) { - rc = -1; - goto EndScan; + if (recdepth > 0) { + secondpass = 1; + goto StartScan; } } - if (recdepth > 0 && !secondpass) { - secondpass = 1; - goto StartScan; - } - EndScan: if (namelist != NULL) { for (int n = 0; n < dirents; n++) @@ -396,9 +473,10 @@ kbdfile_find(const char *fnam, const char *const *dirpath, const char *const *su } fp->flags &= ~KBDFILE_PIPE; + fp->flags &= ~KBDFILE_COMPRESSED; /* Try explicitly given name first */ - strncpy(fp->pathname, fnam, sizeof(fp->pathname) - 1); + kbdfile_set_pathname(fp, fnam); if (!maybe_pipe_open(fp)) return 0; @@ -466,5 +544,5 @@ kbdfile_open(struct kbdfile_ctx *ctx, const char *filename) int kbdfile_is_compressed(struct kbdfile *fp) { - return (fp->flags & KBDFILE_PIPE); + return (fp->flags & KBDFILE_COMPRESSED); } diff --git a/tests/ci/qemu-install-dependencies.sh b/tests/ci/qemu-install-dependencies.sh index 50c36bf..672d6c5 100755 --- a/tests/ci/qemu-install-dependencies.sh +++ b/tests/ci/qemu-install-dependencies.sh @@ -40,4 +40,5 @@ message "system info" apt_get_install \ autoconf automake autopoint libtool libtool-bin pkg-config \ make bison flex gettext kbd strace valgrind libpam0g-dev \ + libz-dev libbz2-dev liblzma-dev libzstd-dev \ gcc diff --git a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map index e69de29..19f0805 100644 --- a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map +++ b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map @@ -0,0 +1 @@ +qwerty diff --git a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.bz2 b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.bz2 index b56f3b9..799ee04 100644 Binary files a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.bz2 and b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.bz2 differ diff --git a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.gz b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.gz index 6525000..f383383 100644 Binary files a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.gz and b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.gz differ diff --git a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.xz b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.xz new file mode 100644 index 0000000..7c6a79e Binary files /dev/null and b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.xz differ diff --git a/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.zst b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.zst new file mode 100644 index 0000000..05ed7cd Binary files /dev/null and b/tests/data/findfile/test_0/keymaps/i386/qwerty/test3.map.zst differ diff --git a/tests/libkbdfile.at b/tests/libkbdfile.at index 810a752..1c6ec77 100644 --- a/tests/libkbdfile.at +++ b/tests/libkbdfile.at @@ -62,7 +62,7 @@ AT_CLEANUP AT_SETUP([test 13]) AT_KEYWORDS([libkbdfile unittest]) -AT_SKIP_IF([ test "$(arch)" != "ppc64el" ]) +AT_SKIP_IF([ test "$(arch)" = "ppc64el" ]) UNITTEST_MEMCHECK([$abs_builddir/libkbdfile/libkbdfile-test13]) AT_CLEANUP @@ -70,3 +70,8 @@ AT_SETUP([test 14]) AT_KEYWORDS([libkbdfile unittest]) UNITTEST_MEMCHECK([$abs_builddir/libkbdfile/libkbdfile-test14]) AT_CLEANUP + +AT_SETUP([test 15]) +AT_KEYWORDS([libkbdfile unittest]) +UNITTEST_MEMCHECK([$abs_builddir/libkbdfile/libkbdfile-test15]) +AT_CLEANUP diff --git a/tests/libkbdfile/Makefile.am b/tests/libkbdfile/Makefile.am index 684a886..544af43 100644 --- a/tests/libkbdfile/Makefile.am +++ b/tests/libkbdfile/Makefile.am @@ -28,4 +28,5 @@ noinst_PROGRAMS = \ libkbdfile-test12 \ libkbdfile-test13 \ libkbdfile-test14 \ + libkbdfile-test15 \ $(NULL) diff --git a/tests/libkbdfile/libkbdfile-test03.c b/tests/libkbdfile/libkbdfile-test03.c index 1eaffbe..382cd3f 100644 --- a/tests/libkbdfile/libkbdfile-test03.c +++ b/tests/libkbdfile/libkbdfile-test03.c @@ -17,14 +17,20 @@ main(int argc KBD_ATTR_UNUSED, char **argv KBD_ATTR_UNUSED) const char *const suffixes[] = { "", ".kmap", ".map", NULL }; const char *expect = TESTDIR "/data/findfile/test_0/keymaps/i386/qwertz/test2"; + const char *const searches[] = { "test2", "qwertz/test2", "i386/qwertz/test2", NULL }; - int rc = kbdfile_find("test2", dirpath, suffixes, fp); + for (int i = 0; searches[i]; i++) { + int rc; - if (rc != 0) - kbd_error(EXIT_FAILURE, 0, "unable to find file"); + rc = kbdfile_find(searches[i], dirpath, suffixes, fp); + if (rc != 0) + kbd_error(EXIT_FAILURE, 0, "unable to find file: %s", searches[i]); - if (strcmp(expect, kbdfile_get_pathname(fp)) != 0) - kbd_error(EXIT_FAILURE, 0, "unexpected file: %s (expected %s)", kbdfile_get_pathname(fp), expect); + if (strcmp(expect, kbdfile_get_pathname(fp)) != 0) + kbd_error(EXIT_FAILURE, 0, "unexpected file: %s (expected %s)", kbdfile_get_pathname(fp), expect); + + kbdfile_close(fp); + } kbdfile_free(fp); diff --git a/tests/libkbdfile/libkbdfile-test13.c b/tests/libkbdfile/libkbdfile-test13.c index be0a9f3..ef42da0 100644 --- a/tests/libkbdfile/libkbdfile-test13.c +++ b/tests/libkbdfile/libkbdfile-test13.c @@ -20,7 +20,7 @@ main(int argc KBD_ATTR_UNUSED, char **argv KBD_ATTR_UNUSED) int rc = 0; - rc = kbdfile_find("simple-1.psf.gz", dirpath, suffixes, fp); + rc = kbdfile_find("simple-1.psf", dirpath, suffixes, fp); if (rc != 0) kbd_error(EXIT_FAILURE, 0, "unable to find file"); diff --git a/tests/libkbdfile/libkbdfile-test15.c b/tests/libkbdfile/libkbdfile-test15.c new file mode 100644 index 0000000..90f4174 --- /dev/null +++ b/tests/libkbdfile/libkbdfile-test15.c @@ -0,0 +1,86 @@ +#include "config.h" + +#include +#include +#include +#include + +#include +#include "libcommon.h" + +int +main(int argc KBD_ATTR_UNUSED, char **argv KBD_ATTR_UNUSED) +{ + struct kbdfile *fp = kbdfile_new(NULL); + if (!fp) + kbd_error(EXIT_FAILURE, 0, "unable to create kbdfile"); + + const char *const dirpath[] = { "", TESTDIR "/data/findfile/test_0/keymaps/**", NULL }; + const char *const suffixes[] = { ".map", NULL }; + + struct testcase { + const char *file; + const char *text; + int compressed; + } cases[] = { +#ifdef HAVE_ZLIB + { TESTDIR "/data/findfile/test_0/keymaps/i386/qwerty/test3.map.gz", "qwerty zlib", 1 }, +#endif +#ifdef HAVE_BZIP2 + { TESTDIR "/data/findfile/test_0/keymaps/i386/qwerty/test3.map.bz2", "qwerty bzip2", 1 }, +#endif +#ifdef HAVE_LZMA + { TESTDIR "/data/findfile/test_0/keymaps/i386/qwerty/test3.map.xz", "qwerty lzma", 1 }, +#endif +#ifdef HAVE_ZSTD + { TESTDIR "/data/findfile/test_0/keymaps/i386/qwerty/test3.map.zst", "qwerty zstd", 1 }, +#endif + { TESTDIR "/data/findfile/test_0/keymaps/i386/qwerty/test3.map", "qwerty", 0 }, + { NULL, NULL, 0 } + }; + struct testcase *ts = &cases[0]; + + setenv("KBDFILE_IGNORE_DECOMP_UTILS", "1", 1); + + for (ts = &cases[0]; ts->file; ts++) { + char buf[256]; + size_t len; + FILE *f; + + //kbd_warning(0, "Check: %s", ts->file); + + int rc = kbdfile_find(ts->file, dirpath, suffixes, fp); + + if (rc != 0) + kbd_error(EXIT_FAILURE, 0, "unable to find file: %s", ts->file); + + if (strcmp(ts->file, kbdfile_get_pathname(fp)) != 0) + kbd_error(EXIT_FAILURE, 0, "unexpected file: %s (expected %s)", + kbdfile_get_pathname(fp), ts->file); + + if (ts->compressed && !kbdfile_is_compressed(fp)) + kbd_error(EXIT_FAILURE, 0, "not compressed: %s", + kbdfile_get_pathname(fp)); + + f = kbdfile_get_file(fp); + if (!f) + kbd_error(EXIT_FAILURE, 0, "unable to get file: %s", ts->file); + + if (fgets(buf, sizeof(buf), f) == NULL) + kbd_error(EXIT_FAILURE, 0, "unable to read file: %s", ts->file); + + len = strlen(buf); + + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + + if (strcmp(buf, ts->text)) + kbd_error(EXIT_FAILURE, 0, "unexpected content of file: %s", ts->file); + + kbdfile_close(fp); + } + + kbdfile_free(fp); + + return EXIT_SUCCESS; +}