Skip to content

Instantly share code, notes, and snippets.

@jeregrine
Created May 3, 2023 20:33
Show Gist options
  • Select an option

  • Save jeregrine/50dc8fca26167b3b4d880855ed69f13d to your computer and use it in GitHub Desktop.

Select an option

Save jeregrine/50dc8fca26167b3b4d880855ed69f13d to your computer and use it in GitHub Desktop.

Revisions

  1. Jason S created this gist May 3, 2023.
    194 changes: 194 additions & 0 deletions elixir_nsid.ex
    Original 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