Skip to content

Commit

Permalink
Use ftype & give windows a glob_gitignore escape
Browse files Browse the repository at this point in the history
  • Loading branch information
robotdana committed Nov 22, 2023
1 parent 223ec20 commit 4dafc89
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 102 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: [2.7, '3.0', 3.1, 3.2, 3.3, ruby-head, jruby-9.4, jruby-head]
ruby: [2.7, '3.0', 3.1, 3.2, ruby-head, jruby-9.4, jruby-head]
platform: [ubuntu, windows, macos]
continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
runs-on: ${{matrix.platform}}-latest
Expand Down
1 change: 1 addition & 0 deletions .spellr_wordlists/english.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ tsx
ttributes
txt
unanchorable
unc
unexpandable
unfuck
unnegated
Expand Down
14 changes: 14 additions & 0 deletions bin/benchmark
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,16 @@ benchmark('add-trailing-slash') do
@string_trailing_slash = 'D:/'
@string_no_trailing_slash = '/bin'

raise unless "/str" == (@string_slash.end_with?('/') ? @string_slash : "#{@string_slash}/") + "str"
raise unless "D:/str" == (@string_trailing_slash.end_with?('/') ? @string_trailing_slash : "#{@string_trailing_slash}/") + "str"
raise unless "/bin/str" == (@string_no_trailing_slash.end_with?('/') ? @string_no_trailing_slash : "#{@string_no_trailing_slash}/") + "str"
raise unless "/str" == ::File.join(@string_slash, "str")
raise unless "D:/str" == ::File.join(@string_trailing_slash, "str")
raise unless "/bin/str" == ::File.join(@string_no_trailing_slash, "str")
raise unless "/str" == @string_slash.sub(/(?<!\/)\z/, '/') + "str"
raise unless "D:/str" == @string_trailing_slash.sub(/(?<!\/)\z/, '/') + "str"
raise unless "/bin/str" == @string_no_trailing_slash.sub(/(?<!\/)\z/, '/') + "str"

x.report(:string_slash_end_with) { (@string_slash.end_with?('/') ? @string_slash : "#{@string_slash}/") + "str" }
x.report(:string_trailing_slash_end_with) { (@string_trailing_slash.end_with?('/') ? @string_trailing_slash : "#{@string_trailing_slash}/") + "str" }
x.report(:string_no_trailing_slash_end_with) { (@string_no_trailing_slash.end_with?('/') ? @string_no_trailing_slash : "#{@string_no_trailing_slash}/") + "str" }
Expand All @@ -276,6 +286,10 @@ benchmark('add-trailing-slash') do
x.report(:string_trailing_slash_file_join) { ::File.join(@string_trailing_slash, "str") }
x.report(:string_no_trailing_slash_file_join) { ::File.join(@string_no_trailing_slash, "str") }

x.report(:string_slash_sub) { @string_slash.sub(/(?<!\/)\z/, '/') + "str" }
x.report(:string_trailing_slash_sub) { @string_trailing_slash.sub(/(?<!\/)\z/, '/') + "str" }
x.report(:string_no_trailing_slash_sub) { @string_no_trailing_slash.sub(/(?<!\/)\z/, '/') + "str" }

x.compare!
end
end
Expand Down
55 changes: 28 additions & 27 deletions lib/path_list/candidate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def initialize(full_path, directory = nil, shebang = nil)

@child_candidates = nil
@children = nil
@ftype = nil
end

# @return [String] full path downcased
Expand Down Expand Up @@ -54,33 +55,16 @@ def children
end
end

# :nocov:
if ::RUBY_PLATFORM == 'java' && ::RbConfig::CONFIG['host_os'].match?(/mswin|mingw/)
# @return [Boolean] whether this path is a directory (false for symlinks to directories)
def directory?
return @directory unless @directory.nil?

@directory = if ::File.symlink?(@full_path)
warn 'Symlink lstat'
warn lstat.inspect
false
else
lstat&.directory? || false
end
end
# :nocov:
else
# @return [Boolean] whether this path is a directory (false for symlinks to directories)
def directory?
return @directory unless @directory.nil?
# @return [Boolean] whether this path is a directory (false for symlinks to directories)
def directory?
return @directory unless @directory.nil?

