activesupport 6.1.0 → 7.1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1075 -325
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -7
  5. data/lib/active_support/actionable_error.rb +4 -2
  6. data/lib/active_support/array_inquirer.rb +2 -2
  7. data/lib/active_support/backtrace_cleaner.rb +32 -7
  8. data/lib/active_support/benchmarkable.rb +3 -2
  9. data/lib/active_support/broadcast_logger.rb +251 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +53 -20
  14. data/lib/active_support/cache/mem_cache_store.rb +201 -62
  15. data/lib/active_support/cache/memory_store.rb +86 -24
  16. data/lib/active_support/cache/null_store.rb +16 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +186 -193
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +63 -71
  20. data/lib/active_support/cache.rb +487 -249
  21. data/lib/active_support/callbacks.rb +227 -105
  22. data/lib/active_support/code_generator.rb +70 -0
  23. data/lib/active_support/concern.rb +9 -7
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +44 -7
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/concurrency/share_lock.rb +2 -2
  27. data/lib/active_support/configurable.rb +18 -5
  28. data/lib/active_support/configuration_file.rb +7 -2
  29. data/lib/active_support/core_ext/array/access.rb +1 -5
  30. data/lib/active_support/core_ext/array/conversions.rb +15 -13
  31. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  32. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  33. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  34. data/lib/active_support/core_ext/class/subclasses.rb +37 -26
  35. data/lib/active_support/core_ext/date/blank.rb +1 -1
  36. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  37. data/lib/active_support/core_ext/date/conversions.rb +16 -15
  38. data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
  39. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  40. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  41. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  42. data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
  43. data/lib/active_support/core_ext/digest/uuid.rb +30 -13
  44. data/lib/active_support/core_ext/enumerable.rb +85 -83
  45. data/lib/active_support/core_ext/erb/util.rb +196 -0
  46. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  47. data/lib/active_support/core_ext/hash/conversions.rb +1 -2
  48. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  49. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  50. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  51. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  52. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  53. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  54. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  55. data/lib/active_support/core_ext/module/attribute_accessors.rb +8 -0
  56. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +49 -22
  57. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  58. data/lib/active_support/core_ext/module/delegation.rb +81 -43
  59. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  60. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  61. data/lib/active_support/core_ext/name_error.rb +2 -8
  62. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  63. data/lib/active_support/core_ext/numeric/conversions.rb +82 -77
  64. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  65. data/lib/active_support/core_ext/object/blank.rb +2 -2
  66. data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
  67. data/lib/active_support/core_ext/object/duplicable.rb +31 -11
  68. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  69. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  70. data/lib/active_support/core_ext/object/json.rb +49 -27
  71. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  72. data/lib/active_support/core_ext/object/try.rb +20 -20
  73. data/lib/active_support/core_ext/object/with.rb +44 -0
  74. data/lib/active_support/core_ext/object/with_options.rb +25 -6
  75. data/lib/active_support/core_ext/object.rb +1 -0
  76. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  77. data/lib/active_support/core_ext/pathname/existence.rb +23 -0
  78. data/lib/active_support/core_ext/pathname.rb +4 -0
  79. data/lib/active_support/core_ext/range/compare_range.rb +0 -25
  80. data/lib/active_support/core_ext/range/conversions.rb +34 -13
  81. data/lib/active_support/core_ext/range/each.rb +1 -1
  82. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  83. data/lib/active_support/core_ext/range.rb +1 -2
  84. data/lib/active_support/core_ext/securerandom.rb +25 -13
  85. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  86. data/lib/active_support/core_ext/string/filters.rb +21 -15
  87. data/lib/active_support/core_ext/string/indent.rb +1 -1
  88. data/lib/active_support/core_ext/string/inflections.rb +17 -10
  89. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  90. data/lib/active_support/core_ext/string/output_safety.rb +85 -165
  91. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
  92. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  93. data/lib/active_support/core_ext/time/calculations.rb +30 -8
  94. data/lib/active_support/core_ext/time/conversions.rb +15 -13
  95. data/lib/active_support/core_ext/time/zones.rb +12 -28
  96. data/lib/active_support/core_ext.rb +2 -1
  97. data/lib/active_support/current_attributes.rb +47 -20
  98. data/lib/active_support/deep_mergeable.rb +53 -0
  99. data/lib/active_support/dependencies/autoload.rb +17 -12
  100. data/lib/active_support/dependencies/interlock.rb +10 -18
  101. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  102. data/lib/active_support/dependencies.rb +58 -788
  103. data/lib/active_support/deprecation/behaviors.rb +66 -40
  104. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  105. data/lib/active_support/deprecation/deprecators.rb +104 -0
  106. data/lib/active_support/deprecation/disallowed.rb +6 -8
  107. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  108. data/lib/active_support/deprecation/method_wrappers.rb +9 -26
  109. data/lib/active_support/deprecation/proxy_wrappers.rb +38 -23
  110. data/lib/active_support/deprecation/reporting.rb +43 -26
  111. data/lib/active_support/deprecation.rb +32 -5
  112. data/lib/active_support/deprecator.rb +7 -0
  113. data/lib/active_support/descendants_tracker.rb +150 -72
  114. data/lib/active_support/digest.rb +5 -3
  115. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  116. data/lib/active_support/duration/iso8601_serializer.rb +9 -3
  117. data/lib/active_support/duration.rb +83 -52
  118. data/lib/active_support/encrypted_configuration.rb +72 -9
  119. data/lib/active_support/encrypted_file.rb +29 -13
  120. data/lib/active_support/environment_inquirer.rb +23 -3
  121. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  122. data/lib/active_support/error_reporter.rb +203 -0
  123. data/lib/active_support/evented_file_update_checker.rb +20 -7
  124. data/lib/active_support/execution_context/test_helper.rb +13 -0
  125. data/lib/active_support/execution_context.rb +53 -0
  126. data/lib/active_support/execution_wrapper.rb +44 -22
  127. data/lib/active_support/executor/test_helper.rb +7 -0
  128. data/lib/active_support/file_update_checker.rb +4 -2
  129. data/lib/active_support/fork_tracker.rb +28 -11
  130. data/lib/active_support/gem_version.rb +4 -4
  131. data/lib/active_support/gzip.rb +2 -0
  132. data/lib/active_support/hash_with_indifferent_access.rb +44 -19
  133. data/lib/active_support/html_safe_translation.rb +53 -0
  134. data/lib/active_support/i18n.rb +2 -1
  135. data/lib/active_support/i18n_railtie.rb +21 -14
  136. data/lib/active_support/inflector/inflections.rb +25 -7
  137. data/lib/active_support/inflector/methods.rb +50 -64
  138. data/lib/active_support/inflector/transliterate.rb +4 -2
  139. data/lib/active_support/isolated_execution_state.rb +76 -0
  140. data/lib/active_support/json/decoding.rb +2 -1
  141. data/lib/active_support/json/encoding.rb +27 -45
  142. data/lib/active_support/key_generator.rb +31 -6
  143. data/lib/active_support/lazy_load_hooks.rb +33 -7
  144. data/lib/active_support/locale/en.yml +4 -2
  145. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  146. data/lib/active_support/log_subscriber.rb +97 -35
  147. data/lib/active_support/logger.rb +9 -60
  148. data/lib/active_support/logger_thread_safe_level.rb +11 -34
  149. data/lib/active_support/message_encryptor.rb +206 -56
  150. data/lib/active_support/message_encryptors.rb +141 -0
  151. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  152. data/lib/active_support/message_pack/extensions.rb +292 -0
  153. data/lib/active_support/message_pack/serializer.rb +63 -0
  154. data/lib/active_support/message_pack.rb +50 -0
  155. data/lib/active_support/message_verifier.rb +235 -84
  156. data/lib/active_support/message_verifiers.rb +135 -0
  157. data/lib/active_support/messages/codec.rb +65 -0
  158. data/lib/active_support/messages/metadata.rb +112 -46
  159. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  160. data/lib/active_support/messages/rotator.rb +34 -32
  161. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  162. data/lib/active_support/multibyte/chars.rb +12 -11
  163. data/lib/active_support/multibyte/unicode.rb +9 -49
  164. data/lib/active_support/multibyte.rb +1 -1
  165. data/lib/active_support/notifications/fanout.rb +304 -114
  166. data/lib/active_support/notifications/instrumenter.rb +117 -35
  167. data/lib/active_support/notifications.rb +25 -25
  168. data/lib/active_support/number_helper/number_converter.rb +14 -7
  169. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  170. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  171. data/lib/active_support/number_helper/number_to_human_size_converter.rb +4 -4
  172. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  173. data/lib/active_support/number_helper/number_to_rounded_converter.rb +10 -6
  174. data/lib/active_support/number_helper/rounding_helper.rb +2 -6
  175. data/lib/active_support/number_helper.rb +379 -319
  176. data/lib/active_support/option_merger.rb +10 -18
  177. data/lib/active_support/ordered_hash.rb +4 -4
  178. data/lib/active_support/ordered_options.rb +15 -1
  179. data/lib/active_support/parameter_filter.rb +105 -81
  180. data/lib/active_support/proxy_object.rb +2 -0
  181. data/lib/active_support/railtie.rb +83 -21
  182. data/lib/active_support/reloader.rb +13 -5
  183. data/lib/active_support/rescuable.rb +18 -16
  184. data/lib/active_support/ruby_features.rb +7 -0
  185. data/lib/active_support/secure_compare_rotator.rb +18 -11
  186. data/lib/active_support/security_utils.rb +1 -1
  187. data/lib/active_support/string_inquirer.rb +3 -3
  188. data/lib/active_support/subscriber.rb +11 -40
  189. data/lib/active_support/syntax_error_proxy.rb +60 -0
  190. data/lib/active_support/tagged_logging.rb +65 -25
  191. data/lib/active_support/test_case.rb +166 -27
  192. data/lib/active_support/testing/assertions.rb +61 -15
  193. data/lib/active_support/testing/autorun.rb +0 -2
  194. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  195. data/lib/active_support/testing/deprecation.rb +53 -2
  196. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  197. data/lib/active_support/testing/isolation.rb +30 -29
  198. data/lib/active_support/testing/method_call_assertions.rb +24 -11
  199. data/lib/active_support/testing/parallelization/server.rb +4 -0
  200. data/lib/active_support/testing/parallelization/worker.rb +3 -0
  201. data/lib/active_support/testing/parallelization.rb +4 -0
  202. data/lib/active_support/testing/parallelize_executor.rb +81 -0
  203. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  204. data/lib/active_support/testing/stream.rb +4 -6
  205. data/lib/active_support/testing/strict_warnings.rb +39 -0
  206. data/lib/active_support/testing/tagged_logging.rb +1 -1
  207. data/lib/active_support/testing/time_helpers.rb +49 -16
  208. data/lib/active_support/time_with_zone.rb +39 -28
  209. data/lib/active_support/values/time_zone.rb +50 -18
  210. data/lib/active_support/version.rb +1 -1
  211. data/lib/active_support/xml_mini/jdom.rb +4 -11
  212. data/lib/active_support/xml_mini/libxml.rb +5 -5
  213. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  214. data/lib/active_support/xml_mini/nokogiri.rb +5 -5
  215. data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
  216. data/lib/active_support/xml_mini/rexml.rb +2 -2
  217. data/lib/active_support/xml_mini.rb +7 -6
  218. data/lib/active_support.rb +28 -1
  219. metadata +150 -18
  220. data/lib/active_support/core_ext/marshal.rb +0 -26
  221. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -28
  222. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  223. data/lib/active_support/core_ext/uri.rb +0 -29
  224. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
  225. data/lib/active_support/per_thread_registry.rb +0 -60
