Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 83 additions & 57 deletions lib/ip.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ defmodule IP do
false
```
"""
defguard is_ipv4(v4) when is_tuple(v4) and tuple_size(v4) == 4 and
is_byte(elem(v4, 0)) and is_byte(elem(v4, 1)) and
is_byte(elem(v4, 2)) and is_byte(elem(v4, 3))
defguard is_ipv4(v4)
when is_tuple(v4) and tuple_size(v4) == 4 and
is_byte(elem(v4, 0)) and is_byte(elem(v4, 1)) and
is_byte(elem(v4, 2)) and is_byte(elem(v4, 3))

@doc """
true if the argument is an ipv6 datatype
Expand All @@ -67,11 +68,12 @@ defmodule IP do
false
```
"""
defguard is_ipv6(v6) when is_tuple(v6) and tuple_size(v6) == 8 and
is_short(elem(v6, 0)) and is_short(elem(v6, 1)) and
is_short(elem(v6, 2)) and is_short(elem(v6, 3)) and
is_short(elem(v6, 4)) and is_short(elem(v6, 5)) and
is_short(elem(v6, 6)) and is_short(elem(v6, 7))
defguard is_ipv6(v6)
when is_tuple(v6) and tuple_size(v6) == 8 and
is_short(elem(v6, 0)) and is_short(elem(v6, 1)) and
is_short(elem(v6, 2)) and is_short(elem(v6, 3)) and
is_short(elem(v6, 4)) and is_short(elem(v6, 5)) and
is_short(elem(v6, 6)) and is_short(elem(v6, 7))

@doc """
true if the argument is either ipv6 or ipv4 datatype
Expand Down Expand Up @@ -110,20 +112,23 @@ defmodule IP do
"::1"
```
"""
@spec to_string(addr | nil, :dots | :hyphens) :: String.t
@spec to_string(addr | nil, :dots | :hyphens) :: String.t()
def to_string(ip_addr, style \\ :dots)
def to_string(nil, _), do: ""

def to_string({a, b, c, d}, :dots) do
"#{a}.#{b}.#{c}.#{d}"
end

def to_string({a, b, c, d}, :hyphens) do
"#{a}-#{b}-#{c}-#{d}"
end

def to_string(v6, _) when is_ipv6(v6) do
# cheat by using the erlang builtin function.
v6
|> :inet.ntoa
|> List.to_string
|> :inet.ntoa()
|> List.to_string()
end

@doc """
Expand All @@ -134,7 +139,7 @@ defmodule IP do
{255, 255, 255, 255}
```
"""
@spec from_string!(String.t) :: addr
@spec from_string!(String.t()) :: addr
def from_string!(str) do
case from_string(str) do
{:ok, v} -> v
Expand All @@ -145,10 +150,10 @@ defmodule IP do
@doc """
Finds an ip address in a string, returning an ok or error tuple on failure.
"""
@spec from_string(String.t) :: {:ok, addr} | {:error, :einval}
@spec from_string(String.t()) :: {:ok, addr} | {:error, :einval}
def from_string(str) do
str
|> String.to_charlist
|> String.to_charlist()
|> :inet.parse_address()
end

Expand Down Expand Up @@ -185,10 +190,10 @@ defmodule IP do

@spec mask(0..32, :v4_int) :: 0..0xFFFF_FFFF
@spec mask(0..32, :v4_bin) :: <<_::32>>
@spec mask(0..32, :v4) :: v4
@spec mask(0..32, :v4) :: v4
@spec mask(0..32, :v6_int) :: 0..0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF
@spec mask(0..32, :v6_bin) :: <<_::128>>
@spec mask(0..32, :v6) :: v6
@spec mask(0..32, :v6) :: v6
@doc """
generates an ip mask with specified bit_length.

