Current section
Files
Jump to
Current section
Files
src/psl.erl
-module(psl).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/psl.gleam").
-export([parse/2, add_rule/3, load_suffix_list/2]).
-export_type([domain_parts/0, parse_error/0, suffix_list/0, suffix/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type domain_parts() :: {domain_parts,
binary(),
binary(),
binary(),
list(binary())}.
-type parse_error() :: invalid_uri | no_host | invalid_domain | unknown_suffix.
-opaque suffix_list() :: {suffix_list,
gleam@dict:dict(binary(), suffix()),
list(binary()),
gleam@dict:dict(binary(), suffix())}.
-type suffix() :: {suffix, binary(), boolean(), integer()}.
-file("src/psl.gleam", 234).
-spec decode_domain(binary()) -> binary().
decode_domain(Domain) ->
case gleam_stdlib:contains_string(Domain, <<"xn--"/utf8>>) of
false ->
Domain;
true ->
idna:to_unicode(Domain)
end.
-file("src/psl.gleam", 296).
?DOC(" Check if a suffix matches any wildcard pattern\n").
-spec check_wildcard_match(list(binary()), list(binary())) -> {ok, binary()} |
{error, nil}.
check_wildcard_match(Suffix_labels, Wildcards) ->
Normal_order = lists:reverse(Suffix_labels),
case Normal_order of
[] ->
{error, nil};
[_] ->
{error, nil};
[_ | Rest] ->
Parent = gleam@string:join(lists:reverse(Rest), <<"."/utf8>>),
case gleam@list:any(Wildcards, fun(W) -> W =:= Parent end) of
true ->
Matched = gleam@string:join(Normal_order, <<"."/utf8>>),
{ok, Matched};
false ->
{error, nil}
end
end.
-file("src/psl.gleam", 262).
?DOC(" Find all matching suffixes for the given labels\n").
-spec find_all_matches(list(binary()), suffix_list()) -> list(binary()).
find_all_matches(Labels, Suffix_list) ->
Reversed = lists:reverse(Labels),
_pipe = gleam@list:range(1, erlang:length(Labels)),
gleam@list:filter_map(
_pipe,
fun(I) ->
Suffix_labels = gleam@list:take(Reversed, I),
Suffix = begin
_pipe@1 = Suffix_labels,
_pipe@2 = lists:reverse(_pipe@1),
gleam@string:join(_pipe@2, <<"."/utf8>>)
end,
case gleam@dict:has_key(erlang:element(4, Suffix_list), Suffix) of
true ->
{error, nil};
false ->
case gleam@dict:has_key(
erlang:element(2, Suffix_list),
Suffix
) of
true ->
{ok, Suffix};
false ->
case check_wildcard_match(
Suffix_labels,
erlang:element(3, Suffix_list)
) of
{ok, Matched} ->
{ok, Matched};
{error, _} ->
{error, nil}
end
end
end
end
).
-file("src/psl.gleam", 193).
?DOC(" Extract domain parts from hostname and suffix\n").
-spec extract_parts(binary(), binary()) -> {ok, domain_parts()} |
{error, parse_error()}.
extract_parts(Host, Suffix) ->
Host_labels = gleam@string:split(Host, <<"."/utf8>>),
Suffix_labels = gleam@string:split(Suffix, <<"."/utf8>>),
case erlang:length(Host_labels) =< erlang:length(Suffix_labels) of
true ->
{error, invalid_domain};
false ->
Remaining_count = erlang:length(Host_labels) - erlang:length(
Suffix_labels
),
case Remaining_count of
0 ->
{error, invalid_domain};
_ ->
Remaining = gleam@list:take(Host_labels, Remaining_count),
case lists:reverse(Remaining) of
[] ->
{error, invalid_domain};
[Domain | Rest] ->
Subdomains = lists:reverse(Rest),
{ok,
{domain_parts,
Suffix,
Domain,
gleam@string:join(Subdomains, <<"."/utf8>>),
Subdomains}}
end
end
end.
-file("src/psl.gleam", 243).
?DOC(" Find the matching public suffix for a hostname\n").
-spec find_suffix(binary(), suffix_list()) -> {ok, binary()} | {error, nil}.
find_suffix(Host, Suffix_list) ->
Labels = gleam@string:split(Host, <<"."/utf8>>),
Matches = find_all_matches(Labels, Suffix_list),
case Matches of
[] ->
{error, nil};
_ ->
_pipe = Matches,
_pipe@1 = gleam@list:sort(
_pipe,
fun(A, B) ->
gleam@int:compare(string:length(B), string:length(A))
end
),
_pipe@2 = gleam@list:first(_pipe@1),
gleam@result:replace_error(_pipe@2, nil)
end.
-file("src/psl.gleam", 129).
?DOC(
" Parse a URI and extract domain parts\n"
"\n"
" ```gleam\n"
" let list = load_suffix_list(True, None)\n"
" let result1 = parse(\"https://example.com\", list)\n"
" let result2 = parse(\"https://test.co.uk\", list)\n"
" ```\n"
).
-spec parse(binary(), suffix_list()) -> {ok, domain_parts()} |
{error, parse_error()}.
parse(Uri_string, List) ->
gleam@result:'try'(
begin
_pipe = gleam_stdlib:uri_parse(Uri_string),
gleam@result:replace_error(_pipe, invalid_uri)
end,
fun(Parsed_uri) ->
gleam@result:'try'(case erlang:element(4, Parsed_uri) of
{some, H} ->
{ok, H};
none ->
{error, no_host}
end, fun(Host) ->
Decoded_host = decode_domain(Host),
gleam@result:'try'(
begin
_pipe@1 = find_suffix(Decoded_host, List),
gleam@result:replace_error(_pipe@1, unknown_suffix)
end,
fun(Suffix) ->
_pipe@2 = extract_parts(Decoded_host, Suffix),
gleam@result:map_error(
_pipe@2,
fun(_) -> invalid_domain end
)
end
)
end)
end
).
-file("src/psl.gleam", 163).
?DOC(
" Add a single rule to the suffix list. This is useful if a suffix is not yet\n"
" in the list provided by publicsuffix.org.\n"
"\n"
" Rules are formatted as follows:\n"
"\n"
" * Normal domaains do not start with a special character.\n"
" * Exceptions start with a \"!\".\n"
" * Wildcards start with a \"*\".\n"
).
-spec add_rule(suffix_list(), binary(), boolean()) -> suffix_list().
add_rule(Sl, Rule, Is_public) ->
case gleam_stdlib:string_starts_with(Rule, <<"!"/utf8>>) of
true ->
Suffix_str = gleam@string:drop_start(Rule, 1),
Suffix = {suffix, Suffix_str, Is_public, string:length(Suffix_str)},
{suffix_list,
erlang:element(2, Sl),
erlang:element(3, Sl),
gleam@dict:insert(erlang:element(4, Sl), Suffix_str, Suffix)};
false ->
case gleam_stdlib:string_starts_with(Rule, <<"*"/utf8, "."/utf8>>) of
true ->
Pattern = gleam@string:drop_start(Rule, 2),
Suffix@1 = {suffix,
Pattern,
Is_public,
string:length(Pattern)},
{suffix_list,
gleam@dict:insert(
erlang:element(2, Sl),
Pattern,
Suffix@1
),
[Pattern | erlang:element(3, Sl)],
erlang:element(4, Sl)};
false ->
Suffix@2 = {suffix, Rule, Is_public, string:length(Rule)},
{suffix_list,
gleam@dict:insert(erlang:element(2, Sl), Rule, Suffix@2),
erlang:element(3, Sl),
erlang:element(4, Sl)}
end
end.
-file("src/psl.gleam", 71).
?DOC(
" Load the public suffix list from the a data file and choose to\n"
" include private domains or not. Pass a file path to include your own list\n"
" or leave `path` empty to load the one included in the package.\n"
).
-spec load_suffix_list(boolean(), gleam@option:option(binary())) -> suffix_list().
load_suffix_list(Include_private, Path) ->
File_path = case Path of
{some, P} ->
P;
none ->
<<"priv/public_suffix_list.dat"/utf8>>
end,
Content@1 = case simplifile:read(File_path) of
{ok, Content} -> Content;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"psl"/utf8>>,
function => <<"load_suffix_list"/utf8>>,
line => 80,
value => _assert_fail,
start => 2031,
'end' => 2088,
pattern_start => 2042,
pattern_end => 2053})
end,
Domain_data = case Include_private of
true ->
Content@1;
false ->
case gleam@string:split_once(
Content@1,
<<"===BEGIN PRIVATE DOMAINS==="/utf8>>
) of
{ok, {Public_domains, _}} ->
Public_domains;
{error, _} ->
Content@1
end
end,
{Suffix_list, _} = begin
_pipe = Domain_data,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
gleam@list:fold(
_pipe@1,
{{suffix_list, maps:new(), gleam@list:new(), maps:new()}, true},
fun(Acc, Line) ->
{Sl, Is_public} = Acc,
Trimmed = gleam@string:trim(Line),
case gleam_stdlib:contains_string(
Trimmed,
<<"===BEGIN PRIVATE DOMAINS==="/utf8>>
) of
true ->
case Include_private of
true ->
{Sl, false};
false ->
Acc
end;
false ->
case (Trimmed =:= <<""/utf8>>) orelse gleam_stdlib:string_starts_with(
Trimmed,
<<"//"/utf8>>
) of
true ->
Acc;
false ->
{add_rule(Sl, Trimmed, Is_public), Is_public}
end
end
end
)
end,
Suffix_list.