Class: Mbeditor::ExclusionMatcher

Inherits:
Object
  • Object
show all
Defined in:
app/services/mbeditor/exclusion_matcher.rb

Overview

Decides whether a workspace-relative path falls under a configured exclusion (Mbeditor.configuration.excluded_paths).

Comparison is normalized rather than byte-for-byte, because the filesystem the workspace lives on may resolve two different spellings of a name to the same file:

* Case. APFS/HFS+ (and NTFS) resolve "TMP/cache.txt" to the excluded
"tmp/", and ".GIT/hooks/post-checkout" to the real ".git/hooks/" — the
latter is arbitrary code execution on the next git operation. Folding is
applied only when the filesystem backing the workspace really is
case-insensitive, probed at runtime: a case-sensitive volume can be
mounted on macOS, and folding there would hide legitimately distinct
paths.

* Unicode normalization. APFS stores one file for the NFC and NFD
spellings of a name, so an NFD request path slips past an NFC pattern.
NFC folding is applied unconditionally: filesystems that do keep the two
spellings apart are rare enough, and the cost there is over-exclusion
(hiding a path) rather than the write-through this class exists to
prevent.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(patterns, root: nil) ⇒ ExclusionMatcher

root is the workspace the paths are relative to; it decides only whether case is folded. Defaults to the resolved workspace root.



88
89
90
91
# File 'app/services/mbeditor/exclusion_matcher.rb', line 88

def initialize(patterns, root: nil)
  @fold_case = self.class.case_insensitive_filesystem?(root || WorkspaceRootResolver.call)
  @patterns  = patterns.map { |pattern| normalize(pattern.to_s) }.reject(&:empty?)
end

Class Method Details

.case_insensitive_filesystem?(root) ⇒ Boolean

Whether the filesystem backing root resolves paths case-insensitively. Probed once per root, then memoized for the life of the process.

Returns:

  • (Boolean)


37
38
39
40
41
42
43
44
45
46
47
# File 'app/services/mbeditor/exclusion_matcher.rb', line 37

def case_insensitive_filesystem?(root)
  key = File.expand_path(root.to_s)

  MUTEX.synchronize do
    @probe_cache ||= {}
    return @probe_cache[key] if @probe_cache.key?(key)

    @probe_cache = {} if @probe_cache.size >= MAX_PROBE_CACHE
    @probe_cache[key] = probe_case_insensitive(key)
  end
end

.reset_filesystem_probe!Object



49
50
51
# File 'app/services/mbeditor/exclusion_matcher.rb', line 49

def reset_filesystem_probe!
  MUTEX.synchronize { @probe_cache = {} }
end

Instance Method Details

#excluded?(relative_path) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
96
# File 'app/services/mbeditor/exclusion_matcher.rb', line 93

def excluded?(relative_path)
  rel = normalize(relative_path.to_s)
  @patterns.any? { |pattern| matches?(pattern, rel) }
end