Class: Baseh::Profile::Prepared

Inherits:
Object
  • Object
show all
Defined in:
lib/baseh/profile.rb

Overview

Immutable, fully validated profile with pre-computed derived values.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(profile) ⇒ Prepared

Returns a new instance of Prepared.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/baseh/profile.rb', line 30

def initialize(profile)
  validate_type!(profile)
  @profile_id = validate_profile_id!(profile[:profile_id])
  @case_sensitive = profile[:case_sensitive] == true

  # Spec 2.2/19.9. A persisted or frozen profile declares its mode;
  # profiles built before the mode field existed are fixed, so the
  # frozen vectors keep matching byte for byte.
  @mode = (profile[:mode] || "fixed").to_s
  unless %w[fixed expandable].include?(@mode)
    self.class.fail_profile!("mode must be fixed or expandable")
  end

  @body_alphabet = validate_body_alphabet!(profile[:body_alphabet])
  # Spec 19.2: in expandable mode the zero ban strips 0 and O from the
  # body alphabet silently, before any other validation, exactly like
  # the no-vowels strip of section 18.1.
  if @mode == "expandable"
    @body_alphabet = @body_alphabet.delete("0O").freeze
  end
  @body_length =
    if @mode == "fixed"
      validate_integer!(profile[:body_length], 1, 32, "bodyLength")
    end
  @checksum_length = validate_integer!(profile[:checksum_length], 0, 8, "checksumLength")
  @min_length = (profile[:min_length] || 4)
  @separator_min_length = (profile[:separator_min_length] || 0)
  if @mode == "fixed"
    unless @separator_min_length.is_a?(Integer) && @separator_min_length.zero?
      self.class.fail_profile!("separatorMinLength must be 0 in fixed mode")
    end
  else
    unless @min_length.is_a?(Integer) && @min_length >= 1
      self.class.fail_profile!("minLength must be an integer of at least 1")
    end
    unless @min_length > @checksum_length
      self.class.fail_profile!("minLength must be greater than checksumLength")
    end
    unless @separator_min_length.is_a?(Integer) && @separator_min_length >= 0
      self.class.fail_profile!("separatorMinLength must be an integer of at least 0")
    end
  end
  # Spec 22. The short checksum is expandable-only; 0 or absent turns
  # it off.
  @short_checksum_length = profile[:short_checksum_length] || 0
  @short_checksum_until = profile[:short_checksum_until] || 0
  # Spec 22. The window field is the switch: shortChecksumUntil of 0
  # turns the feature off (the codebase convention, like
  # maxRepetition), and the length field without a window is
  # INVALID_PROFILE. With a window set, a shortChecksumLength of 0 is
  # legal: the window's generations carry no checksum symbols at all.
  if @mode == "fixed"
    unless @short_checksum_length == 0 && @short_checksum_until == 0
      self.class.fail_profile!(
        "shortChecksumLength and shortChecksumUntil are expandable-mode only"
      )
    end
  elsif @short_checksum_until != 0
    unless @short_checksum_until.is_a?(Integer) && @short_checksum_until >= @min_length
      self.class.fail_profile!("shortChecksumUntil must be an integer of at least minLength")
    end
    # Beyond 8 the window would swallow nearly every practical code,
    # and long codes genuinely want two checksum symbols.
    if @short_checksum_until > 8
      self.class.fail_profile!("shortChecksumUntil must be at most 8")
    end
    unless @short_checksum_length.is_a?(Integer) && @short_checksum_length >= 0 &&
           @short_checksum_length < @checksum_length
      self.class.fail_profile!(
        "shortChecksumLength must be an integer from 0 through checksumLength - 1"
      )
    end
    unless @min_length > @short_checksum_length
      self.class.fail_profile!("minLength must be greater than shortChecksumLength")
    end
  elsif @short_checksum_length != 0
    self.class.fail_profile!("shortChecksumLength requires shortChecksumUntil")
  end
  # Spec 19.3: in expandable mode the checksum alphabet is derived from
  # the body alphabet after every body strip; the configured
  # checksumAlphabet is not consulted.
  @checksum_alphabet =
    if @mode == "expandable"
      "".freeze
    else
      validate_checksum_alphabet!(
        profile[:checksum_alphabet], @checksum_length
      )
    end

  # Spec 18: validation happens before the vowel strip so malformed
  # alphabets are reported as such, then no-vowels strips and
  # re-validates the result.
  @profanity_mode = validate_profanity_mode!(profile[:profanity])
  if @profanity_mode == "no-vowels"
    @body_alphabet = Profanity.strip_vowels(@body_alphabet)
    @checksum_alphabet = Profanity.strip_vowels(@checksum_alphabet)
    validate_stripped!(@body_alphabet, "body")
    if @mode == "fixed" && @checksum_length.positive?
      validate_stripped!(@checksum_alphabet, "checksum")
    end
    @body_alphabet.freeze
    @checksum_alphabet.freeze
  end
  if @mode == "expandable"
    # Spec 19.3: the checksum alphabet is derived, "0" followed by the
    # body alphabet in order, after every body strip (zero ban,
    # no-vowels) so all downstream rules see the final alphabets. The
    # configured checksumAlphabet is not consulted.
    @checksum_alphabet = ("0" + @body_alphabet).freeze
    validate_stripped!(@body_alphabet, "body")
  end
  @blocklist =
    if @profanity_mode == "blocklist"
      Profanity.effective_blocklist(profile[:profanity]).freeze
    else
      [].freeze
    end

  # Spec 21: 0 disables the filter; an active filter needs a floor of
  # 3 — banning pairs (2) would destroy roughly 9% of every generation.
  @max_repetition = profile[:max_repetition] || 0
  unless @max_repetition.is_a?(Integer) && @max_repetition >= 0 &&
         (@max_repetition.zero? || @max_repetition >= 3)
    self.class.fail_profile!("maxRepetition must be 0 (off) or an integer of at least 3")
  end

  @separator = validate_separator!(
    profile[:separator].to_s, @body_alphabet, @checksum_alphabet
  )
  @aliases = validate_aliases!(
    profile[:aliases] || {}, @body_alphabet, @checksum_alphabet, @case_sensitive
  )
  @grouping = validate_grouping!(
    profile[:grouping], @separator, @body_length, @checksum_length
  )
  @permutation = validate_permutation!(profile[:permutation] || { enabled: false })

  # Capacity is the fixed-mode A^bodyLength; meaningless in expandable
  # mode, where Baseh#capacity refuses per spec 12.3.
  @capacity = @body_alphabet.length**(@body_length || 0)
  @checksum_modulus = [@checksum_alphabet.length, 1].max**@checksum_length
  freeze
