Skip to content

Commit 127af38

Browse files
committed
🚚 Move the StringPrep module out of SASL
Also move StringPrepError classes and SASLprep into StringPrep module.
1 parent dcc36a8 commit 127af38

File tree

12 files changed

+168
-134
lines changed

12 files changed

+168
-134
lines changed

benchmarks/stringprep.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ prelude: |
2424
$LOAD_PATH.unshift "./lib"
2525
require "net/imap"
2626
def net_imap_saslprep(string)
27-
Net::IMAP::SASL::SASLprep.saslprep string, exception: false
27+
Net::IMAP::StringPrep::SASLprep.saslprep string, exception: false
2828
end
2929
3030
def libidn_saslprep(string)

lib/net/imap.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,9 @@ module Net
703703
class IMAP < Protocol
704704
VERSION = "0.3.4"
705705

706+
autoload :SASL, File.expand_path("imap/sasl", __dir__)
707+
autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
708+
706709
include MonitorMixin
707710
if defined?(OpenSSL::SSL)
708711
include OpenSSL
@@ -2485,6 +2488,16 @@ def start_tls_session(params = {})
24852488
end
24862489
end
24872490

2491+
#--
2492+
# We could get the saslprep method by extending the SASLprep module
2493+
# directly. It's done indirectly, so SASLprep can be lazily autoloaded,
2494+
# because most users won't need it.
2495+
#++
2496+
# Delegates to Net::IMAP::StringPrep::SASLprep#saslprep.
2497+
def self.saslprep(string, **opts)
2498+
Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts)
2499+
end
2500+
24882501
end
24892502
end
24902503

@@ -2495,4 +2508,3 @@ def start_tls_session(params = {})
24952508
require_relative "imap/response_data"
24962509
require_relative "imap/response_parser"
24972510
require_relative "imap/authenticators"
2498-
require_relative "imap/sasl"

lib/net/imap/sasl.rb

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,54 +25,21 @@ module SASL
2525

2626
# autoloading to avoid loading all of the regexps when they aren't used.
2727

28-
autoload :StringPrep, File.expand_path("sasl/stringprep", __dir__)
29-
autoload :SASLprep, File.expand_path("#{__dir__}/sasl/saslprep", __dir__)
28+
sasl_stringprep_rb = File.expand_path("sasl/stringprep", __dir__)
29+
autoload :StringPrep, sasl_stringprep_rb
30+
autoload :SASLprep, sasl_stringprep_rb
31+
autoload :StringPrepError, sasl_stringprep_rb
32+
autoload :ProhibitedCodepoint, sasl_stringprep_rb
33+
autoload :BidiStringError, sasl_stringprep_rb
3034

31-
# ArgumentError raised when +string+ is invalid for the stringprep
32-
# +profile+.
33-
class StringPrepError < ArgumentError
34-
attr_reader :string, :profile
35+
module_function
3536

36-
def initialize(*args, string: nil, profile: nil)
37-
@string = -string.to_str unless string.nil?
38-
@profile = -profile.to_str unless profile.nil?
39-
super(*args)
40-
end
41-
end
42-
43-
# StringPrepError raised when +string+ contains a codepoint prohibited by
44-
# +table+.
45-
class ProhibitedCodepoint < StringPrepError
46-
attr_reader :table
47-
48-
def initialize(table, *args, **kwargs)
49-
@table = -table.to_str
50-
details = (title = StringPrep::TABLE_TITLES[table]) ?
51-
"%s [%s]" % [title, table] : table
52-
message = "String contains a prohibited codepoint: %s" % [details]
53-
super(message, *args, **kwargs)
54-
end
55-
end
56-
57-
# StringPrepError raised when +string+ contains bidirectional characters
58-
# which violate the StringPrep requirements.
59-
class BidiStringError < StringPrepError
60-
end
61-
62-
#--
63-
# We could just extend SASLprep module directly. It's done this way so
64-
# SASLprep can be lazily autoloaded. Most users won't need it.
65-
#++
66-
extend self
67-
68-
# See SASLprep#saslprep.
37+
# See Net::IMAP::StringPrep::SASLprep#saslprep.
6938
def saslprep(string, **opts)
70-
SASLprep.saslprep(string, **opts)
39+
Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts)
7140
end
7241

7342
end
7443
end
7544

