mirror of
https://github.com/ohmyzsh/ohmyzsh.git
synced 2026-05-29 04:53:17 +02:00
Some checks failed
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
* fix(dotenv): expect explicit yes before loading .env file * fix(dotenv): implement secure parsing for .env files and add comprehensive tests * feat(dotenv): check for .env file size to prevent DoS * fix(dotenv): forbid setting special variables * fix(dotenv): FIFO shouldn't be read twice * fix(dotenv): unknown vars should expand to empty * fix(dotenv): reject extremely large named pipes * docs(dotenv): update to new parsing system * fix(dotenv): add support for escaped dollars * chore(dotenv): only declare local variables once * fix(dotenv): apply review suggestions * docs(dotenv): update test instructions Co-authored-by: Carlo Sala <carlosalag@protonmail.com>
398 lines
8.9 KiB
Text
398 lines
8.9 KiB
Text
#!/usr/bin/env zunit
|
|
|
|
|
|
@setup {
|
|
typeset -g fixture="$(_create_temp_fixture)"
|
|
typeset -gA expected_vars=()
|
|
}
|
|
|
|
@teardown {
|
|
[[ -f "$fixture" ]] && command rm -f "$fixture"
|
|
unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null
|
|
}
|
|
|
|
@test 'dotenv plugin loads successfully' {
|
|
assert "parse_dotenv" function_exists
|
|
assert "source_env" function_exists
|
|
}
|
|
|
|
@test 'parse returns error for unsupported mode' {
|
|
run _parse_dotenv_quiet "/dev/null" "export"
|
|
assert $state equals 0
|
|
|
|
run _parse_dotenv_quiet "/dev/null" "test"
|
|
assert $state equals 0
|
|
|
|
run _parse_dotenv_quiet "/dev/null" "invalid_mode"
|
|
assert $state equals 1
|
|
}
|
|
|
|
@test 'parse returns error for oversized file (> 10MiB)' {
|
|
command truncate -s 11M "$fixture" 2>/dev/null
|
|
|
|
run _parse_dotenv_quiet "$fixture" "test"
|
|
assert $state equals 1
|
|
}
|
|
|
|
@test 'parse returns error for non-existent file' {
|
|
run _parse_dotenv_quiet "/nonexistent/path/.env" "test"
|
|
assert $state equals 1
|
|
}
|
|
|
|
@test 'source_env loads named pipes without blocking' {
|
|
local tmpdir fifo output result
|
|
local child_pid writer_pid killer_pid child_rc
|
|
|
|
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dotenv.XXXXXX")"
|
|
fifo="$tmpdir/.env"
|
|
output="$tmpdir/output"
|
|
command mkfifo "$fifo"
|
|
|
|
(
|
|
print -r -- 'TOKEN=secret' > "$fifo"
|
|
) &
|
|
writer_pid=$!
|
|
|
|
(
|
|
ZSH_DOTENV_PROMPT=false
|
|
ZSH_DOTENV_FILE="$fifo"
|
|
source_env
|
|
print -r -- "${TOKEN-<unset>}" > "$output"
|
|
) &
|
|
child_pid=$!
|
|
|
|
(
|
|
sleep 2
|
|
kill -0 $child_pid 2>/dev/null || exit 0
|
|
kill $child_pid 2>/dev/null || exit 0
|
|
) &
|
|
killer_pid=$!
|
|
|
|
wait $child_pid
|
|
child_rc=$?
|
|
|
|
kill $killer_pid 2>/dev/null || true
|
|
kill $writer_pid 2>/dev/null || true
|
|
wait $writer_pid 2>/dev/null || true
|
|
|
|
[[ -f "$output" ]] && result="$(<"$output")"
|
|
command rm -rf "$tmpdir"
|
|
|
|
assert $child_rc equals 0
|
|
assert "$result" equals 'secret'
|
|
}
|
|
|
|
@test 'source_env rejects oversized named pipes' {
|
|
run zsh -fc '
|
|
source ./dotenv.plugin.zsh
|
|
|
|
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dotenv.XXXXXX")" || exit 1
|
|
fifo="$tmpdir/.env"
|
|
command mkfifo "$fifo" || exit 1
|
|
|
|
cleanup() {
|
|
kill $killer_pid 2>/dev/null || true
|
|
kill $writer_pid 2>/dev/null || true
|
|
wait $writer_pid 2>/dev/null || true
|
|
command rm -rf "$tmpdir"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
(
|
|
{
|
|
print -rn -- "BIG="
|
|
command dd if=/dev/zero bs=10485761 count=1 2>/dev/null | tr "\0" a
|
|
} > "$fifo"
|
|
) &
|
|
writer_pid=$!
|
|
|
|
(
|
|
sleep 2
|
|
kill -0 $$ 2>/dev/null || exit 0
|
|
kill $$ 2>/dev/null || exit 0
|
|
) &
|
|
killer_pid=$!
|
|
|
|
ZSH_DOTENV_PROMPT=false
|
|
ZSH_DOTENV_FILE="$fifo"
|
|
source_env >/dev/null 2>&1
|
|
'
|
|
|
|
assert $state equals 1
|
|
}
|
|
|
|
@test 'parse basic variable assignment' {
|
|
> "$fixture" <<'EOF'
|
|
# Basic assignments
|
|
BASIC=basic
|
|
|
|
# previous line intentionally left blank
|
|
AFTER_LINE=after_line
|
|
EOF
|
|
|
|
expected_vars=(
|
|
BASIC 'basic'
|
|
AFTER_LINE 'after_line'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse empty values' {
|
|
> "$fixture" <<'EOF'
|
|
# Empty values
|
|
EMPTY=
|
|
EMPTY_SINGLE_QUOTES=''
|
|
EMPTY_DOUBLE_QUOTES=""
|
|
EOF
|
|
|
|
expected_vars=(
|
|
EMPTY ''
|
|
EMPTY_SINGLE_QUOTES ''
|
|
EMPTY_DOUBLE_QUOTES ''
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse single quoted values' {
|
|
> "$fixture" <<'EOF'
|
|
# Single quotes (literal, no expansion)
|
|
SINGLE_QUOTES='single_quotes'
|
|
SINGLE_QUOTES_SPACED=' single quotes '
|
|
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
|
|
EOF
|
|
|
|
expected_vars=(
|
|
SINGLE_QUOTES 'single_quotes'
|
|
SINGLE_QUOTES_SPACED ' single quotes '
|
|
DONT_EXPAND_SQUOTED 'dontexpand\nnewlines'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse double quoted values' {
|
|
> "$fixture" <<'EOF'
|
|
# Double quotes (with escapes)
|
|
DOUBLE_QUOTES="double_quotes"
|
|
DOUBLE_QUOTES_SPACED=" double quotes "
|
|
EXPAND_NEWLINES="expand\nnew\nlines"
|
|
EOF
|
|
|
|
expected_vars=(
|
|
DOUBLE_QUOTES 'double_quotes'
|
|
DOUBLE_QUOTES_SPACED ' double quotes '
|
|
EXPAND_NEWLINES "expand\nnew\nlines"
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse unquoted values' {
|
|
> "$fixture" <<'EOF'
|
|
# Unquoted (no escape expansion)
|
|
DONT_EXPAND_UNQUOTED=dontexpand\\nnewlines
|
|
EOF
|
|
|
|
|
|
expected_vars=(
|
|
DONT_EXPAND_UNQUOTED 'dontexpandnnewlines'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse quotes inside quotes' {
|
|
> "$fixture" <<'EOF'
|
|
# Quotes inside quotes
|
|
DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes'
|
|
SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes"
|
|
EOF
|
|
|
|
expected_vars=(
|
|
DOUBLE_QUOTES_INSIDE_SINGLE 'double "quotes" work inside single quotes'
|
|
SINGLE_QUOTES_INSIDE_DOUBLE "single 'quotes' work inside double quotes"
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse inline comments' {
|
|
> "$fixture" <<'EOF'
|
|
# Comments
|
|
# COMMENTS=work
|
|
INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
|
|
INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work
|
|
INLINE_COMMENTS_UNQUOTED=value # work
|
|
EOF
|
|
|
|
expected_vars=(
|
|
INLINE_COMMENTS_SINGLE_QUOTES 'inline comments outside of #singlequotes'
|
|
INLINE_COMMENTS_DOUBLE_QUOTES 'inline comments outside of #doublequotes'
|
|
INLINE_COMMENTS_UNQUOTED 'value'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse ignores non-assignment commands with assignment-looking arguments' {
|
|
> "$fixture" <<'EOF'
|
|
print SHOULD_NOT_PARSE=value
|
|
EOF
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse special characters' {
|
|
> "$fixture" <<'EOF'
|
|
# Special characters
|
|
EQUAL_SIGNS=equals==
|
|
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
|
|
USEREMAIL=therealnerdybeast@example.tld
|
|
EOF
|
|
|
|
expected_vars=(
|
|
EQUAL_SIGNS 'equals=='
|
|
RETAIN_INNER_QUOTES_AS_STRING '{"foo": "bar"}'
|
|
USEREMAIL 'therealnerdybeast@example.tld'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse multiline values with mixed quotes' {
|
|
> "$fixture" <<'EOF'
|
|
# Multiline values with double quotes
|
|
MULTI_DOUBLE_QUOTED="THIS
|
|
IS
|
|
A
|
|
MULTILINE
|
|
STRING"
|
|
|
|
|
|
# Multiline values with single quotes
|
|
MULTI_SINGLE_QUOTED='THIS
|
|
IS
|
|
A
|
|
MULTILINE
|
|
STRING'
|
|
|
|
# Multiline PEM certificate
|
|
MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
|
|
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
|
|
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
|
|
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
|
|
u4QuUoobAgMBAAE=
|
|
-----END PUBLIC KEY-----"
|
|
EOF
|
|
|
|
expected_vars=(
|
|
MULTI_DOUBLE_QUOTED $'THIS\nIS\nA\nMULTILINE\nSTRING'
|
|
MULTI_SINGLE_QUOTED $'THIS\nIS\nA\nMULTILINE\nSTRING'
|
|
MULTI_PEM_DOUBLE_QUOTED $'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u\nLgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/\nbTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/\nkKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V\nu4QuUoobAgMBAAE=\n-----END PUBLIC KEY-----'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse export syntax' {
|
|
> "$fixture" <<'EOF'
|
|
# Exported variables
|
|
export EXPORTED_VAR=exported_value
|
|
export EXPORTED_EMPTY=
|
|
EOF
|
|
|
|
expected_vars=(
|
|
EXPORTED_VAR 'exported_value'
|
|
EXPORTED_EMPTY ''
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse in-file variable expansion' {
|
|
> "$fixture" <<'EOF'
|
|
# Variable expansion (in-file forward references)
|
|
BASE_URL=https://api.example.com
|
|
API_ENDPOINT="${BASE_URL}/v1"
|
|
FULL_ENDPOINT=$BASE_URL/v2/users
|
|
COMBINED="${BASE_URL}_suffix"
|
|
EOF
|
|
|
|
expected_vars=(
|
|
BASE_URL 'https://api.example.com'
|
|
API_ENDPOINT 'https://api.example.com/v1'
|
|
FULL_ENDPOINT 'https://api.example.com/v2/users'
|
|
COMBINED 'https://api.example.com_suffix'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse in-file variable expansion prefers the longest matching variable name' {
|
|
> "$fixture" <<'EOF'
|
|
A=1
|
|
ABC=2
|
|
X=$ABC
|
|
Y=${ABC}
|
|
Z=$ABCD
|
|
EOF
|
|
|
|
expected_vars=(
|
|
A '1'
|
|
ABC '2'
|
|
X '2'
|
|
Y '2'
|
|
Z ''
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|
|
|
|
@test 'parse preserves escaped dollar signs before variable expansion' {
|
|
> "$fixture" <<'EOF'
|
|
BAR=expanded
|
|
ESCAPED_UNQUOTED=foo\$BAR
|
|
ESCAPED_DOUBLE="foo\$BAR"
|
|
ESCAPED_BRACED="\${BAR}"
|
|
EOF
|
|
|
|
expected_vars=(
|
|
BAR 'expanded'
|
|
ESCAPED_UNQUOTED 'foo$BAR'
|
|
ESCAPED_DOUBLE 'foo$BAR'
|
|
ESCAPED_BRACED '${BAR}'
|
|
)
|
|
|
|
_parse_dotenv_test "$fixture"
|
|
|
|
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
|
}
|