@@ -1,80 +1,146 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "time"
4
+ require "active_support/json"
5
+ require_relative "serializer_with_fallback"
4
6
 
5
7
  module ActiveSupport
6
- module Messages #:nodoc:
7
- class Metadata #:nodoc:
8
- def initialize(message, expires_at = nil, purpose = nil)
9
- @message, @purpose = message, purpose
10
- @expires_at = expires_at.is_a?(String) ? parse_expires_at(expires_at) : expires_at
11
- end
8
+ module Messages # :nodoc:
9
+ module Metadata # :nodoc:
10
+ singleton_class.attr_accessor :use_message_serializer_for_metadata
11
+
12
+ ENVELOPE_SERIALIZERS = [
13
+ *SerializerWithFallback::SERIALIZERS.values,
14
+ ActiveSupport::JSON,
15
+ ::JSON,
16
+ Marshal,
17
+ ]
18
+
19
+ TIMESTAMP_SERIALIZERS = [
20
+ SerializerWithFallback::SERIALIZERS.fetch(:message_pack),
21
+ SerializerWithFallback::SERIALIZERS.fetch(:message_pack_allow_marshal),
22
+ ]
12
23
 
13
- def as_json(options = {})
14
- { _rails: { message: @message, exp: @expires_at, pur: @purpose } }
24
+ ActiveSupport.on_load(:message_pack) do
25
+ ENVELOPE_SERIALIZERS << ActiveSupport::MessagePack
26
+ TIMESTAMP_SERIALIZERS << ActiveSupport::MessagePack
15
27
  end