7645
end
77-
78-
Net::IMAP.extend Net::IMAP::SASL

lib/net/imap/sasl/stringprep.rb

Lines changed: 6 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,12 @@
11
# frozen_string_literal: true
22

3-
require_relative "stringprep_tables"
4-
53
module Net::IMAP::SASL
64

7-
# Regexps and utility methods for implementing stringprep profiles. The
8-
# \StringPrep algorithm is defined by
9-
# {RFC-3454}[https://www.rfc-editor.org/rfc/rfc3454.html]. Each
10-
# codepoint table defined in the RFC-3454 appendices is matched by a Regexp
11-
# defined in this module.
12-
#--
13-
# TODO: generic StringPrep mapping (not needed for SASLprep implementation)
14-
#++
15-
module StringPrep
16-
17-
# Returns a Regexp matching the given +table+ name.
18-
def self.[](table)
19-
TABLE_REGEXPS.fetch(table)
20-
end
21-
22-
module_function
23-
24-
# Checks +string+ for any codepoint in +tables+. Raises a
25-
# ProhibitedCodepoint describing the first matching table.
26-
#
27-
# Also checks bidirectional characters, when <tt>bidi: true</tt>, which may
28-
# raise a BidiStringError.
29-
#
30-
# +profile+ is an optional string which will be added to any exception that
31-
# is raised (it does not affect behavior).
32-
def check_prohibited!(string, *tables, bidi: false, profile: nil)
33-
tables = TABLE_TITLES.keys.grep(/^C/) if tables.empty?
34-
tables |= %w[C.8] if bidi
35-
table = tables.find {|t| TABLE_REGEXPS[t].match?(string) }
36-
raise ProhibitedCodepoint.new(
37-
table, string: string, profile: nil
38-
) if table
39-
check_bidi!(string, profile: profile) if bidi
40-
end
41-
42-
# Checks that +string+ obeys all of the "Bidirectional Characters"
43-
# requirements in RFC-3454, §6:
44-
#
45-
# * The characters in \StringPrep\[\"C.8\"] MUST be prohibited
46-
# * If a string contains any RandALCat character, the string MUST NOT
47-
# contain any LCat character.
48-
# * If a string contains any RandALCat character, a RandALCat
49-
# character MUST be the first character of the string, and a
50-
# RandALCat character MUST be the last character of the string.
51-
#
52-
# This is usually combined with #check_prohibited!, so table "C.8" is only
53-
# checked when <tt>c_8: true</tt>.
54-
#
55-
# Raises either ProhibitedCodepoint or BidiStringError unless all
56-
# requirements are met. +profile+ is an optional string which will be
57-
# added to any exception that is raised (it does not affect behavior).
58-
def check_bidi!(string, c_8: false, profile: nil)
59-
check_prohibited!(string, "C.8", profile: profile) if c_8
60-
if BIDI_FAILS_REQ2.match?(string)
61-
raise BidiStringError.new(
62-
BIDI_DESC_REQ2, string: string, profile: profile,
63-
)
64-
elsif BIDI_FAILS_REQ3.match?(string)
65-
raise BidiStringError.new(
66-
BIDI_DESC_REQ3, string: string, profile: profile,
67-
)
68-
end
69-
end
5+
# Alias for Net::IMAP::StringPrep::SASLPrep.
6+
SASLprep = Net::IMAP::StringPrep::SASLprep
7+
StringPrep = Net::IMAP::StringPrep # :nodoc:
8+
BidiStringError = Net::IMAP::StringPrep::BidiStringError # :nodoc:
9+
ProhibitedCodepoint = Net::IMAP::StringPrep::ProhibitedCodepoint # :nodoc:
10+
StringPrepError = Net::IMAP::StringPrep::StringPrepError # :nodoc:
7011

71-
end
7212
end

