Class: Scrubber::Instance

Inherits:
Object
  • Object
show all
Defined in:
lib/scrubber/instance.rb,
sig/scrubber_rb.rbs

Overview

The compiled engine. Frozen; safe to share across threads.

Constant Summary collapse

REPLACEMENTS =

Returns:

  • (Array[Symbol])
%i[label mask hash remove].freeze
CHUNK_SIZE =

Read in 1MB slices, and never cut closer than this to the end of the buffer, so a match straddling a chunk boundary still sees both halves.

Returns:

  • (Integer)
1024 * 1024
CARRY_SIZE =

Returns:

  • (Integer)
8 * 1024

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(detectors: Scrubber::DEFAULTS, custom: {}, replacement: :label, hash_salt: nil) ⇒ Instance

Returns a new instance of Instance.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/scrubber/instance.rb', line 24

def initialize(detectors: Scrubber::DEFAULTS, custom: {}, replacement: :label, hash_salt: nil)
  @detectors = normalize_detectors(detectors)
  @custom = normalize_custom(custom)
  @replacement = normalize_replacement(replacement)
  @hash_salt = hash_salt&.to_s

  @native = Native.new(
    @detectors.map(&:to_s),
    @custom.map { |name, regexp| [name.to_s, regexp.source, regexp.options] },
    @replacement.to_s,
    @hash_salt
  )
  freeze
end

Instance Attribute Details

#customcustom_patterns (readonly)

Returns the value of attribute custom.

Returns:

  • (custom_patterns)


22
23
24
# File 'lib/scrubber/instance.rb', line 22

def custom
  @custom
end

#detectorsArray[Symbol] (readonly)

Returns the value of attribute detectors.

Returns:

  • (Array[Symbol])


22
23
24
# File 'lib/scrubber/instance.rb', line 22

def detectors
  @detectors
end

#hash_saltString? (readonly)

Returns the value of attribute hash_salt.

Returns:

  • (String, nil)


22
23
24
# File 'lib/scrubber/instance.rb', line 22

def hash_salt
  @hash_salt
end

#replacementSymbol (readonly)

Returns the value of attribute replacement.

Returns:

  • (Symbol)


22
23
24
# File 'lib/scrubber/instance.rb', line 22

def replacement
  @replacement
end

Class Method Details

.cache_key(detectors:, custom:, replacement:, hash_salt:) ⇒ Object



122
123
124
125
126
127
128
129
# File 'lib/scrubber/instance.rb', line 122

def self.cache_key(detectors:, custom:, replacement:, hash_salt:)
  customs = custom.to_a.map do |name, regexp|
    source = regexp.respond_to?(:source) ? regexp.source : regexp.to_s
    options = regexp.respond_to?(:options) ? regexp.options : 0
    [name.to_sym, source, options]
  end
  [Array(detectors).map(&:to_sym).sort, customs.sort, replacement.to_sym, hash_salt&.to_s]
end

Instance Method Details

#cache_keyArray[untyped]

Config identity, used to memoize module-level Scrubber.scrub calls.

Returns:

  • (Array[untyped])


115
116
117
118
119
120
# File 'lib/scrubber/instance.rb', line 115

def cache_key
  self.class.cache_key(
    detectors: @detectors, custom: @custom,
    replacement: @replacement, hash_salt: @hash_salt
  )
end

#detect(text) ⇒ Array[Match]

Find matches without replacing them.

Scrubber.detect("mail nik@example.com")
# => [#<Scrubber::Match type=:email 5...20 preview="n**@e******.com">]

Offsets are character offsets into text, not byte offsets, so they index the Ruby string correctly even when it contains emoji or Devanagari.

Parameters:

  • _ToStr (String)

Returns:



66
67
68
69
70
71
72
73
74
# File 'lib/scrubber/instance.rb', line 66

def detect(text)
  str = coerce(text)
  # Only UTF-8 needs byte->character conversion; in single-byte encodings
  # the two are the same number.
  char_offsets = str.encoding == Encoding::UTF_8
  @native.detect(str, char_offsets).map do |type, from, to, preview|
    Match.new(type: type.to_sym, begin: from, end: to, preview: preview)
  end
end

#inspectObject



109
110
111
112
# File 'lib/scrubber/instance.rb', line 109

def inspect
  "#<Scrubber::Instance detectors=#{@detectors.size} rules=#{rule_count} " \
    "replacement=#{@replacement.inspect}>"
end

#match?(text) ⇒ Boolean

True if anything at all would be redacted. Cheaper than scrub when you only need a yes/no (an audit check, a test assertion, a CI gate).

Parameters:

  • _ToStr (String)

Returns:

  • (Boolean)


78
79
80
81
# File 'lib/scrubber/instance.rb', line 78

def match?(text)
  # `detect` stops at the match list; it never builds the output string.
  !@native.detect(coerce(text), false).empty?
end

#rule_countInteger

How many compiled patterns back this instance. Detectors expand to more than one rule each in a few cases (:api_key alone is ~19).

Returns:

  • (Integer)


105
106
107
# File 'lib/scrubber/instance.rb', line 105

def rule_count
  @native.rule_count
end

#scrub(text) ⇒ String

Redact every match in text, returning a new String with the same encoding. The input is never mutated, and frozen input is fine.

Parameters:

  • _ToStr (String)

Returns:

  • (String)


41
42
43
44
45
46
47
# File 'lib/scrubber/instance.rb', line 41

def scrub(text)
  str = coerce(text)
  replaced = @native.scrub(str)
  return str.dup if replaced.nil?

  replaced.force_encoding(str.encoding)
end

#scrub!(text) ⇒ String

Redact in place. Returns the same object, so it raises FrozenError on a frozen string exactly like every other Ruby bang method.

Parameters:

  • (String)

Returns:

  • (String)


51
52
53
54
55
56
57
# File 'lib/scrubber/instance.rb', line 51

def scrub!(text)
  str = coerce(text)
  replaced = @native.scrub(str)
  return str if replaced.nil?

  str.replace(replaced.force_encoding(str.encoding))
end

#scrub_file(input_path, output_path, chunk_size: CHUNK_SIZE) ⇒ Integer

Stream a file through the engine.

Reads in 1MB chunks and cuts each chunk at the last line break before an 8KB carry window, so a match spanning a chunk boundary is never sliced in half. Multi-line PEM blocks get an extra guard: if a chunk would end between BEGIN and END, the cut moves back before the BEGIN.

Returns the number of bytes written.

Parameters:

  • (String, ::Pathname)
  • (String, ::Pathname)
  • chunk_size: (Integer) (defaults to: CHUNK_SIZE)

Returns:

  • (Integer)


91
92
93
94
95
96
97
98
99
100
101
# File 'lib/scrubber/instance.rb', line 91

def scrub_file(input_path, output_path, chunk_size: CHUNK_SIZE)
  written = 0
  File.open(input_path, "rb") do |input|
    File.open(output_path, "wb") do |output|
      each_safe_chunk(input, chunk_size) do |chunk|
        written += output.write(@native.scrub(chunk) || chunk)
      end
    end
  end
  written
end