16
28
 
17
- class << self
18
- def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
19
- if expires_at || expires_in || purpose
20
- JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
29
+ private
30
+ def serialize_with_metadata(data, **metadata)
31
+ has_metadata = metadata.any? { |k, v| v }
32
+
33
+ if has_metadata && !use_message_serializer_for_metadata?
34
+ data_string = serialize_to_json_safe_string(data)
35
+ envelope = wrap_in_metadata_legacy_envelope({ "message" => data_string }, **metadata)
36
+ serialize_to_json(envelope)
21
37
  else
22
- message
38
+ data = wrap_in_metadata_envelope({ "data" => data }, **metadata) if has_metadata
39
+ serialize(data)
23
40
  end
24
41
  end
25
42
 
26
- def verify(message, purpose)
27
- extract_metadata(message).verify(purpose)
28
- end
29
-
30
- private
31
- def pick_expiry(expires_at, expires_in)
32
- if expires_at
33
- expires_at.utc.iso8601(3)
34
- elsif expires_in
35
- Time.now.utc.advance(seconds: expires_in).iso8601(3)
43
+ def deserialize_with_metadata(message, **expected_metadata)
44
+ if dual_serialized_metadata_envelope_json?(message)
45
+ envelope = deserialize_from_json(message)
46
+ extracted = extract_from_metadata_envelope(envelope, **expected_metadata)
47
+ deserialize_from_json_safe_string(extracted["message"])
48
+ else
49
+ deserialized = deserialize(message)
50
+ if metadata_envelope?(deserialized)
51
+ extract_from_metadata_envelope(deserialized, **expected_metadata)["data"]
52
+ elsif expected_metadata.none? { |k, v| v }
53
+ deserialized
54
+ else
55
+ throw :invalid_message_content, "missing metadata"
36
56
  end
