Class: Ruby::Rego::Builtins::Codecs::JsonSchema::Formats::MailAddress

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby/rego/builtins/codecs/json_schema_email.rb

Overview

A port of Go’s net/mail ParseAddress (src/net/mail/message.go, go1.26.4 — the Go OPA 1.17.1 is built with), reduced to the boolean gojsonschema’s email/idn-email checkers need: does the value parse as exactly one RFC 5322 address. There is no Ruby gem matching Go’s specific acceptance boundary, so this is hand-rolled and differentially verified against opa eval.

Faithful to Go’s quirks: a single-member group g: a@b; parses (parseSingleAddress) but a multi-member one does not; a display name + angle-addr Foo <a@b> parses; comments are CFWS only as a trailing run or via skipCFWS, so a LEADING comment fails while a trailing one passes; the local-part/@ boundary skips plain space but not comments (a (c) @b fails); a domain-literal [x] requires net.ParseIP(x) (so [127.0.0.1] and [::1] pass but [IPv6:::1] fails); atext/qtext/dtext allow any rune >= U+0080 (RFC 6532). RFC 2047 encoded-word decoding in phrases IS replicated to the extent it affects acceptance (Rfc2047.status + consume_phrase): Go’s consumePhrase runs mime.WordDecoder.Decode on each atom word. A =?charset?enc?text?= whose payload decodes but whose charset is not utf-8/iso-8859-1/us-ascii makes Decode error (rejecting as the first word, else truncating); an empty payload decodes to “” and is dropped; a malformed or undecodable word is kept as raw text. Go 1.26 also buffers a run of consecutive encoded-words and only flushes them to its words slice on a following raw word, so a comment after a lone encoded-word run ends the phrase (the CFWS skip is gated on len(words)). Verified differentially against opa across the cross-product (19000+ encoded-word cases).

The input is required UTF-8-valid by the caller (Formats#email?), so Go’s invalid-UTF-8 error paths can’t fire mid-parse. Parsing walks a char-array index (see #initialize) so it stays linear on multibyte input, not string re-slicing or O(@pos) codepoint indexing.

rubocop:disable Metrics/ClassLength, Naming/PredicateMethod – a faithful recursive-descent port; the consume_/skip_ methods mutate @pos and return success bools (not pure predicates).

Constant Summary collapse

SPECIALS =
["(", ")", "<", ">", "[", "]", ":", ";", "@", "\\", ",", "\""].freeze
WSP =

Frozen so the per-character predicates scan a shared array rather than allocating a literal each call (a hot-path GC concern on large inputs).

[" ", "\t"].freeze
QTEXT_EXCLUDED =

not qtext: backslash, double-quote

["\\", "\""].freeze
DTEXT_EXCLUDED =

not dtext: brackets, backslash

["[", "]", "\\"].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(string) ⇒ MailAddress

Returns a new instance of MailAddress.



50
51
52
53
54
55
56
57
# File 'lib/ruby/rego/builtins/codecs/json_schema_email.rb', line 50

def initialize(string)
  # Index a char ARRAY, not the String: CRuby's String#[] by codepoint is O(@pos) for a
  # non-ASCII (multibyte) string, which would make the scan loops O(n²) — a DoS on the RFC 6532
  # UTF-8 input that is explicitly in scope. Array#[] is O(1), so the whole parse stays linear.
  @chars = string.chars
  @pos = 0
  @len = @chars.length
end

Class Method Details

.valid?(string) ⇒ bool

Returns whether string is exactly one Go-parseable address.

Returns:

  • (bool)

    whether string is exactly one Go-parseable address.



46
47
48
# File 'lib/ruby/rego/builtins/codecs/json_schema_email.rb', line 46

def self.valid?(string)
  new(string).single_address?
end

Instance Method Details

#single_address?Boolean

Port of parseSingleAddress: one address, then trailing CFWS, then nothing left; a group must have exactly one member. :reek:NilCheck

Returns:

  • (Boolean)


62
63
64
65
66
67
68
69
# File 'lib/ruby/rego/builtins/codecs/json_schema_email.rb', line 62

def single_address?
  count = parse_address(handle_group: true)
  return false if count.nil?
  return false unless skip_cfws
  return false unless empty?

  count == 1
end