aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/journey/router/utils.rb
blob: df3f79a407eb8f248851222e55849f850b452746 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# frozen_string_literal: true

module ActionDispatch
  module Journey # :nodoc:
    class Router # :nodoc:
      class Utils # :nodoc:
        # Normalizes URI path.
        #
        # Strips off trailing slash and ensures there is a leading slash.
        # Also converts downcase URL encoded string to uppercase.
        #
        #   normalize_path("/foo")  # => "/foo"
        #   normalize_path("/foo/") # => "/foo"
        #   normalize_path("foo")   # => "/foo"
        #   normalize_path("")      # => "/"
        #   normalize_path("/%ab")  # => "/%AB"
        def self.normalize_path(path)
          path ||= ""
          encoding = path.encoding
          path = "/#{path}".dup
          path.squeeze!("/".freeze)
          path.sub!(%r{/+\Z}, "".freeze)
          path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
          path = "/".dup if path == "".freeze
          path.force_encoding(encoding)
          path
        end

        # URI path and fragment escaping
        # https://tools.ietf.org/html/rfc3986
        class UriEncoder # :nodoc:
          ENCODE   = "%%%02X".freeze
          US_ASCII = Encoding::US_ASCII
          UTF_8    = Encoding::UTF_8
          EMPTY    = "".dup.force_encoding(US_ASCII).freeze
          DEC2HEX  = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }

          ALPHA = "a-zA-Z".freeze
          DIGIT = "0-9".freeze
          UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze
          SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze

          ESCAPED  = /%[a-zA-Z0-9]{2}/.freeze

          FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
          SEGMENT  = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
          PATH     = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze

          def escape_fragment(fragment)
            escape(fragment, FRAGMENT)
          end

          def escape_path(path)
            escape(path, PATH)
          end

          def escape_segment(segment)
            escape(segment, SEGMENT)
          end

          def unescape_uri(uri)
            encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
            uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
          end

          private
            def escape(component, pattern)
              component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
            end

            def percent_encode(unsafe)
              safe = EMPTY.dup
              unsafe.each_byte { |b| safe << DEC2HEX[b] }
              safe
            end
        end

        ENCODER = UriEncoder.new

        def self.escape_path(path)
          ENCODER.escape_path(path.to_s)
        end

        def self.escape_segment(segment)
          ENCODER.escape_segment(segment.to_s)
        end

        def self.escape_fragment(fragment)
          ENCODER.escape_fragment(fragment.to_s)
        end

        # Replaces any escaped sequences with their unescaped representations.
        #
        #   uri = "/topics?title=Ruby%20on%20Rails"
        #   unescape_uri(uri)  #=> "/topics?title=Ruby on Rails"
        def self.unescape_uri(uri)
          ENCODER.unescape_uri(uri)
        end
      end
    end
  end
end