# frozen_string_literal: true
module Net
class IMAP < Protocol
# Regexps and utility methods for implementing stringprep profiles. The
# \StringPrep algorithm is defined by
# {RFC-3454}[https://www.rfc-editor.org/rfc/rfc3454.html]. Each
# codepoint table defined in the RFC-3454 appendices is matched by a Regexp
# defined in this module.
module StringPrep
autoload :NamePrep, File.expand_path("stringprep/nameprep", __dir__)
autoload :SASLprep, File.expand_path("stringprep/saslprep", __dir__)
autoload :Tables, File.expand_path("stringprep/tables", __dir__)
autoload :Trace, File.expand_path("stringprep/trace", __dir__)
# ArgumentError raised when +string+ is invalid for the stringprep
# +profile+.
class StringPrepError < ArgumentError
attr_reader :string, :profile
def initialize(*args, string: nil, profile: nil)
@string = -string.to_str unless string.nil?
@profile = -profile.to_str unless profile.nil?
super(*args)
end
end
# StringPrepError raised when +string+ contains a codepoint prohibited by
# +table+.
class ProhibitedCodepoint < StringPrepError
attr_reader :table
def initialize(table, *args, **kwargs)
@table = table
details = (title = Tables::TITLES[table]) ?
"%s [%s]" % [title, table] : table
message = "String contains a prohibited codepoint: %s" % [details]
super(message, *args, **kwargs)
end
end
# StringPrepError raised when +string+ contains bidirectional characters
# which violate the StringPrep requirements.
class BidiStringError < StringPrepError
end
# Returns a Regexp matching the given +table+ name.
def self.[](table)
Tables::REGEXPS.fetch(table)
end
module_function
# >>>
# 1. Map -- For each character in the input, check if it has a mapping
# and, if so, replace it with its mapping. This is described in
# section 3.
#
# 2. Normalize -- Possibly normalize the result of step 1 using Unicode
# normalization. This is described in section 4.
#
# 3. Prohibit -- Check for any characters that are not allowed in the
# output. If any are found, return an error. This is described in
# section 5.
#
# 4. Check bidi -- Possibly check for right-to-left characters, and if
# any are found, make sure that the whole string satisfies the
# requirements for bidirectional strings. If the string does not
# satisfy the requirements for bidirectional strings, return an
# error. This is described in section 6.
#
# The above steps MUST be performed in the order given to comply with
# this specification.
#
def stringprep(string,
maps:,
normalization:,
prohibited:,
**opts)
string = string.encode("UTF-8") # also dups (and raises invalid encoding)
map_tables!(string, *maps) if maps
string.unicode_normalize!(normalization) if normalization
check_prohibited!(string, *prohibited, **opts) if prohibited
string
end
def map_tables!(string, *tables)
tables.each do |table|
regexp, replacements = Tables::MAPPINGS.fetch(table)
string.gsub!(regexp, replacements)
end
string
end
# Checks +string+ for any codepoint in +tables+. Raises a
# ProhibitedCodepoint describing the first matching table.
#
# Also checks bidirectional characters, when bidi: true, which may
# raise a BidiStringError.
#
# +profile+ is an optional string which will be added to any exception that
# is raised (it does not affect behavior).
def check_prohibited!(string,
*tables,
bidi: false,
unassigned: "A.1",
stored: false,
profile: nil)
tables = Tables::TITLES.keys.grep(/^C/) if tables.empty?
tables |= [unassigned] if stored
tables |= %w[C.8] if bidi
table = tables.find {|t|
case t
when String then Tables::REGEXPS.fetch(t).match?(string)
when Regexp then t.match?(string)
else raise ArgumentError, "only table names and regexps can be checked"
end
}
if table
raise ProhibitedCodepoint.new(
table, string: string, profile: profile
)
end
check_bidi!(string, profile: profile) if bidi
end
# Checks that +string+ obeys all of the "Bidirectional Characters"
# requirements in RFC-3454, ยง6:
#
# * The characters in \StringPrep\[\"C.8\"] MUST be prohibited
# * If a string contains any RandALCat character, the string MUST NOT
# contain any LCat character.
# * If a string contains any RandALCat character, a RandALCat
# character MUST be the first character of the string, and a
# RandALCat character MUST be the last character of the string.
#
# This is usually combined with #check_prohibited!, so table "C.8" is only
# checked when c_8: true.
#
# Raises either ProhibitedCodepoint or BidiStringError unless all
# requirements are met. +profile+ is an optional string which will be
# added to any exception that is raised (it does not affect behavior).
def check_bidi!(string, c_8: false, profile: nil)
check_prohibited!(string, "C.8", profile: profile) if c_8
if Tables::BIDI_FAILS_REQ2.match?(string)
raise BidiStringError.new(
Tables::BIDI_DESC_REQ2, string: string, profile: profile,
)
elsif Tables::BIDI_FAILS_REQ3.match?(string)
raise BidiStringError.new(
Tables::BIDI_DESC_REQ3, string: string, profile: profile,
)
end
end
end
end
end