Expand Down Expand Up @@ -220,23 +225,29 @@ defmodule IP do
"""
def mask(bit_length, mode \\ :v4)
def mask(0, :v4_int), do: 0

def mask(bit_length, :v4_int) do
<<i::unsigned-integer-size(32)>> = <<(-1 <<< (32-bit_length))::32>>
<<i::unsigned-integer-size(32)>> = <<-1 <<< (32 - bit_length)::32>>
i
end

def mask(bit_length, :v4_bin) do
<<mask(bit_length, :v4_int)::unsigned-integer-size(32)>>
end

def mask(bit_length, :v4) do
bit_length
|> mask(:v4_int)
|> from_integer(:v4)
end

def mask(0, :v6_int), do: 0

def mask(bit_length, :v6_int) do
<<i::unsigned-integer-size(128)>> = <<(-1 <<< (128-bit_length))::128>>
<<i::unsigned-integer-size(128)>> = <<-1 <<< (128 - bit_length)::128>>
i
end

def mask(bit_length, :v6) do
bit_length
|> mask(:v6_int)
Expand All @@ -259,6 +270,7 @@ defmodule IP do
|> Bitwise.&&&(mask(bit_length, :v4_int))
|> from_integer(:v4)
end

def prefix(ip, bit_length) when is_ipv6(ip) do
ip
|> to_integer
Expand Down Expand Up @@ -300,25 +312,28 @@ defmodule IP do
less than about 1024.
"""
def random(range_or_subnet, excludes \\ [])

def random(range_or_subnet, excludes) do
range_or_subnet
|> Enum.map(&(&1))
|> Enum.map(& &1)
|> Kernel.--(expand(excludes))
|> Enum.random
|> Enum.random()
end

defp expand(excludes) do
Enum.flat_map(excludes, fn
exclude when is_ip(exclude) -> [exclude]
exclude = %type{} when type in [IP.Range, IP.Subnet]->
Enum.map(exclude, &(&1))
exclude when is_ip(exclude) ->
[exclude]

exclude = %type{} when type in [IP.Range, IP.Subnet] ->
Enum.map(exclude, & &1)
end)
end

####################################################################
## ETC

@spec sigil_i(Macro.t, [byte]) :: Macro.t
@spec sigil_i(Macro.t(), [byte]) :: Macro.t()
@doc """
allows you to use the convenient ~i sigil to declare an IP address
in its normal string form, instead of using the tuple form.
Expand Down Expand Up @@ -366,41 +381,51 @@ defmodule IP do
1
```
"""
defmacro sigil_i({:<<>>, meta, [definition]}, 'm') do
defmacro sigil_i({:<<>>, meta, [definition]}, :m) do
caller_meta = Keyword.merge([file: __CALLER__.file, line: __CALLER__.line], meta)
unless __CALLER__.context == :match do

if __CALLER__.context != :match do
s(caller_meta, "~s/#{definition}/m must be used inside of a match")
end

# perform matching
case String.split(definition, ".") do
ip = [_, _, _, _] ->
{:{}, meta, Enum.map(ip, &token_to_matchv(&1, caller_meta))}

_ ->
s(caller_meta, "invalid ip match #{definition}")
end
end
defmacro sigil_i({:<<>>, meta, [definition]}, 'config') do

defmacro sigil_i({:<<>>, meta, [definition]}, :config) do
caller_meta = Keyword.merge([file: __CALLER__.file, line: __CALLER__.line], meta)

case String.split(definition, "/") do
[ip_str, bit_size] ->
ip = IP.from_string!(ip_str)
subnet = IP.Subnet.of(ip, String.to_integer(bit_size))
{ip, subnet}

_ ->
s(caller_meta, "invalid configuration definition #{definition}")
end
|> Macro.escape()
end

defmacro sigil_i({:<<>>, _meta, [definition]}, []) do
# check to see if it has a slash, in which case it's an ip range
cond do
String.contains?(definition, "/") ->
IP.Subnet.from_string!(definition)

String.contains?(definition, "..") ->
IP.Range.from_string!(definition)

# only one colon is the sign of an ipv4 socket address
match?([_ , _], String.split(definition, ":")) ->
match?([_, _], String.split(definition, ":")) ->
IP.SockAddr.from_string!(definition)

true ->
IP.from_string!(definition)
end
Expand All @@ -412,14 +437,18 @@ defmodule IP do
@ctx [context: Elixir, import: Kernel]
defp token_to_matchv(<<x>> <> _ = int_str, meta) when x in ?0..?9 do
int_val = String.to_integer(int_str)
unless int_val in 0..255 do

if int_val not in 0..255 do
s(meta, "#{int_val} is out of the range for ipv4 addresses")
end

int_val
end

