Created
May 3, 2023 20:33
-
-
Save jeregrine/50dc8fca26167b3b4d880855ed69f13d to your computer and use it in GitHub Desktop.
Revisions
-
Jason S created this gist
May 3, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,194 @@ defmodule NSID do @moduledoc """ Grammar: alpha = "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "l" / "m" / "n" / "o" / "p" / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z" / "A" / "B" / "C" / "D" / "E" / "F" / "G" / "H" / "I" / "J" / "K" / "L" / "M" / "N" / "O" / "P" / "Q" / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z" number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0" delim = "." segment = alpha *( alpha / number / "-" ) authority = segment *( delim segment ) name = segment nsid = authority delim name nsid-ns = authority delim "*" """ import NimbleParsec defstruct [:authority, :name] alpha = ascii_string([?a..?z, ?A..?Z], min: 1) ascii = ascii_string([?a..?z, ?A..?Z, ?0..?9, ?-], min: 1) delim = string(".") ns = string("*") segment = alpha |> repeat(ascii) |> lookahead(choice([delim, eos()])) |> reduce({Enum, :join, [""]}) authority = segment |> ignore(delim) |> concat(segment) |> post_traverse({__MODULE__, :check_authority, []}) |> tag(:authority) name = choice([ignore(delim), segment, ns]) |> times(min: 1) |> post_traverse({__MODULE__, :check_name, []}) |> tag(:name) nsid = authority |> ignore(delim) |> concat(name) |> reduce({__MODULE__, :to_nsid, []}) defparsecp :parser, nsid def to_nsid(args) do [auth] = Keyword.get(args, :authority) [name] = Keyword.get(args, :name) %NSID{ authority: auth, name: name } end def check_name(rest, args, context, _line, _offset) do name = args |> Enum.reverse() |> Enum.join(".") if String.length(name) > 128 do {:error, "NSID name part too long (max 128 chars)"} else {rest, [name], context} end end def check_authority(rest, args, context, _line, _offset) do auth = args |> Enum.join(".") if String.length(auth) > 63 do {:error, "NSID authority part too long (max 63 chars)"} else {rest, [auth], context} end end @doc ~S""" ## Examples iex> NSID.parse("com.example.bar") %NSID{authority: "example.com", name: "bar"} iex> NSID.parse("com.example.*") %NSID{authority: "example.com", name: "*"} iex> NSID.parse("com.long-thing1.cool.fooBarBaz") %NSID{authority: "long-thing1.com", name: "cool.fooBarBaz"} iex> NSID.parse("onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing") %NSID{authority: "g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", name: "lex.deleteThing"} """ def parse(str) do {:ok, [val], _, _, _, _} = parser(str) true = is_valid?(str) val end @doc ~S""" ## Examples iex> NSID.create("example.com, "bar") %NSID{authority: "example.com", name: "bar"} iex> NSID.create("example.com", "*") %NSID{authority: "example.com", name: "*"} iex> NSID.create("long-thing1.com", "cool.fooBarBaz") %NSID{authority: "long-thing1.com", name: "cool.fooBarBaz"} """ def create(a, n) do %NSID{ authority: a, name: n } end @doc ~S""" ## Examples iex> NSID.create("example.com", "bar") %NSID{authority: "example.com", name: "bar"} iex> NSID.create("example.com", "*") %NSID{authority: "example.com", name: "*"} iex> NSID.create("long-thing1.com", "cool.fooBarBaz") %NSID{authority: "long-thing1.com", name: "cool.fooBarBaz"} """ def create(a, n) do %NSID{ authority: a, name: n } end @doc ~S""" ## Examples iex> NSID.to_string(%NSID{authority: "example.com", name: "bar"}) "com.example.bar" iex> NSID.to_string(%NSID{authority: "example.com", name: "*"}) "com.example.*" iex> NSID.to_string(%NSID{authority: "long-thing1.com", name: "cool.fooBarBaz"}) "com.long-thing1.cool.fooBarBaz" """ defdelegate to_string(uri), to: String.Chars.NSID @doc ~S""" ## Examples iex> NSID.is_valid?("com.example.bar") true iex> NSID.is_valid?("com.example.*") true iex> NSID.is_valid?("com.long-thing1.cool.fooBarBaz") true iex> NSID.is_valid?("onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing") true iex> NSID.is_valid?("cn.8.lex.stuff") {:error, "expected ASCII character in the range \"a\" to \"z\" or in the range \"A\" to \"Z\" at 8.lex.stuff"} iex> NSID.is_valid?("example.com") {:error, "expected string \".\""} iex> NSID.is_valid?(Enum.join(["com", "ex", Enum.map(0..1000, fn _ -> "P" end)], ".")) {:error, "NSID is too long (382 chars max)"} iex> NSID.is_valid?(Enum.join(["com", "ex", Enum.map(0..129, fn _ -> "P" end)], ".")) {:error, "NSID name part too long (max 128 chars)"} """ def is_valid?(str) when is_binary(str) and byte_size(str) > 382 do {:error, "NSID is too long (382 chars max)"} end def is_valid?(str) do case parser(str) do {:ok, _, _, _, _, _} -> true {:error, msg, str, _, _, _} when is_binary(str) and byte_size(str) == 0 -> {:error, msg} {:error, msg, char, _, _, _} -> {:error, msg <> " at " <> char} end end end defimpl String.Chars, for: NSID do def to_string(%{authority: a, name: n}) do auth = String.split(a, ".") |> Enum.reverse() |> Enum.intersperse(?.) IO.iodata_to_binary([ auth, ?., n ]) end end