mirror of
https://github.com/git/git.git
synced 2026-06-30 19:58:12 +00:00
f85b49f3d4 (diff: improve scaling of filenames in diffstat to handle
UTF-8 chars, 2026-01-16) introduced a loop in show_stats() that calls
utf8_width() repeatedly to skip leading characters until the displayed
width fits. However, utf8_width() can return problematic values:
- For invalid UTF-8 sequences, pick_one_utf8_char() sets the name
pointer to NULL and utf8_width() returns 0. Since name_len does
not change, the loop iterates once more and pick_one_utf8_char()
dereferences the NULL pointer, crashing.
- For control characters, utf8_width() returns -1, so name_len
grows when it is expected to shrink. This can cause the loop to
consume more characters than the string contains, reading past
the trailing NUL.
By default, fill_print_name() will C-quote filenames which escapes
control characters and invalid bytes to printable text. That avoids
this bug from being triggered; however, with core.quotePath=false,
most characters are no longer escaped (though some control characters
still are) and raw bytes can reach this code.
Add tests exercising both failure modes with core.quotePath=false and
a narrow --stat-name-width to force truncation: one with a bare 0xC0
byte (invalid UTF-8 lead byte, triggers NULL deref) and one with
several C1 control characters (repeats of 0xC2 0x9F, causing
the loop to read past the end of the string). The second test
reliably catches the out-of-bounds read when run under ASan, though
it may pass silently without sanitizers.
Fix both issues by introducing utf8_ish_width(), a thin wrapper
around utf8_width() that guarantees the pointer always advances and
the returned width is never negative:
- On invalid UTF-8 it restores the pointer, advances by one byte,
and returns width 1 (matching the strlen()-based fallback used
by utf8_strwidth()).
- On a control character it returns 0 (matching utf8_strnwidth()
which skips them).
Also add a "&& *name" guard to the while-loop condition so it
terminates at end-of-string even when utf8_strwidth()'s strlen()
fallback causes name_len to exceed the sum of per-character widths.
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
474 lines
14 KiB
Bash
Executable File
474 lines
14 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# Copyright (c) 2012 Zbigniew Jędrzejewski-Szmek
|
|
#
|
|
|
|
test_description='test --stat output of various commands'
|
|
|
|
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
|
|
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
|
|
|
. ./test-lib.sh
|
|
. "$TEST_DIRECTORY"/lib-terminal.sh
|
|
|
|
# 120-character name
|
|
name=aaaaaaaaaa
|
|
name=$name$name$name$name$name$name$name$name$name$name$name$name
|
|
test_expect_success 'preparation' '
|
|
>"$name" &&
|
|
git add "$name" &&
|
|
git commit -m message &&
|
|
echo a >"$name" &&
|
|
git commit -m message "$name"
|
|
'
|
|
|
|
cat >expect72 <<-'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
EOF
|
|
test_expect_success "format-patch: small change with long name gives more space to the name" '
|
|
git format-patch -1 --stdout >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect72 actual
|
|
'
|
|
|
|
while read cmd args
|
|
do
|
|
cat >expect80 <<-'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
EOF
|
|
test_expect_success "$cmd: small change with long name gives more space to the name" '
|
|
git $cmd $args >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect80 actual
|
|
'
|
|
done <<\EOF
|
|
diff HEAD^ HEAD --stat
|
|
show --stat
|
|
log -1 --stat
|
|
EOF
|
|
|
|
cat >expect.60 <<-'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
EOF
|
|
cat >expect.6030 <<-'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
EOF
|
|
while read verb expect cmd args
|
|
do
|
|
# No width limit applied when statNameWidth is ignored
|
|
case "$expect" in expect72|expect.6030)
|
|
test_expect_success "$cmd $verb diff.statNameWidth with long name" '
|
|
git -c diff.statNameWidth=30 $cmd $args >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp $expect actual
|
|
';;
|
|
esac
|
|
# Maximum width limit still applied when statNameWidth is ignored
|
|
case "$expect" in expect.60|expect.6030)
|
|
test_expect_success "$cmd --stat=width $verb diff.statNameWidth with long name" '
|
|
git -c diff.statNameWidth=30 $cmd $args --stat=60 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp $expect actual
|
|
';;
|
|
esac
|
|
done <<\EOF
|
|
ignores expect72 format-patch -1 --stdout
|
|
ignores expect.60 format-patch -1 --stdout
|
|
respects expect.6030 diff HEAD^ HEAD --stat
|
|
respects expect.6030 show --stat
|
|
respects expect.6030 log -1 --stat
|
|
EOF
|
|
|
|
cat >expect.40 <<-'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
EOF
|
|
cat >expect2.40 <<-'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
EOF
|
|
cat >expect2.6030 <<-'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 +
|
|
EOF
|
|
while read expect cmd args
|
|
do
|
|
test_expect_success "$cmd --stat=width: a long name is given more room when the bar is short" '
|
|
git $cmd $args --stat=40 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp $expect.40 actual
|
|
'
|
|
|
|
test_expect_success "$cmd --stat-width=width with long name" '
|
|
git $cmd $args --stat-width=40 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp $expect.40 actual
|
|
'
|
|
|
|
test_expect_success "$cmd --stat=width,name-width with long name" '
|
|
git $cmd $args --stat=60,30 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp $expect.6030 actual
|
|
'
|
|
|
|
test_expect_success "$cmd --stat-name-width=width with long name" '
|
|
git $cmd $args --stat-name-width=30 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp $expect.6030 actual
|
|
'
|
|
done <<\EOF
|
|
expect2 format-patch --cover-letter -1 --stdout
|
|
expect diff HEAD^ HEAD --stat
|
|
expect show --stat
|
|
expect log -1 --stat
|
|
EOF
|
|
|
|
test_expect_success 'preparation for big-change tests' '
|
|
>abcd &&
|
|
git add abcd &&
|
|
git commit -m message &&
|
|
i=0 &&
|
|
while test $i -lt 1000
|
|
do
|
|
echo $i && i=$(($i + 1)) || return 1
|
|
done >abcd &&
|
|
git commit -m message abcd
|
|
'
|
|
|
|
cat >expect72 <<'EOF'
|
|
abcd | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
abcd | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
test_expect_success "format-patch --cover-letter ignores COLUMNS with big change" '
|
|
COLUMNS=200 git format-patch -1 --stdout --cover-letter >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect72 actual
|
|
'
|
|
|
|
cat >expect72 <<'EOF'
|
|
abcd | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect72-graph <<'EOF'
|
|
| abcd | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect200 <<'EOF'
|
|
abcd | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect200-graph <<'EOF'
|
|
| abcd | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
while read verb expect cmd args
|
|
do
|
|
test_expect_success "$cmd $verb COLUMNS with big change" '
|
|
COLUMNS=200 git $cmd $args >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect" actual
|
|
'
|
|
|
|
case "$cmd" in diff|show) continue;; esac
|
|
|
|
test_expect_success "$cmd --graph $verb COLUMNS with big change" '
|
|
COLUMNS=200 git $cmd $args --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect-graph" actual
|
|
'
|
|
done <<\EOF
|
|
ignores expect72 format-patch -1 --stdout
|
|
respects expect200 diff HEAD^ HEAD --stat
|
|
respects expect200 show --stat
|
|
respects expect200 log -1 --stat
|
|
EOF
|
|
|
|
cat >expect40 <<'EOF'
|
|
abcd | 1000 ++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect40-graph <<'EOF'
|
|
| abcd | 1000 ++++++++++++++++++++++++
|
|
EOF
|
|
while read verb expect cmd args
|
|
do
|
|
test_expect_success "$cmd $verb not enough COLUMNS with big change" '
|
|
COLUMNS=40 git $cmd $args >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect" actual
|
|
'
|
|
|
|
case "$cmd" in diff|show) continue;; esac
|
|
|
|
test_expect_success "$cmd --graph $verb not enough COLUMNS with big change" '
|
|
COLUMNS=40 git $cmd $args --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect-graph" actual
|
|
'
|
|
done <<\EOF
|
|
ignores expect72 format-patch -1 --stdout
|
|
respects expect40 diff HEAD^ HEAD --stat
|
|
respects expect40 show --stat
|
|
respects expect40 log -1 --stat
|
|
EOF
|
|
|
|
cat >expect40 <<'EOF'
|
|
abcd | 1000 ++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect40-graph <<'EOF'
|
|
| abcd | 1000 ++++++++++++++++++++++++++
|
|
EOF
|
|
while read verb expect cmd args
|
|
do
|
|
test_expect_success "$cmd $verb diff.statGraphWidth" '
|
|
git -c diff.statGraphWidth=26 $cmd $args >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect" actual
|
|
'
|
|
|
|
case "$cmd" in diff|show) continue;; esac
|
|
|
|
test_expect_success "$cmd --graph $verb diff.statGraphWidth" '
|
|
git -c diff.statGraphWidth=26 $cmd $args --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect-graph" actual
|
|
'
|
|
done <<\EOF
|
|
ignores expect72 format-patch -1 --stdout
|
|
respects expect40 diff HEAD^ HEAD --stat
|
|
respects expect40 show --stat
|
|
respects expect40 log -1 --stat
|
|
EOF
|
|
|
|
cat >expect <<'EOF'
|
|
abcd | 1000 ++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect-graph <<'EOF'
|
|
| abcd | 1000 ++++++++++++++++++++++++++
|
|
EOF
|
|
while read cmd args
|
|
do
|
|
test_expect_success "$cmd --stat=width with big change" '
|
|
git $cmd $args --stat=40 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
test_expect_success "$cmd --stat-width=width with big change" '
|
|
git $cmd $args --stat-width=40 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
test_expect_success "$cmd --stat-graph-width=width with big change" '
|
|
git $cmd $args --stat-graph-width=26 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
case "$cmd" in diff|show) continue;; esac
|
|
|
|
test_expect_success "$cmd --stat-width=width --graph with big change" '
|
|
git $cmd $args --stat-width=40 --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect-graph actual
|
|
'
|
|
|
|
test_expect_success "$cmd --stat-graph-width=width --graph with big change" '
|
|
git $cmd $args --stat-graph-width=26 --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect-graph actual
|
|
'
|
|
done <<\EOF
|
|
format-patch -1 --stdout
|
|
diff HEAD^ HEAD --stat
|
|
show --stat
|
|
log -1 --stat
|
|
EOF
|
|
|
|
test_expect_success 'preparation for long-name tests' '
|
|
cp abcd aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &&
|
|
git add aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &&
|
|
git commit -m message
|
|
'
|
|
|
|
cat >expect <<'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 ++++++++++++
|
|
EOF
|
|
cat >expect-graph <<'EOF'
|
|
| ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 ++++++++++++
|
|
EOF
|
|
while read cmd args
|
|
do
|
|
test_expect_success "$cmd --stat=width with big change is more balanced" '
|
|
git $cmd $args --stat-width=60 >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
case "$cmd" in diff|show) continue;; esac
|
|
|
|
test_expect_success "$cmd --stat=width --graph with big change is balanced" '
|
|
git $cmd $args --stat-width=60 --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect-graph actual
|
|
'
|
|
done <<\EOF
|
|
format-patch -1 --stdout
|
|
diff HEAD^ HEAD --stat
|
|
show --stat
|
|
log -1 --stat
|
|
EOF
|
|
|
|
cat >expect72 <<'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 +++++++++++++++++
|
|
EOF
|
|
cat >expect72-graph <<'EOF'
|
|
| ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 +++++++++++++++++
|
|
EOF
|
|
cat >expect200 <<'EOF'
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect200-graph <<'EOF'
|
|
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
while read verb expect cmd args
|
|
do
|
|
test_expect_success "$cmd $verb COLUMNS with long name" '
|
|
COLUMNS=200 git $cmd $args >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect" actual
|
|
'
|
|
|
|
case "$cmd" in diff|show) continue;; esac
|
|
|
|
test_expect_success "$cmd --graph $verb COLUMNS with long name" '
|
|
COLUMNS=200 git $cmd $args --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect-graph" actual
|
|
'
|
|
done <<\EOF
|
|
ignores expect72 format-patch -1 --stdout
|
|
respects expect200 diff HEAD^ HEAD --stat
|
|
respects expect200 show --stat
|
|
respects expect200 log -1 --stat
|
|
EOF
|
|
|
|
cat >expect1 <<'EOF'
|
|
...aaaaaaa | 1000 ++++++
|
|
EOF
|
|
cat >expect1-graph <<'EOF'
|
|
| ...aaaaaaa | 1000 ++++++
|
|
EOF
|
|
while read verb expect cmd args
|
|
do
|
|
test_expect_success COLUMNS_CAN_BE_1 \
|
|
"$cmd $verb prefix greater than COLUMNS with big change" '
|
|
COLUMNS=1 git $cmd $args >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect" actual
|
|
'
|
|
|
|
case "$cmd" in diff|show) continue;; esac
|
|
|
|
test_expect_success COLUMNS_CAN_BE_1 \
|
|
"$cmd --graph $verb prefix greater than COLUMNS with big change" '
|
|
COLUMNS=1 git $cmd $args --graph >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp "$expect-graph" actual
|
|
'
|
|
done <<\EOF
|
|
ignores expect72 format-patch -1 --stdout
|
|
respects expect1 diff HEAD^ HEAD --stat
|
|
respects expect1 show --stat
|
|
respects expect1 log -1 --stat
|
|
EOF
|
|
|
|
cat >expect <<'EOF'
|
|
abcd | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
test_expect_success 'merge --stat respects diff.statGraphWidth with big change' '
|
|
git checkout -b branch1 HEAD^^ &&
|
|
git -c diff.statGraphWidth=26 merge --stat --no-ff main^ >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect40 actual
|
|
'
|
|
test_expect_success 'merge --stat respects COLUMNS with big change' '
|
|
git checkout -b branch2 HEAD^^ &&
|
|
COLUMNS=100 git merge --stat --no-ff main^ >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
cat >expect <<'EOF'
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 +++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
cat >expect.30 <<'EOF'
|
|
...aaaaaaaaaaaaaaaaaaaaaaaaaaa | 1000 ++++++++++++++++++++++++++++++++++++++++
|
|
EOF
|
|
test_expect_success 'merge --stat respects diff.statNameWidth with long name' '
|
|
git switch branch1 &&
|
|
git -c diff.statNameWidth=30 merge --stat --no-ff main >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect.30 actual
|
|
'
|
|
test_expect_success 'merge --stat respects COLUMNS with long name' '
|
|
git switch branch2 &&
|
|
COLUMNS=100 git merge --stat --no-ff main >output &&
|
|
grep " | " output >actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
# We want git-log to print only 1 commit containing a single branch graph and a
|
|
# diffstat (the diffstat display width, when not manually set through the
|
|
# option "--stat-width", will be automatically calculated).
|
|
# The diffstat will be only one file, with a placeholder FILENAME, that, with
|
|
# enough terminal display width, will contain the following line:
|
|
# "<RED>|<RESET> ${FILENAME} | 0"
|
|
# where "<RED>" and "<RESET>" are ANSI escape codes to color the text.
|
|
# To calculate the minimium terminal display width MIN_TERM_WIDTH so that the
|
|
# FILENAME in the diffstat will not be shortened, we take the FILENAME length
|
|
# and add 9 to it.
|
|
# To check if the diffstat width, when the line_prefix (the "<RED>|<RESET>" of
|
|
# the graph) contains ANSI escape codes (the ANSI escape codes to color the
|
|
# text), is calculated correctly, we:
|
|
# 1. check if it contains the line defined before when using MIN_TERM_WIDTH
|
|
# 2. check if it contains the line defined before, but with the FILENAME
|
|
# shortened by only one character, when using MIN_TERM_WIDTH - 1
|
|
|
|
test_expect_success 'diffstat where line_prefix contains ANSI escape codes is correct width' '
|
|
FILENAME="placeholder-text-placeholder-text" &&
|
|
FILENAME_TRIMMED="...eholder-text-placeholder-text" &&
|
|
MIN_TERM_WIDTH=$((${#FILENAME} + 9)) &&
|
|
test_config color.diff always &&
|
|
git commit --allow-empty --allow-empty-message &&
|
|
>${FILENAME} &&
|
|
git add ${FILENAME} &&
|
|
git commit --allow-empty-message &&
|
|
COLUMNS=$((MIN_TERM_WIDTH)) git log --graph --stat -n1 | test_decode_color >out &&
|
|
test_grep "<RED>|<RESET> ${FILENAME} | 0" out &&
|
|
COLUMNS=$((MIN_TERM_WIDTH - 1)) git log --graph --stat -n1 | test_decode_color >out &&
|
|
test_grep "<RED>|<RESET> ${FILENAME_TRIMMED} | 0" out
|
|
'
|
|
|
|
test_expect_success 'diffstat truncation with invalid UTF-8 does not crash' '
|
|
empty_blob=$(git hash-object -w --stdin </dev/null) &&
|
|
printf "100644 blob $empty_blob\taaa-\300-aaa\n" |
|
|
git mktree >tree_file &&
|
|
tree=$(cat tree_file) &&
|
|
empty_tree=$(git mktree </dev/null) &&
|
|
c1=$(git commit-tree -m before $empty_tree) &&
|
|
c2=$(git commit-tree -m after -p $c1 $tree) &&
|
|
git -c core.quotepath=false diff --stat --stat-name-width=5 $c1..$c2 >output &&
|
|
test_grep "| 0" output
|
|
'
|
|
|
|
test_expect_success FUNNYNAMES 'diffstat truncation with control chars does not read out of bounds' '
|
|
FNAME=$(printf "aaa-\302\237\302\237\302\237-aaa") &&
|
|
git commit --allow-empty -m setup &&
|
|
>$FNAME &&
|
|
git add -- $FNAME &&
|
|
git commit -m "add file with control char name" &&
|
|
git -c core.quotepath=false diff --stat --stat-name-width=5 HEAD~1..HEAD >output &&
|
|
test_grep "| 0" output &&
|
|
rm -- $FNAME &&
|
|
git rm -- $FNAME &&
|
|
git commit -m "remove test file"
|
|
'
|
|
|
|
test_done
|