Skip to content

Commit 5508037

Browse files
committed
Properly unescape diff paths
Signed-off-by: James Couball <jcouball@yahoo.com>
1 parent cb01d2b commit 5508037

File tree

6 files changed

+123
-15
lines changed

6 files changed

+123
-15
lines changed

lib/git/diff.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ def process_full_diff
129129
final = {}
130130
current_file = nil
131131
@full_diff.split("\n").each do |line|
132-
if m = /^diff --git a\/(.*?) b\/(.*?)/.match(line)
133-
current_file = m[1]
132+
if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
133+
current_file = Git::Lib.unescape_path(m[2])
134134
final[current_file] = defaults.merge({:patch => line, :path => current_file})
135135
else
136136
if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)

lib/git/lib.rb

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1085,7 +1085,8 @@ def command(cmd, *opts, &block)
10851085
global_opts = []
10861086
global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
10871087
global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
1088-
global_opts << ["-c", "color.ui=false"]
1088+
global_opts << %w[-c core.quotePath=true]
1089+
global_opts << %w[-c color.ui=false]
10891090

10901091
opts = [opts].flatten.map {|s| escape(s) }.join(' ')
10911092

@@ -1226,5 +1227,38 @@ def windows_platform?
12261227
RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex
12271228
end
12281229

1230+
UNESCAPES = {
1231+
'a' => 0x07,
1232+
'b' => 0x08,
1233+
't' => 0x09,
1234+
'n' => 0x0a,
1235+
'v' => 0x0b,
1236+
'f' => 0x0c,
1237+
'r' => 0x0d,
1238+
'e' => 0x1b,
1239+
"\\" => 0x5c,
1240+
"\"" => 0x22,
1241+
"'" => 0x27
1242+
}
1243+
1244+
def self.unescape_path(path)
1245+
index = 0
1246+
bytes = []
1247+
while index < path.length
1248+
if path[index] == '\\'
1249+
if '0' <= path[index + 1] && path[index + 1] <= '7'
1250+
bytes << path[index + 1..index + 4].to_i(8)
1251+
index += 4
1252+
elsif UNESCAPES.include?(path[index + 1])
1253+
bytes << UNESCAPES[path[index + 1]]
1254+
index += 2
1255+
end
1256+
else
1257+
bytes << path[index].ord
1258+
index += 1
1259+
end
1260+
end
1261+
bytes.pack('c*').force_encoding(Encoding::UTF_8)
1262+
end
12291263
end
12301264
end

tests/units/test_archive.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def test_archive
4545

4646
f = @git.object('v2.6').archive(tempfile, :format => 'tar', :prefix => 'test/', :path => 'ex_dir/')
4747
assert(File.exist?(f))
48-
48+
4949
lines = Minitar::Input.open(f).each.to_a.map(&:full_name)
5050
assert_match(%r{test/}, lines[1])
5151
assert_match(%r{test/ex_dir/ex\.txt}, lines[3])
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env ruby
2+
# encoding: utf-8
3+
4+
require File.dirname(__FILE__) + '/../test_helper'
5+
6+
# Test diff when the file path has to be quoted according to core.quotePath
7+
# See https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
8+
#
9+
class TestDiffWithEscapedPath < Test::Unit::TestCase
10+
def test_diff_with_non_ascii_filename
11+
in_temp_dir do |path|
12+
create_file('my_other_file_☠', "First Line\n")
13+
`git init`
14+
`git add .`
15+
`git config --local core.safecrlf false` if Gem.win_platform?
16+
`git commit -m "First Commit"`
17+
update_file('my_other_file_☠', "Second Line\n")
18+
diff_paths = Git.open('.').diff.map(&:path)
19+
assert_equal(["my_other_file_☠"], diff_paths)
20+
end
21+
end
22+
end