37
57
  end
58
+ end
38
59
 
39
- def extract_metadata(message)
40
- data = JSON.decode(message) rescue nil
60
+ def use_message_serializer_for_metadata?
61
+ Metadata.use_message_serializer_for_metadata && Metadata::ENVELOPE_SERIALIZERS.include?(serializer)
62
+ end
41
63
 
42
- if data.is_a?(Hash) && data.key?("_rails")
43
- new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
44
- else
45
- new(message)
46
- end
47
- end
64
+ def wrap_in_metadata_envelope(hash, expires_at: nil, expires_in: nil, purpose: nil)
65
+ expiry = pick_expiry(expires_at, expires_in)
66
+ hash["exp"] = expiry if expiry
67
+ hash["pur"] = purpose.to_s if purpose
68
+ { "_rails" => hash }
69
+ end
70
+
71
+ def wrap_in_metadata_legacy_envelope(hash, expires_at: nil, expires_in: nil, purpose: nil)
72
+ expiry = pick_expiry(expires_at, expires_in)
73
+ hash["exp"] = expiry
74
+ hash["pur"] = purpose
75
+ { "_rails" => hash }
76
+ end
77
+
78
+ def extract_from_metadata_envelope(envelope, purpose: nil)
79
+ hash = envelope["_rails"]
48
80
 
49
- def encode(message)
50
- ::Base64.strict_encode64(message)
81
+ if hash["exp"] && Time.now.utc >= parse_expiry(hash["exp"])
82
+ throw :invalid_message_content, "expired"
51
83
  end
52
84
 
53
- def decode(message)
54
- ::Base64.strict_decode64(message)
85
+ if hash["pur"].to_s != purpose.to_s
86
+ throw :invalid_message_content, "mismatched purpose"
55
87
  end
56
- end
57
88
 
58
- def verify(purpose)
59
- @message if match?(purpose) && fresh?
60
- end
89
+ hash
90
+ end
61
91
 
62
- private
63
- def match?(purpose)
64
- @purpose.to_s == purpose.to_s
92
+ def metadata_envelope?(object)
93
+ object.is_a?(Hash) && object.key?("_rails")
65
94
  end
66
95
 
67
- def fresh?
68
- @expires_at.nil? || Time.now.utc < @expires_at
96
+ def dual_serialized_metadata_envelope_json?(string)
97
+ string.start_with?('{"_rails":{"message":')
98
+ end
99
+
100
+ def pick_expiry(expires_at, expires_in)
101
+ expiry = if expires_at
102
+ expires_at.utc
103
+ elsif expires_in
104
+ Time.now.utc.advance(seconds: expires_in)
105
+ end
106
+
107
+ unless Metadata::TIMESTAMP_SERIALIZERS.include?(serializer)
108
+ expiry = expiry&.iso8601(3)
109
+ end
110
+
111
+ expiry
69
112
  end
