Repository: rails/rails
File: activesupport/lib/active_support/cache.rb (Ruby)

Code before:
              " default serializer and cache format version is < 7.1"
          end

          options
        end

        # Expands, namespaces and truncates the cache key.
        # Raises an exception when the key is +nil+ or an empty string.
        # May be overridden by cache stores to do additional normalization.
        def normalize_key(key, options = nil)
          key = expand_and_namespace_key(key, options)
          truncate_key(key)
        end

        def expand_and_namespace_key(key, options = nil)
          str_key = expanded_key(key)
          raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?

          namespace_key str_key, options
        end

        def truncate_key(key)
          if key && @max_key_size && key.bytesize > @max_key_size
            suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
            truncate_at = @max_key_size - suffix.bytesize
            "#{key.byteslice(0, truncate_at)}#{suffix}"
          else
            key
          end
        end

        # Prefix the key with a namespace string:
        #
        #   namespace_key 'foo', namespace: 'cache'
        #   # => 'cache:foo'
        #
        # With a namespace block:
        #
        #   namespace_key 'foo', namespace: -> { 'cache' }
        #   # => 'cache:foo'
        def namespace_key(key, call_options = nil)
          namespace = if call_options&.key?(:namespace)
            call_options[:namespace]
          else
            options[:namespace]
          end

          if namespace.respond_to?(:call)
            namespace = namespace.call
          end


Review comment (suggestion):
One small concern here is that we could end up with invalid encoding if we truncate an utf8 string in the middle of a multi-byte character.

I need to test a few things as to how to better handle this.

Code after:
              " default serializer and cache format version is < 7.1"
          end

          options
        end

        # Expands, namespaces and truncates the cache key.
        # Raises an exception when the key is +nil+ or an empty string.
        # May be overridden by cache stores to do additional normalization.
        def normalize_key(key, options = nil)
          key = expand_and_namespace_key(key, options)
          truncate_key(key)
        end

        def expand_and_namespace_key(key, options = nil)
          str_key = expanded_key(key)
          raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?

          namespace_key str_key, options
        end

        def truncate_key(key)
          if key && @max_key_size && key.bytesize > @max_key_size
            suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
            truncate_at = @max_key_size - suffix.bytesize
            key = key.byteslice(0, truncate_at)
            key.scrub!("")
            "#{key}#{suffix}"
          else
            key
          end
        end

        # Prefix the key with a namespace string:
        #
        #   namespace_key 'foo', namespace: 'cache'
        #   # => 'cache:foo'
        #
        # With a namespace block:
        #
        #   namespace_key 'foo', namespace: -> { 'cache' }
        #   # => 'cache:foo'
        def namespace_key(key, call_options = nil)
          namespace = if call_options&.key?(:namespace)
            call_options[:namespace]
          else
            options[:namespace]
          end

          if namespace.respond_to?(:call)
            namespace = namespace.call