lib/net/imap/stringprep.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# frozen_string_literal: true
2+
3+
module Net
4+
class IMAP < Protocol
5+
6+
# Regexps and utility methods for implementing stringprep profiles. The
7+
# \StringPrep algorithm is defined by
8+
# {RFC-3454}[https://www.rfc-editor.org/rfc/rfc3454.html]. Each
9+
# codepoint table defined in the RFC-3454 appendices is matched by a Regexp
10+
# defined in this module.
11+
module StringPrep
12+
autoload :SASLprep, File.expand_path("stringprep/saslprep", __dir__)
13+
autoload :Tables, File.expand_path("stringprep/tables", __dir__)
14+
15+
# ArgumentError raised when +string+ is invalid for the stringprep
16+
# +profile+.
17+
class StringPrepError < ArgumentError
18+
attr_reader :string, :profile
19+
20+
def initialize(*args, string: nil, profile: nil)
21+
@string = -string.to_str unless string.nil?
22+
@profile = -profile.to_str unless profile.nil?
23+
super(*args)
24+
end
25+
end
26+
27+
# StringPrepError raised when +string+ contains a codepoint prohibited by
28+
# +table+.
29+
class ProhibitedCodepoint < StringPrepError
30+
attr_reader :table
31+
32+
def initialize(table, *args, **kwargs)
33+
@table = table
34+
details = (title = Tables::TITLES[table]) ?
35+
"%s [%s]" % [title, table] : table
36+
message = "String contains a prohibited codepoint: %s" % [details]
37+
super(message, *args, **kwargs)
38+
end
39+
end
40+
41+
# StringPrepError raised when +string+ contains bidirectional characters
42+
# which violate the StringPrep requirements.
43+
class BidiStringError < StringPrepError
44+
end
45+
46+
# Returns a Regexp matching the given +table+ name.
47+
def self.[](table)
48+
Tables::REGEXPS.fetch(table)
49+
end
50+
51+
module_function
52+
53+
# Checks +string+ for any codepoint in +tables+. Raises a
54+
# ProhibitedCodepoint describing the first matching table.
55+
#
56+
# Also checks bidirectional characters, when <tt>bidi: true</tt>, which may
57+
# raise a BidiStringError.
58+
#
59+
# +profile+ is an optional string which will be added to any exception that
60+
# is raised (it does not affect behavior).
61+
def check_prohibited!(string, *tables, bidi: false, profile: nil)
62+
tables = TABLE_TITLES.keys.grep(/^C/) if tables.empty?
63+
tables |= %w[C.8] if bidi
64+
table = tables.find {|t| Tables::REGEXPS[t].match?(string) }
65+
raise ProhibitedCodepoint.new(
66+
table, string: string, profile: nil
67+
) if table
68+
check_bidi!(string, profile: profile) if bidi
69+
end
70+
71+
# Checks that +string+ obeys all of the "Bidirectional Characters"
72+
# requirements in RFC-3454, §6:
73+
#
74+
# * The characters in \StringPrep\[\"C.8\"] MUST be prohibited
75+
# * If a string contains any RandALCat character, the string MUST NOT
76+
# contain any LCat character.
77+
# * If a string contains any RandALCat character, a RandALCat
78+
# character MUST be the first character of the string, and a
79+
# RandALCat character MUST be the last character of the string.
80+
#
81+
# This is usually combined with #check_prohibited!, so table "C.8" is only
82+
# checked when <tt>c_8: true</tt>.
83+
#
84+
# Raises either ProhibitedCodepoint or BidiStringError unless all
85+
# requirements are met. +profile+ is an optional string which will be
86+
# added to any exception that is raised (it does not affect behavior).
87+
def check_bidi!(string, c_8: false, profile: nil)
88+
check_prohibited!(string, "C.8", profile: profile) if c_8
89+
if Tables::BIDI_FAILS_REQ2.match?(string)
90+
raise BidiStringError.new(
91+
Tables::BIDI_DESC_REQ2, string: string, profile: profile,
92+
)
93+
elsif Tables::BIDI_FAILS_REQ3.match?(string)
94+
raise BidiStringError.new(
95+
Tables::BIDI_DESC_REQ3, string: string, profile: profile,
96+
)
97+
end
98+
end
99+
100+
end
101+
end
102+
end

lib/net/imap/sasl/saslprep.rb renamed to lib/net/imap/stringprep/saslprep.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22

3-
require_relative "saslprep_tables"
4-
5-
module Net::IMAP::SASL
3+
module Net
4+
class IMAP
5+
module StringPrep
66

77
# SASLprep#saslprep can be used to prepare a string according to [RFC4013].
88
#
@@ -16,6 +16,16 @@ module SASLprep
1616
# Used to short-circuit strings that don't need preparation.
1717
ASCII_NO_CTRLS = /\A[\x20-\x7e]*\z/u.freeze
1818