70
113
 
71
- def parse_expires_at(expires_at)
72
- if ActiveSupport.use_standard_json_time_format
114
+ def parse_expiry(expires_at)
115
+ if !expires_at.is_a?(String)
116
+ expires_at
117
+ elsif ActiveSupport.use_standard_json_time_format
73
118
  Time.iso8601(expires_at)
74
119
  else
75
120
  Time.parse(expires_at)
76
121
  end
77
122
  end
123
+
124
+ def serialize_to_json(data)
125
+ ActiveSupport::JSON.encode(data)
126
+ end
127
+
128
+ def deserialize_from_json(serialized)
129
+ ActiveSupport::JSON.decode(serialized)
130
+ rescue ::JSON::ParserError => error
131
+ # Throw :invalid_message_format instead of :invalid_message_serialization
132
+ # because here a parse error is due to a bad message rather than an
133
+ # incompatible `self.serializer`.
134
+ throw :invalid_message_format, error
135
+ end
136
+
137
+ def serialize_to_json_safe_string(data)
138
+ encode(serialize(data), url_safe: false)
139
+ end
140
+
141
+ def deserialize_from_json_safe_string(string)
142
+ deserialize(decode(string, url_safe: false))
143
+ end
78
144
  end
79
145
  end
80
146
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/slice"
4
+
5
+ module ActiveSupport
6
+ module Messages
7
+ class RotationCoordinator # :nodoc:
8
+ attr_accessor :transitional
9
+
10
+ def initialize(&secret_generator)
11
+ raise ArgumentError, "A secret generator block is required" unless secret_generator
12
+ @secret_generator = secret_generator
13
+ @rotate_options = []
14
+ @on_rotation = nil
15
+ @codecs = {}
16
+ end
17
+
18
+ def [](salt)
19
+ @codecs[salt] ||= build_with_rotations(salt)
20
+ end
21
+
22
+ def []=(salt, codec)
23
+ @codecs[salt] = codec
24
+ end
25
+
26
+ def rotate(**options, &block)
27
+ raise ArgumentError, "Options cannot be specified when using a block" if block && !options.empty?
28
+ changing_configuration!
29
+
30
+ @rotate_options << (block || options)
31
+
32
+ self
33
+ end
34
+
35
+ def rotate_defaults
36
+ rotate()
37
+ end
38
+
39
+ def clear_rotations
40
+ changing_configuration!
41
+ @rotate_options.clear
42
+ self
43
+ end
44
+
45
+ def on_rotation(&callback)
46
+ changing_configuration!
47
+ @on_rotation = callback
48
+ end
49
+
50
+ private
51
+ def changing_configuration!
52
+ if @codecs.any?
53
+ raise <<~MESSAGE
54
+ Cannot change #{self.class} configuration after it has already been applied.
55
+
56
+ The configuration has been applied with the following salts:
57
+ #{@codecs.keys.map { |salt| "- #{salt.inspect}" }.join("\n")}
58
+ MESSAGE
59
+ end
60
+ end
61
+
62
+ def normalize_options(options)
63
+ options = options.dup
64
+
65
+ options[:secret_generator] ||= @secret_generator
66
+
67
+ secret_generator_kwargs = options[:secret_generator].parameters.
68
+ filter_map { |type, name| name if type == :key || type == :keyreq }
69
+ options[:secret_generator_options] = options.extract!(*secret_generator_kwargs)
70
+
71
+ options[:on_rotation] = @on_rotation
72
+
73
+ options
74
+ end
75
+
76
+ def build_with_rotations(salt)
77
+ rotate_options = @rotate_options.map { |options| options.is_a?(Proc) ? options.(salt) : options }
78
+ transitional = self.transitional && rotate_options.first
79
+ rotate_options.compact!
80
+ rotate_options[0..1] = rotate_options[0..1].reverse if transitional
81
+ rotate_options = rotate_options.map { |options| normalize_options(options) }.uniq
82
+
83
+ raise "No options have been configured for #{salt}" if rotate_options.empty?
84
+
85
+ rotate_options.map { |options| build(salt.to_s, **options) }.reduce(&:fall_back_to)
86
+ end
87
+
88
+ def build(salt, secret_generator:, secret_generator_options:, **options)
89
+ raise NotImplementedError
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,54 +3,56 @@
3
3
  module ActiveSupport
