utils.ex (8282B)
1 defmodule Plug.Conn.Utils do 2 @moduledoc """ 3 Utilities for working with connection data 4 """ 5 6 @type params :: %{optional(binary) => binary} 7 8 @upper ?A..?Z 9 @lower ?a..?z 10 @alpha ?0..?9 11 @other [?., ?-, ?+] 12 @space [?\s, ?\t] 13 @specials ~c|()<>@,;:\\"/[]?={}| 14 15 @doc ~S""" 16 Parses media types (with wildcards). 17 18 Type and subtype are case insensitive while the 19 sensitiveness of params depends on their keys and 20 therefore are not handled by this parser. 21 22 Returns: 23 24 * `{:ok, type, subtype, map_of_params}` if everything goes fine 25 * `:error` if the media type isn't valid 26 27 ## Examples 28 29 iex> media_type "text/plain" 30 {:ok, "text", "plain", %{}} 31 32 iex> media_type "APPLICATION/vnd.ms-data+XML" 33 {:ok, "application", "vnd.ms-data+xml", %{}} 34 35 iex> media_type "text/*; q=1.0" 36 {:ok, "text", "*", %{"q" => "1.0"}} 37 38 iex> media_type "*/*; q=1.0" 39 {:ok, "*", "*", %{"q" => "1.0"}} 40 41 iex> media_type "x y" 42 :error 43 44 iex> media_type "*/html" 45 :error 46 47 iex> media_type "/" 48 :error 49 50 iex> media_type "x/y z" 51 :error 52 53 """ 54 @spec media_type(binary) :: {:ok, type :: binary, subtype :: binary, params} | :error 55 def media_type(binary) do 56 case strip_spaces(binary) do 57 "*/*" <> t -> mt_params(t, "*", "*") 58 t -> mt_first(t, "") 59 end 60 end 61 62 defp mt_first(<<?/, t::binary>>, acc) when acc != "", do: mt_wildcard(t, acc) 63 64 defp mt_first(<<h, t::binary>>, acc) when h in @upper, 65 do: mt_first(t, <<acc::binary, downcase_char(h)>>) 66 67 defp mt_first(<<h, t::binary>>, acc) when h in @lower or h in @alpha or h == ?-, 68 do: mt_first(t, <<acc::binary, h>>) 69 70 defp mt_first(_, _acc), do: :error 71 72 defp mt_wildcard(<<?*, t::binary>>, first), do: mt_params(t, first, "*") 73 defp mt_wildcard(t, first), do: mt_second(t, "", first) 74 75 defp mt_second(<<h, t::binary>>, acc, first) when h in @upper, 76 do: mt_second(t, <<acc::binary, downcase_char(h)>>, first) 77 78 defp mt_second(<<h, t::binary>>, acc, first) when h in @lower or h in @alpha or h in @other, 79 do: mt_second(t, <<acc::binary, h>>, first) 80 81 defp mt_second(t, acc, first), do: mt_params(t, first, acc) 82 83 defp mt_params(t, first, second) do 84 case strip_spaces(t) do 85 "" -> {:ok, first, second, %{}} 86 ";" <> t -> {:ok, first, second, params(t)} 87 _ -> :error 88 end 89 end 90 91 @doc ~S""" 92 Parses content type (without wildcards). 93 94 It is similar to `media_type/1` except wildcards are 95 not accepted in the type nor in the subtype. 96 97 ## Examples 98 99 iex> content_type "x-sample/json; charset=utf-8" 100 {:ok, "x-sample", "json", %{"charset" => "utf-8"}} 101 102 iex> content_type "x-sample/json ; charset=utf-8 ; foo=bar" 103 {:ok, "x-sample", "json", %{"charset" => "utf-8", "foo" => "bar"}} 104 105 iex> content_type "\r\n text/plain;\r\n charset=utf-8\r\n" 106 {:ok, "text", "plain", %{"charset" => "utf-8"}} 107 108 iex> content_type "text/plain" 109 {:ok, "text", "plain", %{}} 110 111 iex> content_type "x/*" 112 :error 113 114 iex> content_type "*/*" 115 :error 116 117 """ 118 @spec content_type(binary) :: {:ok, type :: binary, subtype :: binary, params} | :error 119 def content_type(binary) do 120 case media_type(binary) do 121 {:ok, _, "*", _} -> :error 122 {:ok, _, _, _} = ok -> ok 123 :error -> :error 124 end 125 end 126 127 @doc ~S""" 128 Parses headers parameters. 129 130 Keys are case insensitive and downcased, 131 invalid key-value pairs are discarded. 132 133 ## Examples 134 135 iex> params("foo=bar") 136 %{"foo" => "bar"} 137 138 iex> params(" foo=bar ") 139 %{"foo" => "bar"} 140 141 iex> params("FOO=bar") 142 %{"foo" => "bar"} 143 144 iex> params("Foo=bar; baz=BOING") 145 %{"foo" => "bar", "baz" => "BOING"} 146 147 iex> params("foo=BAR ; wat") 148 %{"foo" => "BAR"} 149 150 iex> params("foo=\"bar\"; baz=\"boing\"") 151 %{"foo" => "bar", "baz" => "boing"} 152 153 iex> params("foo=\"bar;\"; baz=\"boing\"") 154 %{"foo" => "bar;", "baz" => "boing"} 155 156 iex> params("=") 157 %{} 158 159 iex> params(";") 160 %{} 161 162 """ 163 @spec params(binary) :: params 164 def params(t) do 165 t 166 |> split_semicolon("", [], false) 167 |> Enum.reduce(%{}, ¶ms/2) 168 end 169 170 defp params(param, acc) do 171 case params_key(strip_spaces(param), "") do 172 {k, v} -> Map.put(acc, k, v) 173 false -> acc 174 end 175 end 176 177 defp params_key(<<?=, t::binary>>, acc) when acc != "", do: params_value(t, acc) 178 179 defp params_key(<<h, _::binary>>, _acc) 180 when h in @specials or h in @space or h < 32 or h === 127, 181 do: false 182 183 defp params_key(<<h, t::binary>>, acc), do: params_key(t, <<acc::binary, downcase_char(h)>>) 184 defp params_key(<<>>, _acc), do: false 185 186 defp params_value(token, key) do 187 case token(token) do 188 false -> false 189 value -> {key, value} 190 end 191 end 192 193 @doc ~S""" 194 Parses a value as defined in [RFC-1341](http://www.w3.org/Protocols/rfc1341/4_Content-Type.html). 195 196 For convenience, trims whitespace at the end of the token. 197 Returns `false` if the token is invalid. 198 199 ## Examples 200 201 iex> token("foo") 202 "foo" 203 204 iex> token("foo-bar") 205 "foo-bar" 206 207 iex> token("<foo>") 208 false 209 210 iex> token(~s["<foo>"]) 211 "<foo>" 212 213 iex> token(~S["<f\oo>\"<b\ar>"]) 214 "<foo>\"<bar>" 215 216 iex> token(~s["]) 217 false 218 219 iex> token("foo ") 220 "foo" 221 222 iex> token("foo bar") 223 false 224 225 iex> token("") 226 false 227 228 iex> token(" ") 229 "" 230 231 """ 232 @spec token(binary) :: binary | false 233 def token(""), do: false 234 def token(<<?", quoted::binary>>), do: quoted_token(quoted, "") 235 def token(token), do: unquoted_token(token, "") 236 237 defp quoted_token(<<>>, _acc), do: false 238 defp quoted_token(<<?", t::binary>>, acc), do: strip_spaces(t) == "" and acc 239 defp quoted_token(<<?\\, h, t::binary>>, acc), do: quoted_token(t, <<acc::binary, h>>) 240 defp quoted_token(<<h, t::binary>>, acc), do: quoted_token(t, <<acc::binary, h>>) 241 242 defp unquoted_token(<<>>, acc), do: acc 243 defp unquoted_token("\r\n" <> t, acc), do: strip_spaces(t) == "" and acc 244 defp unquoted_token(<<h, t::binary>>, acc) when h in @space, do: strip_spaces(t) == "" and acc 245 246 defp unquoted_token(<<h, _::binary>>, _acc) when h in @specials or h < 32 or h === 127, 247 do: false 248 249 defp unquoted_token(<<h, t::binary>>, acc), do: unquoted_token(t, <<acc::binary, h>>) 250 251 @doc """ 252 Parses a comma-separated list of header values. 253 254 ## Examples 255 256 iex> list("foo, bar") 257 ["foo", "bar"] 258 259 iex> list("foobar") 260 ["foobar"] 261 262 iex> list("") 263 [] 264 265 iex> list("empties, , are,, filtered") 266 ["empties", "are", "filtered"] 267 268 """ 269 @spec list(binary) :: [binary] 270 def list(binary) do 271 for elem <- :binary.split(binary, ",", [:global]), 272 stripped = strip_spaces(elem), 273 stripped != "", 274 do: stripped 275 end 276 277 @doc """ 278 Validates the given binary is valid UTF-8. 279 """ 280 @spec validate_utf8!(binary, module, binary) :: :ok | no_return 281 def validate_utf8!(binary, exception, context) 282 283 def validate_utf8!(<<binary::binary>>, exception, context) do 284 do_validate_utf8!(binary, exception, context) 285 end 286 287 defp do_validate_utf8!(<<_::utf8, rest::bits>>, exception, context) do 288 do_validate_utf8!(rest, exception, context) 289 end 290 291 defp do_validate_utf8!(<<byte, _::bits>>, exception, context) do 292 raise exception, "invalid UTF-8 on #{context}, got byte #{byte}" 293 end 294 295 defp do_validate_utf8!(<<>>, _exception, _context) do 296 :ok 297 end 298 299 ## Helpers 300 301 defp strip_spaces("\r\n" <> t), do: strip_spaces(t) 302 defp strip_spaces(<<h, t::binary>>) when h in [?\s, ?\t], do: strip_spaces(t) 303 defp strip_spaces(t), do: t 304 305 defp downcase_char(char) when char in @upper, do: char + 32 306 defp downcase_char(char), do: char 307 308 defp split_semicolon(<<>>, <<>>, acc, _), do: acc 309 defp split_semicolon(<<>>, buffer, acc, _), do: [buffer | acc] 310 311 defp split_semicolon(<<?", rest::binary>>, buffer, acc, quoted?), 312 do: split_semicolon(rest, <<buffer::binary, ?">>, acc, not quoted?) 313 314 defp split_semicolon(<<?;, rest::binary>>, buffer, acc, false), 315 do: split_semicolon(rest, <<>>, [buffer | acc], false) 316 317 defp split_semicolon(<<char, rest::binary>>, buffer, acc, quoted?), 318 do: split_semicolon(rest, <<buffer::binary, char>>, acc, quoted?) 319 end