fix: trust archive permissions instead of file(1) heuristics in ziextract (#771)

This commit is contained in:
Andrea Alberti 2026-03-14 23:39:35 +01:00 committed by GitHub
parent f82d91ccac
commit 1334994f20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 65 additions and 9 deletions

View file

@ -2,6 +2,21 @@
@setup {
ZPLUGINS=$ZINIT[PLUGINS_DIR]
function _zunit_assert_not_executable(){
local pathname=$1 filepath
# If filepath is relative, prepend the test directory
if [[ "${pathname:0:1}" != "/" ]]; then
filepath="$testdir/${pathname}"
else
filepath="$pathname"
fi
[[ ! -x "$filepath" ]] && return 0
echo "'$pathname' is executable but should not be"
exit 1
}
function _zunit_assert_not_exists(){
local file_path=$1
if [[ "${file_path:0:1}" != "/" ]]; then # relative path - prepend the test directory
@ -119,5 +134,38 @@
assert "$workdir/test.zip" is_file
rm -rf "$workdir"
}
@test 'ziextract-permissions' {
# Build a zip with known permissions, call ziextract directly, and verify
# that only files with +x in the archive come out executable.
local srcdir workdir
srcdir=$(mktemp -d)
workdir=$(mktemp -d)
# run_me.sh: +x in archive, has shebang → must be executable
printf '#!/bin/sh\necho run\n' > "$srcdir/run_me.sh"
chmod +x "$srcdir/run_me.sh"
# also_run.py: no +x in archive, has shebang → must NOT be executable (shebang alone is not enough)
printf '#!/usr/bin/env python3\nprint("hi")\n' > "$srcdir/also_run.py"
# library.py: no +x, no shebang → must NOT be executable
printf 'def foo(): pass\n' > "$srcdir/library.py"
# pre_marked.sh: +x in archive, no shebang → must be executable (archive bit)
printf 'echo pre\n' > "$srcdir/pre_marked.sh"
chmod +x "$srcdir/pre_marked.sh"
# Build zip preserving permissions, then extract via ziextract
( cd "$srcdir" && zip -q "$workdir/test.zip" run_me.sh also_run.py library.py pre_marked.sh )
(( ${+functions[ziextract]} )) || builtin source "${ZINIT[BIN_DIR]}/zinit-install.zsh"
( builtin cd "$workdir" && ziextract test.zip )
assert "$workdir/run_me.sh" is_executable
assert "$workdir/pre_marked.sh" is_executable
assert "$workdir/also_run.py" not_executable
assert "$workdir/library.py" not_executable
rm -rf "$srcdir" "$workdir"
}
# vim:ft=zsh:sw=2:sts=2:et:foldmarker=\ {,}:foldmethod=marker

View file

@ -1514,9 +1514,9 @@ builtin source "${ZINIT[BIN_DIR]}/zinit-side.zsh" || {
} # ]]]
# FUNCTION: ziextract [[[
# If the file is an archive, it is extracted by this function.
# Next stage is scanning of files with the common utility file
# to detect executables. They are given +x mode. There are also
# messages to the user on performed actions.
# Executable permissions are determined solely by the execute bit already
# stored in the archive — i.e. whatever the package author intended.
# No heuristics (file(1), shebang scanning, etc.) are applied.
#
# $1 - url
# $2 - file
@ -1757,13 +1757,21 @@ ziextract() {
}
unfunction -- .zinit-extract-wrapper
# Glob qualifier legend:
# (DN-.) — D=include dotfiles, N=null glob (no error if empty),
# -=no symlinks, .=regular files → all regular files
# (DN-*.) — same plus *=has execute bit set → files already marked
# executable by the archive extractor
local -aU execs
execs=( **/*~(._zinit(|/*)|.git(|/*)|.svn(|/*)|.hg(|/*)|._backup(|/*))(DN-.) )
if [[ ${#execs} -gt 0 && -n $execs ]] {
execs=( ${(@f)"$( file ${execs[@]} )"} )
execs=( "${(M)execs[@]:#[^(:]##:*executable*}" )
execs=( "${execs[@]/(#b)([^(:]##):*/${match[1]}}" )
}
# Collect files that already have the execute bit set in the archive.
# This is the authoritative signal: if the package author wanted a file
# executable, they set +x when creating the archive. Any file that lacks
# +x in the archive (library, documentation, data file, …) should stay
# non-executable regardless of its content or extension.
# If a poorly maintained package fails to set permissions correctly,
# the user can fix it with the atclone/atpull ices — there is no need
# for zinit to second-guess the archive on every install.
execs=( **/*~(._zinit(|/*)|.git(|/*)|.svn(|/*)|.hg(|/*)|._backup(|/*))(DN-*.) )
builtin print -rl -- ${execs[@]} >! ${TMPDIR:-/tmp}/zinit-execs.$$.lst
if [[ ${#execs} -gt 0 ]] {