4
4
  module Messages
5
5
  module Rotator # :nodoc:
6
- def initialize(*secrets, on_rotation: nil, **options)
7
- super(*secrets, **options)
8
-
9
- @options = options
6
+ def initialize(*args, on_rotation: nil, **options)
7
+ super(*args, **options)
8
+ @args = args
9
+ @options = options
10
10
  @rotations = []
11
11
  @on_rotation = on_rotation
12
12
  end
13
13
 
14
- def rotate(*secrets, **options)
15
- @rotations << build_rotation(*secrets, @options.merge(options))
14
+ def rotate(*args, **options)
15
+ fall_back_to build_rotation(*args, **options)
16
16
  end
17
17
 
18
- module Encryptor
19
- include Rotator
20
-
21
- def decrypt_and_verify(*args, on_rotation: @on_rotation, **options)
22
- super
23
- rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
24
- run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, **options) } || raise
25
- end
18
+ def fall_back_to(fallback)
19
+ @rotations << fallback
20
+ self
21
+ end
26
22
 
27
- private
28
- def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
29
- self.class.new(secret, sign_secret, **options)
23
+ def read_message(message, on_rotation: @on_rotation, **options)
24
+ if @rotations.empty?
25
+ super(message, **options)
26
+ else
27
+ thrown, error = catch_rotation_error do
28
+ return super(message, **options)
30
29
  end
31
- end
32
30
 
33
- module Verifier
34
- include Rotator
31
+ @rotations.each do |rotation|
32
+ catch_rotation_error do
33
+ value = rotation.read_message(message, **options)
34
+ on_rotation&.call
35
+ return value
36
+ end
37
+ end
35
38
 
36
- def verified(*args, on_rotation: @on_rotation, **options)
37
- super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, **options) }
39
+ throw thrown, error
38
40
  end
39
-
40
- private
41
- def build_rotation(secret = @secret, options)
42
- self.class.new(secret, **options)
43
- end
44
41
  end
45
42
 
46
43
  private
47
- def run_rotations(on_rotation)
48
- @rotations.find do |rotation|
49
- if message = yield(rotation) rescue next
50
- on_rotation&.call
51
- return message
44
+ def build_rotation(*args, **options)
45
+ self.class.new(*args, *@args.drop(args.length), **@options, **options)
46
+ end
47
+
48
+ def catch_rotation_error(&block)
49
+ error = catch :invalid_message_format do
50
+ error = catch :invalid_message_serialization do
51
+ return [nil, block.call]
52
52
  end
53
+ return [:invalid_message_serialization, error]
53
54
  end
55
+ [:invalid_message_format, error]
54
56
  end
55
57
  end