end

Instance Attribute Details

#aliasesObject (readonly)

Returns the value of attribute aliases.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def aliases
  @aliases
end

#blocklistObject (readonly)

Returns the value of attribute blocklist.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def blocklist
  @blocklist
end

#body_alphabetObject (readonly)

Returns the value of attribute body_alphabet.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def body_alphabet
  @body_alphabet
end

#body_lengthObject (readonly)

Returns the value of attribute body_length.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def body_length
  @body_length
end

#capacityObject (readonly)

Returns the value of attribute capacity.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def capacity
  @capacity
end

#case_sensitiveObject (readonly)

Returns the value of attribute case_sensitive.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def case_sensitive
  @case_sensitive
end

#checksum_alphabetObject (readonly)

Returns the value of attribute checksum_alphabet.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def checksum_alphabet
  @checksum_alphabet
end

#checksum_lengthObject (readonly)

Returns the value of attribute checksum_length.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def checksum_length
  @checksum_length
end

#checksum_modulusObject (readonly)

Returns the value of attribute checksum_modulus.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def checksum_modulus
  @checksum_modulus
end

#groupingObject (readonly)

Returns the value of attribute grouping.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def grouping
  @grouping
end

#max_repetitionObject (readonly)

Returns the value of attribute max_repetition.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def max_repetition
  @max_repetition
end

#min_lengthObject (readonly)

Returns the value of attribute min_length.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def min_length
  @min_length
end

#modeObject (readonly)

Returns the value of attribute mode.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def mode
  @mode
end

#permutationObject (readonly)

Returns the value of attribute permutation.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def permutation
  @permutation
end

#profanity_modeObject (readonly)

Returns the value of attribute profanity_mode.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def profanity_mode
  @profanity_mode
end

#profile_idObject (readonly)

Returns the value of attribute profile_id.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def profile_id
  @profile_id
end

#separatorObject (readonly)

Returns the value of attribute separator.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def separator
  @separator
end

#separator_min_lengthObject (readonly)

Returns the value of attribute separator_min_length.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def separator_min_length
  @separator_min_length
end

#short_checksum_lengthObject (readonly)

Returns the value of attribute short_checksum_length.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def short_checksum_length
  @short_checksum_length
end

#short_checksum_untilObject (readonly)

Returns the value of attribute short_checksum_until.



22
23
24
# File 'lib/baseh/profile.rb', line 22

def short_checksum_until
  @short_checksum_until
end

Class Method Details

.fail_profile!(reason) ⇒ Object

Raises:



192
193
194
# File 'lib/baseh/profile.rb', line 192

def self.fail_profile!(reason)
  raise BasehError.new("INVALID_PROFILE", "Invalid baseH profile: #{reason}", safe_for_customer: false)
end

Instance Method Details

#effective_checksum_length(length) ⇒ Object

Spec 22. The checksum length that applies to a generation of the given total length: short_checksum_length at or below short_checksum_until, checksum_length above it (and always in fixed mode). The feature is on exactly when short_checksum_until is non-zero; a short_checksum_length of 0 then means the window's generations carry no checksum symbols at all.



181
182
183
184
185
186
187
188
# File 'lib/baseh/profile.rb', line 181

def effective_checksum_length(length)
  if @mode == "expandable" && @short_checksum_until.positive? &&
     length <= @short_checksum_until
    @short_checksum_length
  else
    @checksum_length
  end
end