Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package apk-tools for openSUSE:Factory checked in at 2026-07-31 15:30:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/apk-tools (Old) and /work/SRC/openSUSE:Factory/.apk-tools.new.2004 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "apk-tools" Fri Jul 31 15:30:39 2026 rev:8 rq:1368481 version:3.0.7 Changes: -------- --- /work/SRC/openSUSE:Factory/apk-tools/apk-tools.changes 2026-04-13 23:22:32.569176698 +0200 +++ /work/SRC/openSUSE:Factory/.apk-tools.new.2004/apk-tools.changes 2026-07-31 16:05:50.709376250 +0200 @@ -1,0 +2,11 @@ +Wed Jul 29 07:43:26 UTC 2026 - Martin Hauke <[email protected]> + +- Update to version 3.0.7: + * extract: harden validation of malicious filenames during + extraction. + * genhelp: add Zsh completion generator. + * repoparser: allow lines starting with variable. + * genhelp: make gzipped embedded help reproducible. + * doc: misc improvements. + +------------------------------------------------------------------- Old: ---- apk-tools-3.0.6.tar.xz New: ---- apk-tools-3.0.7.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ apk-tools.spec ++++++ --- /var/tmp/diff_new_pack.THSS6Y/_old 2026-07-31 16:05:51.161391978 +0200 +++ /var/tmp/diff_new_pack.THSS6Y/_new 2026-07-31 16:05:51.165392117 +0200 @@ -21,7 +21,7 @@ %define soname 3_0_0 %define libname libapk%{soname} Name: apk-tools -Version: 3.0.6 +Version: 3.0.7 Release: 0 Summary: Alpine package manager License: GPL-2.0-or-later @@ -98,6 +98,7 @@ %{_mandir}/man8/apk.8%{?ext_man} %{_mandir}/man[5,8]/apk-*.[5,8]%{?ext_man} %{_datadir}/bash-completion/completions/_apk +%{_datadir}/zsh/site-functions/_apk_zsh %{python_sitearch}/apk.cpython-*.so %files -n %{libname} ++++++ _service ++++++ --- /var/tmp/diff_new_pack.THSS6Y/_old 2026-07-31 16:05:51.201393370 +0200 +++ /var/tmp/diff_new_pack.THSS6Y/_new 2026-07-31 16:05:51.205393509 +0200 @@ -2,7 +2,7 @@ <service name="tar_scm" mode="manual"> <param name="url">https://git.alpinelinux.org/apk-tools</param> <param name="scm">git</param> - <param name="revision">v3.0.6</param> + <param name="revision">v3.0.7</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="versionformat">@PARENT_TAG@</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.THSS6Y/_old 2026-07-31 16:05:51.225394205 +0200 +++ /var/tmp/diff_new_pack.THSS6Y/_new 2026-07-31 16:05:51.233394483 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://git.alpinelinux.org/apk-tools</param> - <param name="changesrevision">7e92634afe99db29e00037e6a134104ad9720b68</param></service></servicedata> + <param name="changesrevision">522171e05c72c8a0dd2a9392f053c89196fbdb9b</param></service></servicedata> (No newline at EOF) ++++++ apk-tools-3.0.6.tar.xz -> apk-tools-3.0.7.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/VERSION new/apk-tools-3.0.7/VERSION --- old/apk-tools-3.0.6/VERSION 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/VERSION 2026-07-28 13:28:06.000000000 +0200 @@ -1 +1 @@ -3.0.6 +3.0.7 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/doc/apk-protected_paths.5.scd new/apk-tools-3.0.7/doc/apk-protected_paths.5.scd --- old/apk-tools-3.0.6/doc/apk-protected_paths.5.scd 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/doc/apk-protected_paths.5.scd 2026-07-28 13:28:06.000000000 +0200 @@ -47,13 +47,25 @@ Lines starting with *#* and empty lines are ignored. -# EXAMPLES +# DEFAULTS + +Even if _/etc/apk/protected_paths.d/\*.list_ is empty, the following is used by +default: ``` -# This line is ignored; it is a comment. +etc/ @etc/init.d !etc/apk +``` + +The contents of _/etc/apk/protected_paths.d/\*.list_ are applied after the +above defaults. When a flag overrides the _protected_paths.d_ directory, it also +overrides these defaults. + +# MORE EXAMPLES + +``` +# This line is ignored; it is a comment. \-etc/ssl/certs/ca-cert-\*.pem ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/doc/apk-query.8.scd new/apk-tools-3.0.7/doc/apk-query.8.scd --- old/apk-tools-3.0.6/doc/apk-query.8.scd 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/doc/apk-query.8.scd 2026-07-28 13:28:06.000000000 +0200 @@ -134,4 +134,4 @@ apk query --match name,provides cmd:apk --fields origin \# print source packages with specific dependency name++ -apk query --match dependency so:libapk.so.2.14.9 --fields origin +apk query --match depends so:libapk.so.2.14.9 --fields origin diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/doc/apk-repositories.5.scd new/apk-tools-3.0.7/doc/apk-repositories.5.scd --- old/apk-tools-3.0.6/doc/apk-repositories.5.scd 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/doc/apk-repositories.5.scd 2026-07-28 13:28:06.000000000 +0200 @@ -40,10 +40,9 @@ *ndx \[@tag\] url*++ *\[v2|v3\] \[@tag\] url \[component...\]* - If a repository declaration omits the type field, it defaults to the type - defined in latest *default* command, or if absent, to *v2* (or *ndx* if *url* - ends with *.adb* or *.tar.gz*). If the format is *v3* or the *ndx* filename - ends with *.adb* a v3 format index file required. + If a repository declaration omits the type field, it defaults to *v2* + (or *ndx* if *url* ends with *.adb* or *.tar.gz*). If the format is *v3* + or the *ndx* filename ends with *.adb* a v3 format index file required. The optional *@tag* can be present to indicate that the repository should not be used by default. Refer to *apk-world*(5) on how to enable installation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/doc/apk.8.scd new/apk-tools-3.0.7/doc/apk.8.scd --- old/apk-tools-3.0.6/doc/apk.8.scd 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/doc/apk.8.scd 2026-07-28 13:28:06.000000000 +0200 @@ -337,7 +337,7 @@ The following options are available for all commands which generate APKv3 files. -*--compression, -C* _ALGORITHM[:LEVEL]_ +*--compression*, *-c* _ALGORITHM[:LEVEL]_ Compress the file with given _ALGORITHM_ and _LEVEL_. Supported algorithms: - none - deflate (level 1-9) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/doc/meson.build new/apk-tools-3.0.7/doc/meson.build --- old/apk-tools-3.0.6/doc/meson.build 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/doc/meson.build 2026-07-28 13:28:06.000000000 +0200 @@ -1,5 +1,6 @@ man_filenames = [ 'apk.8.scd', + 'apk-cache.5.scd', 'apk-keys.5.scd', 'apk-package.5.scd', 'apk-protected_paths.5.scd', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/apk_fs.h new/apk-tools-3.0.7/src/apk_fs.h --- old/apk-tools-3.0.6/src/apk_fs.h 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/apk_fs.h 2026-07-28 13:28:06.000000000 +0200 @@ -51,6 +51,9 @@ #define APK_FSEXTRACTF_NO_SYS_XATTRS 0x0004 #define APK_FSEXTRACTF_NO_DEVICES 0x0008 +bool apk_fs_is_malicious_filename(apk_blob_t file); +bool apk_fs_is_malicious_pathname(apk_blob_t path); + int apk_fs_extract(struct apk_ctx *, const struct apk_file_info *, struct apk_istream *, unsigned int, apk_blob_t); void apk_fsdir_get(struct apk_fsdir *, apk_blob_t dir, unsigned int extract_flags, struct apk_ctx *ac, apk_blob_t pkgctx); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/app_mkpkg.c new/apk-tools-3.0.7/src/app_mkpkg.c --- old/apk-tools-3.0.6/src/app_mkpkg.c 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/app_mkpkg.c 2026-07-28 13:28:06.000000000 +0200 @@ -24,6 +24,7 @@ #include "apk_balloc.h" #include "apk_print.h" #include "apk_xattr.h" +#include "apk_fs.h" struct mkpkg_hardlink_key { dev_t device; @@ -224,6 +225,8 @@ if (r) return r; if (!S_ISDIR(fi.mode)) return 0; + if (apk_fs_is_malicious_filename(APK_BLOB_STR(entry))) return -APKE_ADB_SCHEMA; + int n = apk_pathbuilder_push(&ctx->pb, entry); apk_string_array_add(&ctx->pathnames, apk_balloc_cstr(&ctx->ba, apk_pathbuilder_get(&ctx->pb))); r = apk_dir_foreach_file_sorted(dirfd, entry, mkpkg_scan_dirent, ctx, NULL); @@ -270,6 +273,8 @@ } ft; int r, n; + if (apk_fs_is_malicious_filename(APK_BLOB_STR(entry))) return -APKE_ADB_SCHEMA; + ctx->num_dirents++; r = apk_fileinfo_get(dirfd, entry, APK_FI_NOFOLLOW | APK_FI_DIGEST(APK_DIGEST_SHA256), &fi, NULL); if (r) return r; @@ -382,7 +387,7 @@ adb_wo_obj(&fio, ADBI_DI_FILES, &ctx->files); adb_wa_append_obj(&ctx->paths, &fio); done: - if (r) apk_err(out, "failed to process directory '%s': %d", apk_pathbuilder_cstr(&ctx->pb), r); + if (r) apk_err(out, "failed to process directory '%s': %s", entry, apk_error_str(r)); return r; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/database.c new/apk-tools-3.0.7/src/database.c --- old/apk-tools-3.0.6/src/database.c 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/database.c 2026-07-28 13:28:06.000000000 +0200 @@ -2727,14 +2727,6 @@ return 0; } -static int contains_control_character(const char *str) -{ - for (const uint8_t *p = (const uint8_t *) str; *p; p++) { - if (*p < 0x20 || *p == 0x7f) return 1; - } - return 0; -} - static int apk_db_install_v2meta(struct apk_extract_ctx *ectx, struct apk_istream *is) { struct install_ctx *ctx = container_of(ectx, struct install_ctx, ectx); @@ -2804,7 +2796,6 @@ static int apk_db_install_file(struct apk_extract_ctx *ectx, const struct apk_file_info *ae, struct apk_istream *is) { struct install_ctx *ctx = container_of(ectx, struct install_ctx, ectx); - static const char dot1[] = "/./", dot2[] = "/../"; struct apk_database *db = ctx->db; struct apk_ctx *ac = db->ctx; struct apk_out *out = &ac->out; @@ -2817,17 +2808,6 @@ apk_db_run_pending_script(ctx); - /* Sanity check the file name */ - if (ae->name[0] == '/' || contains_control_character(ae->name) || - strncmp(ae->name, &dot1[1], 2) == 0 || - strncmp(ae->name, &dot2[1], 3) == 0 || - strstr(ae->name, dot1) || strstr(ae->name, dot2)) { - apk_warn(out, PKG_VER_FMT": ignoring malicious file %s", - PKG_VER_PRINTF(pkg), ae->name); - ipkg->broken_files = 1; - return 0; - } - /* Installable entry */ if (!S_ISDIR(ae->mode)) { if (!apk_blob_rsplit(name, '/', &bdir, &bfile)) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/extract_v3.c new/apk-tools-3.0.7/src/extract_v3.c --- old/apk-tools-3.0.6/src/extract_v3.c 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/extract_v3.c 2026-07-28 13:28:06.000000000 +0200 @@ -13,6 +13,7 @@ #include "apk_extract.h" #include "apk_adb.h" #include "apk_pathbuilder.h" +#include "apk_fs.h" struct apk_extract_v3_ctx { struct apk_extract_ctx *ectx; @@ -137,6 +138,26 @@ return r; } +static int apk_extract_v3_validate_filetree(struct adb_obj *pkg) +{ + struct adb_obj paths, path, files, file; + + adb_ro_obj(pkg, ADBI_PKG_PATHS, &paths); + + for (int p = ADBI_FIRST; p <= adb_ra_num(&paths); p++) { + adb_ro_obj(&paths, p, &path); + apk_blob_t pathname = adb_ro_blob(&path, ADBI_DI_NAME); + if (pathname.len && apk_fs_is_malicious_pathname(pathname)) return -APKE_FORMAT_INVALID; + adb_ro_obj(&path, ADBI_DI_FILES, &files); + + for (int f = ADBI_FIRST; f <= adb_ra_num(&files); f++) { + adb_ro_obj(&files, f, &file); + if (apk_fs_is_malicious_filename(adb_ro_blob(&file, ADBI_FI_NAME))) return -APKE_FORMAT_INVALID; + } + } + return 0; +} + static int apk_extract_v3_next_file(struct apk_extract_ctx *ectx) { struct apk_extract_v3_ctx *ctx = ectx->pctx; @@ -149,6 +170,9 @@ ctx->cur_file = ADBI_FIRST; adb_r_rootobj(&ctx->db, &ctx->pkg, &schema_package); + r = apk_extract_v3_validate_filetree(&ctx->pkg); + if (r < 0) return r; + r = ectx->ops->v3meta(ectx, &ctx->pkg); if (r < 0) return r; @@ -175,9 +199,7 @@ for (; ctx->cur_file <= adb_ra_num(&ctx->files); ctx->cur_file++) { adb_ro_obj(&ctx->files, ctx->cur_file, &ctx->file); - n = apk_pathbuilder_pushb(&ctx->pb, adb_ro_blob(&ctx->file, ADBI_FI_NAME)); - target = adb_ro_blob(&ctx->file, ADBI_FI_TARGET); if (adb_ro_int(&ctx->file, ADBI_FI_SIZE) != 0 && APK_BLOB_IS_NULL(target)) return 0; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/fs_fsys.c new/apk-tools-3.0.7/src/fs_fsys.c --- old/apk-tools-3.0.6/src/fs_fsys.c 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/fs_fsys.c 2026-07-28 13:28:06.000000000 +0200 @@ -266,6 +266,28 @@ return &fsdir_ops_fsys; } +bool apk_fs_is_malicious_filename(apk_blob_t file) +{ + const uint8_t *ptr = (const uint8_t *) file.ptr; + for (int i = 0; i < file.len; i++) { + if (ptr[i] < 0x20 || ptr[i] == '/' || ptr[i] == 0x7f) return true; + } + switch (file.len) { + case 2: if (ptr[1] != '.') break; // fallthrough + case 1: if (ptr[0] != '.') break; // fallthrough + case 0: return true; + } + return false; +} + +bool apk_fs_is_malicious_pathname(apk_blob_t path) +{ + apk_blob_t dir; + while (apk_blob_split(path, APK_BLOB_STRLIT("/"), &dir, &path)) + if (apk_fs_is_malicious_filename(dir)) return true; + return path.len > 0 && apk_fs_is_malicious_filename(path); +} + static bool need_checksum(const struct apk_file_info *fi) { switch (fi->mode & S_IFMT) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/genhelp.lua new/apk-tools-3.0.7/src/genhelp.lua --- old/apk-tools-3.0.6/src/genhelp.lua 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/genhelp.lua 2026-07-28 13:28:06.000000000 +0200 @@ -21,6 +21,33 @@ - From OPTIONS take each option and it's first sentence (within the first line) --]] +-- Take the first sentence of a (possibly multi-line) description. +-- stop at ". " or a trailing ".". +function first_sentence(text) + local i = text:find("%.%s") + if not i then i = text:find("%.$") end + if i then text = text:sub(1, i - 1) end + return text +end + +-- Name completion rule per applet +applet_argument_completion = { + add="available", + del="installed", + dot="available", + fetch="available", + fix="installed", + index="available", + info="available", + list="available", + manifest="available", + mkndx="available", + policy="available", + query="available", + search="available", + upgrade="installed", +} + local scdoc = { usage_prefix = "Usage: ", } @@ -168,7 +195,7 @@ f:write(data) f:close() - local p = io.popen(('gzip -%d < %s'):format(level, tmp), 'r') + local p = io.popen(('gzip -n%d < %s'):format(level, tmp), 'r') if p ~= nil then ret = p:read("*all") p:close() @@ -187,8 +214,8 @@ for _, fn in ipairs(arg) do if fn == '--debug' then self.debug = true - elseif fn == '--format=bash' then - self.format = "bash" + elseif fn:match("^%-%-format=") then + self.format = fn:match("^%-%-format=(.*)$") else doc = new_scdoc() doc:parse(fn) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/genhelp_apk.lua new/apk-tools-3.0.7/src/genhelp_apk.lua --- old/apk-tools-3.0.6/src/genhelp_apk.lua 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/genhelp_apk.lua 2026-07-28 13:28:06.000000000 +0200 @@ -48,10 +48,7 @@ table.insert(out, fmt:format(k, v)) end - v = table.concat(v, " ") - local i = v:find("%.%s") - if not i then i = v:find("%.$") end - if i then v = v:sub(1, i-1) end + v = first_sentence(table.concat(v, " ")) v = textwrap(v, width - nindent - 1) table.insert(out, v[1]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/genhelp_bash.lua new/apk-tools-3.0.7/src/genhelp_bash.lua --- old/apk-tools-3.0.6/src/genhelp_bash.lua 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/genhelp_bash.lua 2026-07-28 13:28:06.000000000 +0200 @@ -73,25 +73,9 @@ return opts end -local wildcard_installed = 'COMPREPLY=($(compgen -W "$(__apk_installed_pkgs "${cur}")" -- ${cur}))' -local wildcard_available = 'COMPREPLY=($(compgen -W "$(__apk_available_pkgs "${cur}")" -- ${cur}))' - --- Applets needing package name completion -local applet_wildcard = { - add=wildcard_available, - del=wildcard_installed, - dot=wildcard_available, - fetch=wildcard_available, - fix=wildcard_installed, - index=wildcard_available, - info=wildcard_available, - list=wildcard_available, - manifest=wildcard_available, - mkndx=wildcard_available, - policy=wildcard_available, - query=wildcard_available, - search=wildcard_available, - upgrade=wildcard_installed, +local completion_function = { + available='COMPREPLY=($(compgen -W "$(__apk_available_pkgs "${cur}")" -- ${cur}))', + installed='COMPREPLY=($(compgen -W "$(__apk_installed_pkgs "${cur}")" -- ${cur}))', } function M:generate(app, docs) @@ -113,7 +97,7 @@ local t_applet = {} for _, name in ipairs(applets) do local o = table.concat(options[name], " ") - local wildcard = applet_wildcard[name] or "COMPREPLY=()" + local wildcard = completion_function[applet_argument_completion[name]] or "COMPREPLY=()" table.insert(t_applet, template_applet:format(name, o, wildcard)) end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/genhelp_zsh.lua new/apk-tools-3.0.7/src/genhelp_zsh.lua --- old/apk-tools-3.0.6/src/genhelp_zsh.lua 1970-01-01 01:00:00.000000000 +0100 +++ new/apk-tools-3.0.7/src/genhelp_zsh.lua 2026-07-28 13:28:06.000000000 +0200 @@ -0,0 +1,175 @@ +#!/usr/bin/lua5.3 +-- Generate Zsh completion for apk(8) from scdoc manpages + +local template_main = [==[ +#compdef apk + +# apk(8) completion *- shell-script -*- +# Automatically generated by apk-tools (src/genhelp_zsh.lua). Do not modify. + +__apk_installed_pkgs() { + local -a pkgs + pkgs=(${(f)"$(apk query --summarize name --installed "${PREFIX}*" 2>/dev/null)"}) + compadd -a pkgs +} + +__apk_available_pkgs() { + local -a pkgs + pkgs=(${(f)"$(apk query --summarize name --available "${PREFIX}*" 2>/dev/null)"}) + compadd -a pkgs +} + +_apk() { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ +%s '1: :->applet' \ + '*::arg:->args' + + case $state in + applet) + local -a applets + applets=( +%s ) + _describe -t applets 'apk command' applets + ;; + args) + case $line[1] in +%s esac + ;; + esac +} + +_apk "$@" +]==] + +local M = {} + +-- Escape text for use inside a single-quoted zsh _arguments spec, where +-- '[', ']' and ':' are field delimiters and must be escaped to appear +-- literally, and embedded single quotes must close/re-open the string. +local function zsh_escape(s) + s = s:gsub("\\", "\\\\") + s = s:gsub("%[", "\\[") + s = s:gsub("%]", "\\]") + s = s:gsub(":", "\\:") + s = s:gsub("'", "'\\''") + return s +end + +-- Escape text for a 'name:description' entry consumed by _describe. +local function describe_escape(s) + s = s:gsub("\\", "\\\\") + s = s:gsub(":", "\\:") + s = s:gsub("'", "'\\''") + return s +end + +-- Applets name completion functions +local completion_function = { + available="__apk_available_pkgs", + installed="__apk_installed_pkgs", +} + +-- Build one or more zsh _arguments spec strings (each already wrapped in +-- single quotes) for a single option entry { raw_flags, {desc_lines} }. +local function build_opt_specs(opt) + local desc = zsh_escape(first_sentence(table.concat(opt[2], " "))) + local plain, specs = {}, {} + + for flag in opt[1]:gmatch("(%-%-?[^ ,]+)") do + local base, val = flag:match("(.*)%[=(%w+)%]") + if val == "BOOL" then + table.insert(specs, ("'%s[%s]'"):format(base, desc)) + local no_opt = base:gsub("^%-%-", "--no-") + table.insert(specs, ("'%s[%s]'"):format(no_opt, desc)) + elseif val then + -- optional value, e.g. --sync[=AUTO] + table.insert(specs, ("'%s=-[%s]:%s:'"):format(base, desc, val:lower())) + else + table.insert(plain, flag) + end + end + + -- Group plain boolean aliases (-h, --help) into one mutually-exclusive spec + if #plain == 1 then + table.insert(specs, ("'%s[%s]'"):format(plain[1], desc)) + elseif #plain > 1 then + table.insert(specs, ("'(%s)'{%s}'[%s]'"):format( + table.concat(plain, " "), table.concat(plain, ","), desc)) + end + + return specs +end + +local function collect_arg_specs(doc) + local specs = {} + for _, group in ipairs(doc.optgroup or {}) do + for _, opt in ipairs(group) do + for _, s in ipairs(build_opt_specs(opt)) do + table.insert(specs, s) + end + end + end + return specs +end + +function M:generate(app, docs) + local applets, options, goptions = {}, {}, {} + local descriptions, main_doc = {}, nil + + for _, doc in ipairs(docs) do + -- only process section 8 + if doc.mansection == "8" then + if doc.applet == "APK" then + goptions = collect_arg_specs(doc) + main_doc = doc + else + table.insert(applets, doc.applet) + options[doc.applet] = collect_arg_specs(doc) + end + end + end + table.sort(applets) + + if main_doc then + for _, cmd in ipairs(main_doc.commands) do + if type(cmd) == "table" then + descriptions[cmd[1]] = cmd[2] + end + end + end + + local global_block = {} + for _, s in ipairs(goptions) do + table.insert(global_block, "\t\t" .. s .. " \\\n") + end + + local applets_block = {} + for _, name in ipairs(applets) do + local d = describe_escape(descriptions[name] or "") + table.insert(applets_block, ("\t\t\t\t'%s:%s'\n"):format(name, d)) + end + + local cases_block = {} + for _, name in ipairs(applets) do + local opt_lines = {} + for _, s in ipairs(options[name]) do + table.insert(opt_lines, "\t\t\t\t\t\t" .. s .. " \\\n") + end + local wildcard = completion_function[applet_argument_completion[name]] + local tail = wildcard + and ("\t\t\t\t\t\t'*:package:%s'\n"):format(wildcard) + or "\t\t\t\t\t\t'*:arg:_default'\n" + table.insert(cases_block, ("\t\t\t\t%s)\n\t\t\t\t\t_arguments \\\n%s%s\t\t\t\t\t;;\n"):format( + name, table.concat(opt_lines), tail)) + end + + return string.format(template_main, + table.concat(global_block), + table.concat(applets_block), + table.concat(cases_block)) +end + +return M diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/meson.build new/apk-tools-3.0.7/src/meson.build --- old/apk-tools-3.0.6/src/meson.build 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/meson.build 2026-07-28 13:28:06.000000000 +0200 @@ -110,6 +110,18 @@ install: true, install_dir: join_paths(get_option('datadir'), 'bash-completion', 'completions') ) + + generated_zsh_completion = custom_target( + 'apk.zsh-completion', + capture: true, + output: '_apk_zsh', + input: man_files, + command: [lua_bin, genhelp_script, '--format=zsh', '@INPUT@'], + depend_files: 'genhelp_zsh.lua', + env: lua_env, + install: true, + install_dir: join_paths(get_option('datadir'), 'zsh', 'site-functions') + ) else generated_help = custom_target( 'help.h', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/repoparser.c new/apk-tools-3.0.7/src/repoparser.c --- old/apk-tools-3.0.6/src/repoparser.c 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/repoparser.c 2026-07-28 13:28:06.000000000 +0200 @@ -143,6 +143,7 @@ static bool is_keyword(apk_blob_t word) { if (word.ptr[0] == '@') return false; // tag + if (word.ptr[0] == '$') return false; // variable return !is_url(word); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/src/tar.c new/apk-tools-3.0.7/src/tar.c --- old/apk-tools-3.0.6/src/tar.c 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/src/tar.c 2026-07-28 13:28:06.000000000 +0200 @@ -12,6 +12,7 @@ #include "apk_defines.h" #include "apk_tar.h" +#include "apk_fs.h" struct tar_header { /* ustar header, Posix 1003.1 */ @@ -221,13 +222,18 @@ break; } - if (strnlen(entry.name, PATH_MAX) >= PATH_MAX-10 || + size_t entry_len = strnlen(entry.name, PATH_MAX); + if (entry_len >= PATH_MAX-10 || (entry.link_target && strnlen(entry.link_target, PATH_MAX) >= PATH_MAX-10)) { r = -ENAMETOOLONG; goto err; } if (entry.mode & S_IFMT) { + if (apk_fs_is_malicious_pathname(APK_BLOB_PTR_LEN((char*) entry.name, entry_len))) { + r = -APKE_FORMAT_INVALID; + goto err; + } apk_istream_segment(&segment, is, entry.size, entry.mtime); r = parser(ctx, &entry, &segment.is); if (r != 0) goto err; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/test/testlib.sh new/apk-tools-3.0.7/test/testlib.sh --- old/apk-tools-3.0.6/test/testlib.sh 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/test/testlib.sh 2026-07-28 13:28:06.000000000 +0200 @@ -9,6 +9,16 @@ exit 1 } +skip_test() { + exit 77 +} + +require_programs() { + for PROG in "$@"; do + which "$PROG" > /dev/null || skip_test + done +} + glob_one() { # shellcheck disable=SC2048 # argument is wildcard needing expansion for a in $*; do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/test/unit/fs_test.c new/apk-tools-3.0.7/test/unit/fs_test.c --- old/apk-tools-3.0.6/test/unit/fs_test.c 1970-01-01 01:00:00.000000000 +0100 +++ new/apk-tools-3.0.7/test/unit/fs_test.c 2026-07-28 13:28:06.000000000 +0200 @@ -0,0 +1,29 @@ +#include "apk_test.h" +#include "apk_fs.h" + +APK_TEST(fs_malicious_filename) { + assert_false(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("..."))); + assert_false(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("..bar"))); + assert_false(apk_fs_is_malicious_filename(APK_BLOB_STRLIT(".bar"))); + assert_false(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("bar"))); + assert_false(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("båäö"))); + assert_true(apk_fs_is_malicious_filename(APK_BLOB_STRLIT(""))); + assert_true(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("."))); + assert_true(apk_fs_is_malicious_filename(APK_BLOB_STRLIT(".."))); + assert_true(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("foobar/"))); + assert_true(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("foo/bar"))); + assert_true(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("foo""\x00""bar"))); + assert_true(apk_fs_is_malicious_filename(APK_BLOB_STRLIT("foo""\x10""bar"))); +} + +APK_TEST(fs_malicious_pathname) { + assert_false(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("foo/foo"))); + assert_false(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("foo/..foo/foo"))); + assert_false(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("foo/foo/"))); + assert_false(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT(".foo/foo"))); + assert_true(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("../foo"))); + assert_true(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("foo//foo"))); + assert_true(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("foo/../foo"))); + assert_true(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("foo/./foo"))); + assert_true(apk_fs_is_malicious_pathname(APK_BLOB_STRLIT("foo/\x1f/foo"))); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/test/unit/meson.build new/apk-tools-3.0.7/test/unit/meson.build --- old/apk-tools-3.0.6/test/unit/meson.build 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/test/unit/meson.build 2026-07-28 13:28:06.000000000 +0200 @@ -4,6 +4,7 @@ unit_test_src = [ 'blob_test.c', + 'fs_test.c', 'io_test.c', 'package_test.c', 'process_test.c', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/test/unit/repoparser_test.c new/apk-tools-3.0.7/test/unit/repoparser_test.c --- old/apk-tools-3.0.6/test/unit/repoparser_test.c 2026-04-13 10:36:44.000000000 +0200 +++ new/apk-tools-3.0.7/test/unit/repoparser_test.c 2026-07-28 13:28:06.000000000 +0200 @@ -70,18 +70,21 @@ "set var-foo=bad-name\n" "set APK_FOO=reserved\n" "http://${mirror}/ ${comp}\n" + "set mirror=/alpine\n" + "${mirror}/main\n" "v2 foobar main\n", "WARNING: repositories:1: invalid option: -unknown\n" "WARNING: repositories:8: undefined variable: undefined\n" "WARNING: repositories:11: invalid variable definition: var-foo=bad-name\n" "WARNING: repositories:12: invalid variable definition: APK_FOO=reserved\n" - "WARNING: repositories:14: invalid url: foobar\n", + "WARNING: repositories:16: invalid url: foobar\n", "http://alpine.org/main:APKINDEX.tar.gz:\n" "http://example.com/main:APKINDEX.tar.gz:\n" "http://example.com/main:APKINDEX.tar.gz:\n" "http://example.com/alpine/main:APKINDEX.tar.gz:\n" "http://example.com/alpine/community:APKINDEX.tar.gz:\n" "http://example.com/alpine/testing:APKINDEX.tar.gz:\n" + "/alpine/main:APKINDEX.tar.gz:\n" ); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apk-tools-3.0.6/test/user/malicious.sh new/apk-tools-3.0.7/test/user/malicious.sh --- old/apk-tools-3.0.6/test/user/malicious.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/apk-tools-3.0.7/test/user/malicious.sh 2026-07-28 13:28:06.000000000 +0200 @@ -0,0 +1,28 @@ +#!/bin/sh + +TESTDIR=$(realpath "${TESTDIR:-"$(dirname "$0")"/..}") +. "$TESTDIR"/testlib.sh + +require_programs xxd + +setup_apkroot +APK="$APK --allow-untrusted --no-interactive" + +mkdir -p files/a files/dir===dir +echo hello > files/a/file=file + +$APK mkpkg --compression=none -I name:malicious -I version:1.0 -F files -o malicious-1.0.apk + +echo hello > files/a/"file file" +$APK mkpkg --compression=none -I name:malicious -I version:1.0 -F files -o malicious-1.0b.apk && assert "mkpkg filename check failed" + +mkdir dest dest-a dest-b +$APK extract malicious-1.0.apk --destination dest || assert "failed to extract" + +xxd -p -c100000 malicious-1.0.apk | sed -e 's,6469723d3d3d646972,6469722f2e2f646972,' | xxd -rp > malicious-1.0-a.apk +$APK extract malicious-1.0-a.apk --destination dest-a && assert "malicious check 1 failed" + +xxd -p -c100000 malicious-1.0.apk | sed -e 's,66696c653d66696c65,66696c652f66696c65,' | xxd -rp > malicious-1.0-b.apk +$APK extract malicious-1.0-b.apk --destination dest-b && assert "malicious check 2 failed" + +exit 0