56
58
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/kernel/reporting"
4
+ require "active_support/notifications"
5
+
6
+ module ActiveSupport
7
+ module Messages # :nodoc:
8
+ module SerializerWithFallback # :nodoc:
9
+ def self.[](format)
10
+ if format.to_s.include?("message_pack") && !defined?(ActiveSupport::MessagePack)
11
+ require "active_support/message_pack"
12
+ end
13
+
14
+ SERIALIZERS.fetch(format)
15
+ end
16
+
17
+ def load(dumped)
18
+ format = detect_format(dumped)
19
+
20
+ if format == self.format
21
+ _load(dumped)
22
+ elsif format && fallback?(format)
23
+ payload = { serializer: SERIALIZERS.key(self), fallback: format, serialized: dumped }
24
+ ActiveSupport::Notifications.instrument("message_serializer_fallback.active_support", payload) do
25
+ payload[:deserialized] = SERIALIZERS[format]._load(dumped)
26
+ end
27
+ else
28
+ raise "Unsupported serialization format"
29
+ end
30
+ end
31
+
32
+ private
33
+ def detect_format(dumped)
34
+ case
35
+ when MessagePackWithFallback.dumped?(dumped)
36
+ :message_pack
37
+ when MarshalWithFallback.dumped?(dumped)
38
+ :marshal
39
+ when JsonWithFallback.dumped?(dumped)
40
+ :json
41
+ end
42
+ end
43
+
44
+ def fallback?(format)
45
+ format != :marshal
46
+ end
47
+
48
+ module AllowMarshal
49
+ private
50
+ def fallback?(format)
51
+ super || format == :marshal
52
+ end
53
+ end
54
+
55
+ module MarshalWithFallback
56
+ include SerializerWithFallback
57
+ extend self
58
+
59
+ def format
60
+ :marshal
61
+ end
62
+
63
+ def dump(object)
64
+ Marshal.dump(object)
65
+ end
66
+
67
+ def _load(dumped)
68
+ Marshal.load(dumped)
69
+ end
70
+
71
+ MARSHAL_SIGNATURE = "\x04\x08"
72
+
73
+ def dumped?(dumped)
74
+ dumped.start_with?(MARSHAL_SIGNATURE)
75
+ end
76
+ end
77
+
78
+ module JsonWithFallback
79
+ include SerializerWithFallback
80
+ extend self
81
+
82
+ def format
83
+ :json
84
+ end
85
+
86
+ def dump(object)
87
+ ActiveSupport::JSON.encode(object)
88
+ end
89
+
90
+ def _load(dumped)
91
+ ActiveSupport::JSON.decode(dumped)
92
+ end
93
+
94
+ JSON_START_WITH = /\A(?:[{\["]|-?\d|true|false|null)/
95
+
96
+ def dumped?(dumped)
97
+ JSON_START_WITH.match?(dumped)
98
+ end
99
+
100
+ private
101
+ def detect_format(dumped)
102
+ # Assume JSON format if format could not be determined.
103
+ super || :json
104
+ end
105
+ end
106
+
107
+ module JsonWithFallbackAllowMarshal
108
+ include JsonWithFallback
109
+ include AllowMarshal
110
+ extend self
111
+ end
112
+
113
+ module MessagePackWithFallback
114
+ include SerializerWithFallback
115
+ extend self
116
+
117
+ def format
118
+ :message_pack
119
+ end
120
+
121
+ def dump(object)
122
+ ActiveSupport::MessagePack.dump(object)
123
+ end
124
+
125
+ def _load(dumped)
126
+ ActiveSupport::MessagePack.load(dumped)
127
+ end
128
+
129
+ def dumped?(dumped)
130
+ available? && ActiveSupport::MessagePack.signature?(dumped)
131
+ end
132
+
133
+ private
134
+ def available?
135
+ return @available if defined?(@available)
136
+ silence_warnings { require "active_support/message_pack" }
137
+ @available = true
138
+ rescue LoadError
139
+ @available = false
140
+ end
141
+ end
142
+
143
+ module MessagePackWithFallbackAllowMarshal
144
+ include MessagePackWithFallback
145
+ include AllowMarshal
146
+ extend self
147
+ end
148
+
149
+ SERIALIZERS = {
150
+ marshal: MarshalWithFallback,
151
+ json: JsonWithFallback,
152
+ json_allow_marshal: JsonWithFallbackAllowMarshal,
153
+ message_pack: MessagePackWithFallback,
154
+ message_pack_allow_marshal: MessagePackWithFallbackAllowMarshal,
155
+ }
156
+ end
157
+ end
158
+ end
@@ -3,11 +3,12 @@
3
3
  require "active_support/json"
4
4
  require "active_support/core_ext/string/access"
5
5
  require "active_support/core_ext/string/behavior"
6
- require "active_support/core_ext/symbol/starts_ends_with"
7
6
  require "active_support/core_ext/module/delegation"
8
7
 
9
- module ActiveSupport #:nodoc:
10
- module Multibyte #:nodoc:
8
+ module ActiveSupport # :nodoc:
9
+ module Multibyte # :nodoc:
10
+ # = Active Support \Multibyte \Chars
11
+ #
11
12
  # Chars enables you to work transparently with UTF-8 encoding in the Ruby
12
13
  # String class without having extensive knowledge about the encoding. A
13
14
  # Chars object accepts a string upon initialization and proxies String
@@ -103,7 +104,7 @@ module ActiveSupport #:nodoc:
103
104
  #
104
105
  # 'Café'.mb_chars.reverse.to_s # => 'éfaC'
105
106
  def reverse
106
- chars(@wrapped_string.scan(/\X/).reverse.join)
107
+ chars(@wrapped_string.grapheme_clusters.reverse.join)
107
108
  end
108
109
 
109
110
  # Limits the byte size of the string to a number of bytes without breaking
@@ -126,16 +127,16 @@ module ActiveSupport #:nodoc:
126
127
 
127
128
  # Performs canonical decomposition on all the characters.
128
129
  #
129
- # 'é'.length # => 2
130
- # 'é'.mb_chars.decompose.to_s.length # => 3
130
+ # 'é'.length # => 1
131
+ # 'é'.mb_chars.decompose.to_s.length # => 2
131
132
  def decompose
132
133
  chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack("U*"))
133
134
  end
134
135
 
135
136
  # Performs composition on all the characters.
136
137
  #
137
- # 'é'.length # => 3
138
- # 'é'.mb_chars.compose.to_s.length # => 2
138
+ # 'é'.length # => 1
139
+ # 'é'.mb_chars.compose.to_s.length # => 1
139
140
  def compose
140
141
  chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack("U*"))
141
142
  end
@@ -143,9 +144,9 @@ module ActiveSupport #:nodoc:
143
144
  # Returns the number of grapheme clusters in the string.
144
145
  #
145
146
  # 'क्षि'.mb_chars.length # => 4
146
- # 'क्षि'.mb_chars.grapheme_length # => 3
147
+ # 'क्षि'.mb_chars.grapheme_length # => 2
147
148
  def grapheme_length
148
- @wrapped_string.scan(/\X/).length
149
+ @wrapped_string.grapheme_clusters.length
149
150
  end
150
151
 
151
152
  # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
@@ -157,7 +158,7 @@ module ActiveSupport #:nodoc:
157
158
  chars(Unicode.tidy_bytes(@wrapped_string, force))
158
159
  end
159
160
 
160
- def as_json(options = nil) #:nodoc:
161
+ def as_json(options = nil) # :nodoc:
161
162
  to_s.as_json(options)
162
163
  end
163
164
 
@@ -8,18 +8,6 @@ module ActiveSupport
8
8
  # The Unicode version that is supported by the implementation
9
9
  UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
10
10
 
11
- def default_normalization_form
12
- ActiveSupport::Deprecation.warn(
13
- "ActiveSupport::Multibyte::Unicode.default_normalization_form is deprecated and will be removed in Rails 6.2."
14
- )
15
- end
16
-
17
- def default_normalization_form=(_)
18
- ActiveSupport::Deprecation.warn(
19
- "ActiveSupport::Multibyte::Unicode.default_normalization_form= is deprecated and will be removed in Rails 6.2."
20
- )
21
- end
22
-
23
11
  # Decompose composed characters to the decomposed form.
24
12
  def decompose(type, codepoints)
25
13
  if type == :compatibility
@@ -34,43 +22,15 @@ module ActiveSupport
34
22
  codepoints.pack("U*").unicode_normalize(:nfc).codepoints
35
23
  end
36
24
 
37
- # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
38
- if !defined?(Rubinius)
39
- # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
40
- # resulting in a valid UTF-8 string.
41
- #
42
- # Passing +true+ will forcibly tidy all bytes, assuming that the string's
43
- # encoding is entirely CP1252 or ISO-8859-1.
44
- def tidy_bytes(string, force = false)
45
- return string if string.empty? || string.ascii_only?
46
- return recode_windows1252_chars(string) if force
47
- string.scrub { |bad| recode_windows1252_chars(bad) }
48
- end
49
- else
50
- def tidy_bytes(string, force = false)
51
- return string if string.empty?
52
- return recode_windows1252_chars(string) if force
53
-
54
- # We can't transcode to the same format, so we choose a nearly-identical encoding.
55
- # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
56
- # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
57
- # before returning.
58
- reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
59
-
60
- source = string.dup
61
- out = "".force_encoding(Encoding::UTF_16LE)
62
-
63
- loop do
64
- reader.primitive_convert(source, out)
65
- _, _, _, error_bytes, _ = reader.primitive_errinfo
66
- break if error_bytes.nil?
67
- out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace)
68
- end
69
-
70
- reader.finish
71
-
72
- out.encode!(Encoding::UTF_8)
73
- end
25
+ # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
26
+ # resulting in a valid UTF-8 string.
27
+ #
28
+ # Passing +true+ will forcibly tidy all bytes, assuming that the string's
29
+ # encoding is entirely CP1252 or ISO-8859-1.
30
+ def tidy_bytes(string, force = false)
31
+ return string if string.empty? || string.ascii_only?
32
+ return recode_windows1252_chars(string) if force
33
+ string.scrub { |bad| recode_windows1252_chars(bad) }
74
34
  end
75
35
 
76
36
  private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveSupport #:nodoc:
3
+ module ActiveSupport # :nodoc:
4
4
  module Multibyte
5
5
  autoload :Chars, "active_support/multibyte/chars"
6
6
  autoload :Unicode, "active_support/multibyte/unicode"