tests/units/test_logger.rb

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,49 @@ class TestLogger < Test::Unit::TestCase
77
def setup
88
set_file_paths
99
end
10-
10+
11+
def missing_log_entry
12+
'Did not find expected log entry.'
13+
end
14+
15+
def unexpected_log_entry
16+
'Unexpected log entry found'
17+
end
18+
1119
def test_logger
1220
log = Tempfile.new('logfile')
1321
log.close
14-
22+
1523
logger = Logger.new(log.path)
1624
logger.level = Logger::DEBUG
17-
25+
1826
@git = Git.open(@wdir, :log => logger)
1927
@git.branches.size
20-
28+
2129
logc = File.read(log.path)
22-
assert(/INFO -- : git ['"]--git-dir=[^'"]+['"] ['"]--work-tree=[^'"]+['"] ['"]-c['"] ['"]color.ui=false['"] branch ['"]-a['"]/.match(logc))
23-
assert(/DEBUG -- : cherry\n diff_over_patches\n\* git_grep/m.match(logc))
2430

31+
expected_log_entry = /INFO -- : git (?<global_options>.*?) branch ['"]-a['"]/
32+
assert_match(expected_log_entry, logc, missing_log_entry)
33+
34+
expected_log_entry = /DEBUG -- : cherry/
35+
assert_match(expected_log_entry, logc, missing_log_entry)
36+
end
37+
38+
def test_logging_at_info_level_should_not_show_debug_messages
2539
log = Tempfile.new('logfile')
2640
log.close
2741
logger = Logger.new(log.path)
2842
logger.level = Logger::INFO
29-
43+
3044
@git = Git.open(@wdir, :log => logger)
3145
@git.branches.size
32-
46+
3347
logc = File.read(log.path)
34-
assert(/INFO -- : git ['"]--git-dir=[^'"]+['"] ['"]--work-tree=[^'"]+['"] ['"]-c['"] ['"]color.ui=false['"] branch ['"]-a['"]/.match(logc))
35-
assert(!/DEBUG -- : cherry\n diff_over_patches\n\* git_grep/m.match(logc))
48+
49+
expected_log_entry = /INFO -- : git (?<global_options>.*?) branch ['"]-a['"]/
50+
assert_match(expected_log_entry, logc, missing_log_entry)
51+
52+
expected_log_entry = /DEBUG -- : cherry/
53+
assert_not_match(expected_log_entry, logc, unexpected_log_entry)
3654
end
37-
3855
end

tests/units/test_unescape_path.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env ruby
2+
# encoding: utf-8
3+
4+
require File.dirname(__FILE__) + '/../test_helper'
5+
6+
# Test diff when the file path has to be quoted according to core.quotePath
7+
# See https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
8+
# See https://www.jvt.me/posts/2020/06/23/byte-array-to-string-ruby/
9+
#
10+
class TestUnescapePath < Test::Unit::TestCase
11+
def test_simple_path
12+
path = 'my_other_file'
13+
expected_unescaped_path = 'my_other_file'
14+
assert_equal(expected_unescaped_path, Git::Lib.unescape_path(path))
15+
end
16+
17+
def test_unicode_path
18+
path = 'my_other_file_\\342\\230\\240'
19+
expected_unescaped_path = 'my_other_file_☠'
20+
assert_equal(expected_unescaped_path, Git::Lib.unescape_path(path))
21+
end
22+
23+
def test_single_char_escapes
24+
Git::Lib::UNESCAPES.each_pair do |escape_char, expected_char|
25+
path = "\\#{escape_char}"
26+
assert_equal(expected_char.chr, Git::Lib.unescape_path(path))
27+
end
28+
end
29+
30+
def test_compound_escape
31+
path = "my_other_file_\\\"\\342\\230\\240\\n\\\""
32+
expected_unescaped_path = 'my_other_file_"☠' + "\n" + '"'
33+
assert_equal(expected_unescaped_path, Git::Lib.unescape_path(path))
34+
end
35+
end

0 commit comments

Comments
 (0)