19+
# Avoid loading these tables unless they are needed (they are only
20+
# needed for non-ASCII).
21+
saslprep_tables = File.expand_path("saslprep_tables", __dir__)
22+
autoload :MAP_TO_NOTHING, saslprep_tables
23+
autoload :MAP_TO_SPACE, saslprep_tables
24+
autoload :PROHIBITED, saslprep_tables
25+
autoload :PROHIBITED_STORED, saslprep_tables
26+
autoload :TABLES_PROHIBITED, saslprep_tables
27+
autoload :TABLES_PROHIBITED_STORED, saslprep_tables
28+
1929
module_function
2030

2131
# Prepares a UTF-8 +string+ for comparison, using the \SASLprep profile
@@ -40,7 +50,7 @@ def saslprep(str, stored: false, exception: false)
4050
# raise helpful errors to indicate *why* it failed:
4151
tables = stored ? TABLES_PROHIBITED_STORED : TABLES_PROHIBITED
4252
StringPrep.check_prohibited! str, *tables, bidi: true, profile: "SASLprep"
43-
raise StringPrep::InvalidStringError.new(
53+
raise InvalidStringError.new(
4454
"unknown error", string: string, profile: "SASLprep"
4555
)
4656
rescue ArgumentError, Encoding::CompatibilityError => ex
@@ -52,4 +62,7 @@ def saslprep(str, stored: false, exception: false)
5262
end
5363

5464
end
65+
66+
end
67+
end
5568
end

lib/net/imap/sasl/saslprep_tables.rb renamed to lib/net/imap/stringprep/saslprep_tables.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# This file is generated from RFC3454, by rake. Don't edit directly.
55
#++
66

7-
module Net::IMAP::SASL
7+
module Net::IMAP::StringPrep
88

99
module SASLprep
1010

lib/net/imap/sasl/stringprep_tables.rb renamed to lib/net/imap/stringprep/tables.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
# This file is generated from RFC3454, by rake. Don't edit directly.
55
#++
66

7-
module Net::IMAP::SASL
7+
module Net::IMAP::StringPrep
88

9-
module StringPrep
9+
module Tables
1010

1111
# Unassigned code points in Unicode 3.2 \StringPrep\[\"A.1\"]
1212
IN_A_1 = /\p{^AGE=3.2}/.freeze
@@ -106,7 +106,7 @@ module StringPrep
106106
)/mx.freeze
107107

108108
# Names of each codepoint table in the RFC-3454 appendices
109-
TABLE_TITLES = {
109+
TITLES = {
110110
"A.1" => "Unassigned code points in Unicode 3.2",
111111
"B.1" => "Commonly mapped to nothing",
112112
"B.2" => "Mapping for case-folding used with NFKC",
@@ -129,7 +129,7 @@ module StringPrep
129129
}.freeze
130130

131131
# Regexps matching each codepoint table in the RFC-3454 appendices
132-
TABLE_REGEXPS = {
132+
REGEXPS = {
133133
"A.1" => IN_A_1,
134134
"B.1" => IN_B_1,
135135
"B.2" => IN_B_2,

rakelib/saslprep.rake

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ end
1010

1111
directory "lib/net/imap/sasl"
1212

13-
file "lib/net/imap/sasl/stringprep_tables.rb" => generator.rb_deps do |t|
13+
file "lib/net/imap/stringprep/tables.rb" => generator.rb_deps do |t|
1414
File.write t.name, generator.stringprep_rb
1515
end
1616

17-
file "lib/net/imap/sasl/saslprep_tables.rb" => generator.rb_deps do |t|
17+
file "lib/net/imap/stringprep/saslprep_tables.rb" => generator.rb_deps do |t|
1818
File.write t.name, generator.saslprep_rb
1919
end
2020

2121
GENERATED_RUBY = FileList.new(
22-
"lib/net/imap/sasl/stringprep_tables.rb",
23-
"lib/net/imap/sasl/saslprep_tables.rb",
22+
"lib/net/imap/stringprep/tables.rb",
23+
"lib/net/imap/stringprep/saslprep_tables.rb",
2424
)
2525

2626
CLEAN.include generator.clean_deps

0 commit comments

Comments
 (0)