Skip to content

Commit

Permalink
Properly unescape diff paths
Browse files Browse the repository at this point in the history
  • Loading branch information
jcouball committed Dec 23, 2021
1 parent cb01d2b commit 6ba1c8d
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 15 deletions.
4 changes: 2 additions & 2 deletions lib/git/diff.rb
Expand Up @@ -129,8 +129,8 @@ def process_full_diff
final = {}
current_file = nil
@full_diff.split("\n").each do |line|
if m = /^diff --git a\/(.*?) b\/(.*?)/.match(line)
current_file = m[1]
if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
current_file = Git::Lib.unescape_path(m[2])
final[current_file] = defaults.merge({:patch => line, :path => current_file})
else
if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)
Expand Down
36 changes: 35 additions & 1 deletion lib/git/lib.rb
Expand Up @@ -1085,7 +1085,8 @@ def command(cmd, *opts, &block)
global_opts = []
global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
global_opts << ["-c", "color.ui=false"]
global_opts << %w[-c core.quotePath=true]
global_opts << %w[-c color.ui=false]

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

Expand Down Expand Up @@ -1226,5 +1227,38 @@ def windows_platform?
RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex
end

UNESCAPES = {
'a' => 0x07,
'b' => 0x08,
't' => 0x09,
'n' => 0x0a,
'v' => 0x0b,
'f' => 0x0c,
'r' => 0x0d,
'e' => 0x1b,
"\\" => 0x5c,
"\"" => 0x22,
"'" => 0x27
}

def self.unescape_path(path)
index = 0
bytes = []
while index < path.length
if path[index] == '\\'
if '0' <= path[index + 1] && path[index + 1] <= '7'
bytes << path[index + 1..index + 4].to_i(8)
index += 4
elsif UNESCAPES.include?(path[index + 1])
bytes << UNESCAPES[path[index + 1]]
index += 2
end
else
bytes << path[index].ord
index += 1
end
end
bytes.pack('c*').force_encoding(Encoding::UTF_8)
end
end
end
2 changes: 1 addition & 1 deletion tests/units/test_archive.rb
Expand Up @@ -45,7 +45,7 @@ def test_archive

f = @git.object('v2.6').archive(tempfile, :format => 'tar', :prefix => 'test/', :path => 'ex_dir/')
assert(File.exist?(f))

lines = Minitar::Input.open(f).each.to_a.map(&:full_name)
assert_match(%r{test/}, lines[1])
assert_match(%r{test/ex_dir/ex\.txt}, lines[3])
Expand Down
22 changes: 22 additions & 0 deletions tests/units/test_diff_with_escaped_path.rb
@@ -0,0 +1,22 @@
#!/usr/bin/env ruby
# encoding: utf-8

require File.dirname(__FILE__) + '/../test_helper'

# Test diff when the file path has to be quoted according to core.quotePath
# See https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
#
class TestDiffWithEscapedPath < Test::Unit::TestCase
def test_diff_with_non_ascii_filename
in_temp_dir do |path|
create_file('my_other_file_☠', "First Line\n")
`git init`
`git add .`
`git config --local core.safecrlf false` if Gem.win_platform?
`git commit -m "First Commit"`
update_file('my_other_file_☠', "Second Line\n")
diff_paths = Git.open('.').diff.map(&:path)
assert_equal(["my_other_file_☠"], diff_paths)
end
end
end
39 changes: 28 additions & 11 deletions tests/units/test_logger.rb
Expand Up @@ -7,32 +7,49 @@ class TestLogger < Test::Unit::TestCase
def setup
set_file_paths
end


def missing_log_entry
'Did not find expected log entry.'
end

def unexpected_log_entry
'Unexpected log entry found'
end

def test_logger
log = Tempfile.new('logfile')
log.close

logger = Logger.new(log.path)
logger.level = Logger::DEBUG

@git = Git.open(@wdir, :log => logger)
@git.branches.size

logc = File.read(log.path)
assert(/INFO -- : git ['"]--git-dir=[^'"]+['"] ['"]--work-tree=[^'"]+['"] ['"]-c['"] ['"]color.ui=false['"] branch ['"]-a['"]/.match(logc))
assert(/DEBUG -- : cherry\n diff_over_patches\n\* git_grep/m.match(logc))

expected_log_entry = /INFO -- : git (?<global_options>.*?) branch ['"]-a['"]/
assert_match(expected_log_entry, logc, missing_log_entry)

expected_log_entry = /DEBUG -- : cherry/
assert_match(expected_log_entry, logc, missing_log_entry)
end

def test_logging_at_info_level_should_not_show_debug_messages
log = Tempfile.new('logfile')
log.close
logger = Logger.new(log.path)
logger.level = Logger::INFO

@git = Git.open(@wdir, :log => logger)
@git.branches.size

logc = File.read(log.path)
assert(/INFO -- : git ['"]--git-dir=[^'"]+['"] ['"]--work-tree=[^'"]+['"] ['"]-c['"] ['"]color.ui=false['"] branch ['"]-a['"]/.match(logc))
assert(!/DEBUG -- : cherry\n diff_over_patches\n\* git_grep/m.match(logc))

expected_log_entry = /INFO -- : git (?<global_options>.*?) branch ['"]-a['"]/
assert_match(expected_log_entry, logc, missing_log_entry)

expected_log_entry = /DEBUG -- : cherry/
assert_not_match(expected_log_entry, logc, unexpected_log_entry)
end

end
35 changes: 35 additions & 0 deletions tests/units/test_unescape_path.rb
@@ -0,0 +1,35 @@
#!/usr/bin/env ruby
# encoding: utf-8

require File.dirname(__FILE__) + '/../test_helper'

# Test diff when the file path has to be quoted according to core.quotePath
# See https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
# See https://www.jvt.me/posts/2020/06/23/byte-array-to-string-ruby/
#
class TestUnescapePath < Test::Unit::TestCase
def test_simple_path
path = 'my_other_file'
expected_unescaped_path = 'my_other_file'
assert_equal(expected_unescaped_path, Git::Lib.unescape_path(path))
end

def test_unicode_path
path = 'my_other_file_\\342\\230\\240'
expected_unescaped_path = 'my_other_file_☠'
assert_equal(expected_unescaped_path, Git::Lib.unescape_path(path))
end

def test_single_char_escapes
Git::Lib::UNESCAPES.each_pair do |escape_char, expected_char|
path = "\\#{escape_char}"
assert_equal(expected_char.chr, Git::Lib.unescape_path(path))
end
end

def test_compound_escape
path = "my_other_file_\\\"\\342\\230\\240\\n\\\""
expected_unescaped_path = 'my_other_file_"☠' + "\n" + '"'
assert_equal(expected_unescaped_path, Git::Lib.unescape_path(path))
end
end

0 comments on commit 6ba1c8d

Please sign in to comment.