defp token_to_matchv(<<?^>> <> var, meta) do
{:^, meta, [token_to_matchv(var, meta)]}
end

defp token_to_matchv(var, meta) do
{:var!, @ctx, [{String.to_atom(var), meta, Elixir}]}
end
Expand All @@ -436,16 +465,13 @@ defmodule IP do
<<i::unsigned-integer-size(32)>> = <<a, b, c, d>>
i
end

def to_integer(ip = {a, b, c, d, e, f, g, h}) when is_ipv6(ip) do
<<i::unsigned-integer-size(128)>> =
<<a::unsigned-integer-size(16),
b::unsigned-integer-size(16),
c::unsigned-integer-size(16),
d::unsigned-integer-size(16),
e::unsigned-integer-size(16),
f::unsigned-integer-size(16),
g::unsigned-integer-size(16),
h::unsigned-integer-size(16)>>
<<a::unsigned-integer-size(16), b::unsigned-integer-size(16), c::unsigned-integer-size(16),
d::unsigned-integer-size(16), e::unsigned-integer-size(16), f::unsigned-integer-size(16),
g::unsigned-integer-size(16), h::unsigned-integer-size(16)>>

i
end

Expand All @@ -456,15 +482,12 @@ defmodule IP do
<<a, b, c, d>> = <<i::unsigned-integer-size(32)>>
{a, b, c, d}
end

def from_integer(i, :v6) when is_integer(i) do
<<a::unsigned-integer-size(16),
b::unsigned-integer-size(16),
c::unsigned-integer-size(16),
d::unsigned-integer-size(16),
e::unsigned-integer-size(16),
f::unsigned-integer-size(16),
g::unsigned-integer-size(16),
h::unsigned-integer-size(16)>> = <<i::128>>
<<a::unsigned-integer-size(16), b::unsigned-integer-size(16), c::unsigned-integer-size(16),
d::unsigned-integer-size(16), e::unsigned-integer-size(16), f::unsigned-integer-size(16),
g::unsigned-integer-size(16), h::unsigned-integer-size(16)>> = <<i::128>>

{a, b, c, d, e, f, g, h}
end

Expand All @@ -477,56 +500,59 @@ defmodule IP do
# useful for some guards
@doc false
defmacro octet_4(ip) do
quote do elem(unquote(ip), 3) end
quote do
elem(unquote(ip), 3)
end
end

@doc false
defmacro octet_34(ip) do
quote do
Bitwise.<<<(elem(unquote(ip), 2), 8) +
elem(unquote(ip), 3)
elem(unquote(ip), 3)
end
end

@doc false
defmacro octet_24(ip) do
quote do
Bitwise.<<<(elem(unquote(ip), 1), 16) +
Bitwise.<<<(elem(unquote(ip), 2), 8) +
elem(unquote(ip), 3)
Bitwise.<<<(elem(unquote(ip), 2), 8) +
elem(unquote(ip), 3)
end
end

@doc false
defmacro octet_14(ip) do
quote do
Bitwise.<<<(elem(unquote(ip), 0), 24) +
Bitwise.<<<(elem(unquote(ip), 1), 16) +
Bitwise.<<<(elem(unquote(ip), 2), 8) +
elem(unquote(ip), 3)
Bitwise.<<<(elem(unquote(ip), 1), 16) +
Bitwise.<<<(elem(unquote(ip), 2), 8) +
elem(unquote(ip), 3)
end
end

@doc false
defmacro octet_1(ip) do
quote do Bitwise.<<<(elem(unquote(ip), 0), 24) end
quote do
Bitwise.<<<(elem(unquote(ip), 0), 24)
end
end

@doc false
defmacro octet_12(ip) do
quote do
Bitwise.<<<(elem(unquote(ip), 0), 24) +
Bitwise.<<<(elem(unquote(ip), 1), 16)
Bitwise.<<<(elem(unquote(ip), 1), 16)
end
end

@doc false
defmacro octet_13(ip) do
quote do
Bitwise.<<<(elem(unquote(ip), 0), 24) +
Bitwise.<<<(elem(unquote(ip), 1), 16) +
Bitwise.<<<(elem(unquote(ip), 2), 8)
Bitwise.<<<(elem(unquote(ip), 1), 16) +
Bitwise.<<<(elem(unquote(ip), 2), 8)
end
end

end
Loading