@directory = lstat&.directory? || false
end
@directory = ftype == 'directory'
end

# @return [Boolean] whether this path exists
def exists?
lstat ? true : false
ftype != 'error'
end

alias_method :original_inspect, :inspect # leftovers:keep
Expand Down Expand Up @@ -112,7 +96,7 @@ def shebang
''
end
rescue ::IOError, ::SystemCallError
@lstat ||= nil
@ftype ||= 'error'
''
ensure
file&.close
Expand All @@ -121,12 +105,29 @@ def shebang

private

def lstat
return @lstat if defined?(@lstat)
# :nocov:
# https://github.com/jruby/jruby/issues/8018
# ftype follows symlinks on jruby on windows.
if ::RUBY_PLATFORM == 'java' && ::RbConfig::CONFIG['host_os'].match?(/mswin|mingw/)
refine ::File do
# :nodoc:
def ftype(path)
if ::File.symlink?(path)
'link'
else
super
end
end
end
end
# :nocov:

def ftype
return @ftype if @ftype

@lstat = ::File.lstat(@full_path)
@ftype = ::File.ftype(@full_path)
rescue ::SystemCallError
@lstat = nil
@ftype = 'error'
end
end
end
34 changes: 26 additions & 8 deletions lib/path_list/pattern_parser/glob_gitignore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ class PatternParser
# - Patterns containing with `/../` are resolved relative to the `root:` directory
# - Patterns beginning with `*` (or `!*`) will match any descendant of the `root:` directory
# - Other patterns match children (not descendants) of the `root:` directory
# - Additionally, on windows
# - \ is treated as a path separator, not an escape character.
# There is no cross-platform escape character when using :glob_gitignore format.
# - Patterns beginning with `c:/`, `d:\`, or `!c:/`, or etc are absolute.
# - Additionally, on windows:
# - either / or \ (slash or backslash) can be used as path separators.
# - therefore \ (backslash) isn't available to be used as an escape character
# - instead ` (grave accent) is used as an escape character
# - patterns beginning with `c:/`, `d:\`, or `!c:/`, or etc are absolute.
# - a path beginning with / or \ is a shortcut for the current working directory drive.
# - there is no cross platform escape character, this is intended to match the current shell
# @example
# PathList.only(ARGV, format: :glob_gitignore)
# PathList.only(
Expand Down Expand Up @@ -51,19 +54,34 @@ class GlobGitignore < Gitignore
def initialize(pattern, polarity, root)
pattern = +'' if pattern.start_with?('#')
negated_sigil = '!' if pattern.delete_prefix!('!')
pattern = normalize_slash(pattern)
if pattern.start_with?('*')
pattern = "#{negated_sigil}#{pattern.tr(::File::ALT_SEPARATOR.to_s, ::File::SEPARATOR)}"
pattern = "#{negated_sigil}#{pattern}"
elsif pattern.match?(EXPANDABLE_PATH)
dir_only! if pattern.match?(%r{[/\\]\s*\z}) # expand_path will remove it
dir_only! if pattern.match?(%r{/\s*\z}) # expand_path will remove it

pattern = "#{negated_sigil}#{CanonicalPath.full_path_from(pattern, root)}"
root = nil
@anchored = true
else
pattern = "#{negated_sigil}/#{pattern.tr(::File::ALT_SEPARATOR.to_s, ::File::SEPARATOR)}"
pattern = "#{negated_sigil}/#{pattern}"
end

super(pattern, polarity, root)
super(normalize_escape(pattern), polarity, root)
end

private

def normalize_slash(pattern)
return pattern unless ::File::ALT_SEPARATOR

pattern.tr('\\\\', '/')
end

def normalize_escape(pattern)
return pattern unless ::File::ALT_SEPARATOR

