Module: CovLoupe::VolumeCaseSensitivity

Defined in:
lib/cov_loupe/paths/volume_case_sensitivity.rb

Overview

Handles detection and caching of filesystem volume case sensitivity. Provides thread-safe case sensitivity detection with caching for performance.

Constant Summary collapse

CACHE_MUTEX =

Mutex for thread-safe cache access

Mutex.new

Class Method Summary collapse

Class Method Details

.cacheHash

Returns the current cache contents (useful for testing)

Returns:

  • (Hash)

    cache contents



58
59
60
61
62
63
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 58

def cache
  CACHE_MUTEX.synchronize do
    @cache ||= {}
    @cache.dup
  end
end

.clear_cachevoid

This method returns an undefined value.

Clears the case sensitivity cache (useful for testing)



49
50
51
52
53
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 49

def clear_cache
  CACHE_MUTEX.synchronize do
    @cache = {}
  end
end

.detect_case_sensitive_using_existing_file?(abs_path, existing_file) ⇒ Boolean

Detects case sensitivity using an existing file in the directory

Parameters:

  • abs_path (String)

    absolute path to directory

  • existing_file (String)

    name of existing file

Returns:

  • (Boolean)

    true if case-sensitive, false if case-insensitive



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 118

def detect_case_sensitive_using_existing_file?(abs_path, existing_file)
  original = File.join(abs_path, existing_file)
  # tr('A-Za-z', 'a-zA-Z') swaps the case of every letter in the filename
  alternate_name = existing_file.tr('A-Za-z', 'a-zA-Z')
  alternate = File.join(abs_path, alternate_name)

  if File.exist?(alternate)
    # Same file -> case-insensitive, different files -> case-sensitive
    !File.identical?(original, alternate)
  else
    # On a case-insensitive filesystem, File.exist? for the alternate-cased name would
    # resolve to the same inode and return true. Reaching this branch means the filesystem
    # treats the two paths as distinct entries, confirming case-sensitivity.
    true
  end
end

.detect_case_sensitive_using_temp_file?(abs_path) ⇒ Boolean

Detects case sensitivity using a temporary test file

Parameters:

  • abs_path (String)

    absolute path to directory

Returns:

  • (Boolean)

    true if case-sensitive, false if case-insensitive



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 139

def detect_case_sensitive_using_temp_file?(abs_path)
  require 'securerandom'

  # Create a temporary test file with a unique name
  test_file = generate_unique_test_filename(abs_path)

  begin
    FileUtils.touch(test_file)
    variants = [test_file, test_file.upcase, test_file.downcase]
    # Test if exactly one variant exists (case-sensitive) vs all exist (case-insensitive)
    variants.one? { |variant| File.exist?(variant) }
  ensure
    # Clean up all potential variants
    [test_file, test_file.upcase, test_file.downcase].each do |variant|
      FileUtils.rm_f(variant)
    end
  end
end

.detect_case_sensitivity?(abs_path) ⇒ Boolean

Detects case sensitivity for a given directory

Parameters:

  • abs_path (String)

    absolute path to directory

Returns:

  • (Boolean)

    true if case-sensitive, false if case-insensitive



92
93
94
95
96
97
98
99
100
101
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 92

def detect_case_sensitivity?(abs_path)
  # Try to use an existing file to avoid filesystem writes
  existing_file = find_existing_file(abs_path)

  if existing_file
    detect_case_sensitive_using_existing_file?(abs_path, existing_file)
  else
    detect_case_sensitive_using_temp_file?(abs_path)
  end
end

.find_existing_file(abs_path) ⇒ String?

Finds an existing file in the directory suitable for case sensitivity testing

Parameters:

  • abs_path (String)

    absolute path to directory

Returns:

  • (String, nil)

    filename or nil if no suitable file found



107
108
109
110
111
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 107

def find_existing_file(abs_path)
  Dir.children(abs_path).find do |name|
    name.match?(/[A-Za-z]/) && File.file?(File.join(abs_path, name))
  end
end

.generate_unique_test_filename(abs_path) ⇒ String

Generates a unique test filename that doesn't conflict with existing files

Parameters:

  • abs_path (String)

    absolute path to directory

Returns:

  • (String)

    unique filename path



162
163
164
165
166
167
168
169
170
171
172
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 162

def generate_unique_test_filename(abs_path)
  require 'securerandom'

  test_file = nil
  while test_file.nil?
    candidate = File.join(abs_path, "CovLoupe_CaseSensitivity_Test_#{SecureRandom.hex(16)}.tmp")
    variants = [candidate, candidate.upcase, candidate.downcase]
    test_file = candidate if variants.none? { |v| File.exist?(v) }
  end
  test_file
end

.get_from_cache(abs_path) ⇒ Boolean?

Retrieves a value from the cache (thread-safe)

Parameters:

  • abs_path (String)

    absolute path to look up

Returns:

  • (Boolean, nil)

    cached value or nil if not found



69
70
71
72
73
74
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 69

def get_from_cache(abs_path)
  CACHE_MUTEX.synchronize do
    @cache ||= {}
    @cache[abs_path]
  end
end

.set_in_cache(abs_path, value) ⇒ void

This method returns an undefined value.

Stores a value in the cache (thread-safe)

Parameters:

  • abs_path (String)

    absolute path to cache

  • value (Boolean)

    value to cache



81
82
83
84
85
86
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 81

def set_in_cache(abs_path, value)
  CACHE_MUTEX.synchronize do
    @cache ||= {}
    @cache[abs_path] = value
  end
end

.volume_case_sensitive?(path = nil) ⇒ Boolean

Detects whether the volume at the given path is case-sensitive. Prefer using an existing file (via File.identical?) to avoid writing; fall back to a temporary file if no suitable file exists.

This method caches results by path to avoid repeated filesystem checks, which can be expensive, especially on network-mounted volumes.

Parameters:

  • path (String, nil) (defaults to: nil)

    directory path to test (defaults to current directory)

Returns:

  • (Boolean)

    true if case-sensitive, false if case-insensitive or on error



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/cov_loupe/paths/volume_case_sensitivity.rb', line 22

def volume_case_sensitive?(path = nil)
  require 'securerandom'

  test_path = path ? File.absolute_path(path) : Dir.pwd
  abs_path = File.absolute_path(test_path)

  # Check cache first (thread-safe read)
  cached_value = get_from_cache(abs_path)
  return cached_value unless cached_value.nil?

  # Return false if directory doesn't exist
  return false unless File.directory?(abs_path)

  result = detect_case_sensitivity?(abs_path)

  # Store result in cache (thread-safe write)
  set_in_cache(abs_path, result)

  result
rescue SystemCallError, IOError
  # Can't detect from filesystem, assume case-insensitive to be conservative
  false
end