-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", 238). -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", 300). ?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", 266). ?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", 197). ?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", 247). ?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", 133). ?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", 167). ?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", 72). ?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_path@1 = case gleam_erlang_ffi:priv_directory(<<"psl"/utf8>>) of {ok, Priv_path} -> Priv_path; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"psl"/utf8>>, function => <<"load_suffix_list"/utf8>>, line => 79, value => _assert_fail, start => 2034, 'end' => 2094, pattern_start => 2045, pattern_end => 2058}) end, <> end, Content@1 = case simplifile:read(File_path) of {ok, Content} -> Content; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"psl"/utf8>>, function => <<"load_suffix_list"/utf8>>, line => 84, value => _assert_fail@1, start => 2153, 'end' => 2210, pattern_start => 2164, pattern_end => 2175}) 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.