pattern.tr('`', '\\')
end
end
end
Expand Down
30 changes: 22 additions & 8 deletions lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@
class PathList
class PatternParser
class GlobGitignore
EXPANDABLE_PATH = if File.expand_path('/') == '/'
%r{(?:\A(?:[~/]|\.{1,2}(?:/|\z))|(?:[^\\]|\A)(?:\\{2})*/\.\./)}
# :nocov:
else
# this isn't actually nocov, but it's cov is because i reload the file
%r{(?:\A(?:[~/\\]|[a-zA-Z]:[/\\]|[/\\]{2}|\.{1,2}(?:[/\\]|\z))|[\\/]\.\.[/\\])}
# :nocov:
end
# :nocov:
# this isn't actually nocov, but it's cov is because i reload the file
EXPANDABLE_PATH = %r{(?:
\A(?:
[~/] # start with slash or tilde
|
\.{1,2}(?:/|\z) # start with dot or dot dot followed by slash or nothing
#{
if ::File.expand_path('/') != '/' # only if drive letters are applicable
"
|
[a-zA-Z]:/ # drive letter
|
// # UNC path
"
end
}
)
|
(?:[^\\]|\A)(?:\\{2})*/\.\./) # unescaped slash dot dot slash
}x.freeze
# :nocov:
end
end
end
27 changes: 12 additions & 15 deletions spec/candidate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
end

describe '#parent' do
before { allow(File).to receive_messages(exist?: nil, lstat: nil, directory?: nil) }
before { allow(File).to receive_messages(exist?: nil, ftype: nil, directory?: nil) }

it 'returns a candidate for the parent with preset directory value' do
expect(candidate.parent).to be_like described_class.new('/path/from/root', true)
expect(candidate.parent).to have_attributes(
directory?: true
)
expect(File).not_to have_received(:directory?)
expect(File).not_to have_received(:lstat)
expect(File).not_to have_received(:ftype)
end

context 'when the path is /' do
Expand All @@ -54,35 +54,35 @@
before { create_file_list 'foo' }

it 'is memoized when true' do
allow(File).to receive(:lstat).and_call_original
allow(File).to receive(:ftype).and_call_original

expect(candidate.exists?).to be true
expect(File).to have_received(:lstat).once
expect(File).to have_received(:ftype).once
expect(candidate.exists?).to be true
expect(File).to have_received(:lstat).once
expect(File).to have_received(:ftype).once
end
end

context 'when the file does not exist' do
let(:full_path) { './foo' }

it 'is memoized when false' do
allow(File).to receive(:lstat).and_call_original
allow(File).to receive(:ftype).and_call_original

expect(candidate.exists?).to be false
expect(File).to have_received(:lstat).with('./foo').once
expect(File).to have_received(:ftype).with('./foo').once
expect(candidate.exists?).to be false
expect(File).to have_received(:lstat).with('./foo').once
expect(File).to have_received(:ftype).with('./foo').once
end

it 'is false when there is an error' do
allow(File).to receive(:lstat).and_call_original
allow(File).to receive(:lstat).with(full_path).and_raise(Errno::EACCES)
allow(File).to receive(:ftype).and_call_original
allow(File).to receive(:ftype).with(full_path).and_raise(Errno::EACCES)

expect(candidate.exists?).to be false
expect(File).to have_received(:lstat).with('./foo').once
expect(File).to have_received(:ftype).with('./foo').once
expect(candidate.exists?).to be false
expect(File).to have_received(:lstat).with('./foo').once
expect(File).to have_received(:ftype).with('./foo').once
end
end
end
Expand Down Expand Up @@ -139,9 +139,6 @@
create_symlink('foo' => 'foo_target')

candidate = described_class.new(File.expand_path('foo'))
expect(File.symlink?('./foo')).to be true
expect(File.stat('./foo')).to have_attributes(directory?: false, symlink?: true)
expect(candidate.send(:lstat)).to have_attributes(directory?: false, symlink?: true)
expect(candidate).not_to be_directory
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/path_list_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
it 'copes with being given fs root' do
whatever_file_we_get = subject.each('/').first
expect(whatever_file_we_get).not_to start_with('/')
# use lstat because it could be a symlink to nowhere and File.exist? will be sad
expect(File.lstat("/#{whatever_file_we_get}")).to be_a File::Stat
# use symlink? because it could be a symlink to nowhere and File.exist? would return false
expect { File.symlink?("/#{whatever_file_we_get}") || File.exist?("/#{whatever_file_we_get}") }.not_to raise_error
end

it 'copes with being given nonsense root' do
Expand Down
Loading

0 comments on commit 4dafc89

Please sign in to comment.