File Coverage

lib/At/Protocol/Handle.pm
Criterion Covered Total %
statement 72 81 88.8
branch 24 26 92.3
condition 8 8 100.0
subroutine 13 14 92.8
pod 7 7 100.0
total 124 136 91.1


line stmt bran cond sub pod time code
1             package At::Protocol::Handle 1.0 {
2 5     5   217630 use v5.42;
  5         18  
3 5     5   474 use At::Error qw[register throw];
  5         9  
  5         63  
4 5     5   496 use parent -norequire => 'Exporter';
  5         390  
  5         28  
5 5     5   312 use feature 'try';
  5         17  
  5         565  
6 5     5   24 no warnings qw[experimental::try];
  5         16  
  5         460  
7             use overload
8 27     27   4249 '""' => sub ( $s, $u, $q ) {
  27         46  
  27         42  
  27         35  
  27         37  
9 27         118 $$s;
10 5     5   31 };
  5         8  
  5         36  
11             our %EXPORT_TAGS = (
12             all => [
13             our @EXPORT_OK
14             = qw[
15             ensureValidHandle ensureValidHandleRegex
16             normalizeHandle normalizeAndEnsureValidHandle
17             isValidHandle isValidTld]
18             ]
19             );
20             #
21             my $INVALID_HANDLE = 'handle.invalid';
22              
23             #~ Currently these are registration-time restrictions, not protocol-level
24             #~ restrictions. We have a couple accounts in the wild that we need to clean up
25             #~ before hard-disallow.
26             #~ See also: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains
27             my @DISALLOWED_TLDS = (
28             '.local', '.arpa', '.invalid', '.localhost', '.internal', '.example', '.alt',
29              
30             # policy could concievably change on ".onion" some day
31             '.onion',
32              
33             #~ NOTE: .test is allowed in testing and devopment. In practical terms
34             #~ "should" "never" actually resolve and get registered in production
35             );
36              
37 22     22 1 290475 sub new( $class, $id ) {
  22         34  
  22         34  
  22         25  
38 22 100       59 throw UnsupportedDomainError('invalid TLD') unless isValidTld($id);
39 19         47 ensureValidHandle($id);
40 10         21 ensureValidHandleRegex($id);
41 10   100     23 CORE::state $warned //= 0;
42 10 100 100     29 if ( $id =~ /\.(test)$/ && !$warned ) {
43 1         8 require Carp;
44 1         158 Carp::carp 'development or testing TLD used in handle: ' . $id;
45 1         5 $warned = 1;
46             }
47 10         72 bless \$id, $class;
48             }
49              
50             # Taken from https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/handle.ts
51             # Handle constraints, in English:
52             # - must be a possible domain name
53             # - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696,
54             # section 2. and RFC-3986, section 3. can now have leading numbers (eg,
55             # 4chan.org)
56             # - "labels" (sub-names) are made of ASCII letters, digits, hyphens
57             # - can not start or end with a hyphen
58             # - TLD (last component) should not start with a digit
59             # - can't end with a hyphen (can end with digit)
60             # - each segment must be between 1 and 63 characters (not including any periods)
61             # - overall length can't be more than 253 characters
62             # - separated by (ASCII) periods; does not start or end with period
63             # - case insensitive
64             # - domains (handles) are equal if they are the same lower-case
65             # - punycode allowed for internationalization
66             # - no whitespace, null bytes, joining chars, etc
67             # - does not validate whether domain or TLD exists, or is a reserved or
68             # special TLD (eg, .onion or .local)
69             # - does not validate punycode
70 291     291 1 869659 sub ensureValidHandle ($handle) {
  291         889  
  291         479  
71              
72             # check that all chars are boring ASCII
73 291 100       2090 throw InvalidHandleError('Disallowed characters in handle (ASCII letters, digits, dashes, periods only)') if $handle !~ /^[a-zA-Z0-9.-]*$/;
74             #
75 243 100       876 throw InvalidHandleError('Handle is too long (253 chars max)') if length $handle > 253;
76             #
77 240         924 my @labels = split /\./, $handle, -1; # negative limit, ftw
78 240 100       748 throw InvalidHandleError('Handle domain needs at least two parts') if scalar @labels < 2;
79 221         912 for my $i ( 0 .. $#labels ) {
80 512         993 my $l = $labels[$i];
81 512 100       1036 throw InvalidHandleError('Handle parts can not be empty') if !length $l;
82 496 100       1075 throw InvalidHandleError('Handle part too long (max 63 chars)') if length $l > 63;
83 494 100       2559 throw InvalidHandleError('Handle parts can not start or end with hyphens') if $l =~ /^-|-$/;
84 475 100 100     2213 throw InvalidHandleError('Handle final component (TLD) must start with ASCII letter') if $i == $#labels && $l !~ /^[a-zA-Z]/;
85             }
86 166         1269 1;
87             }
88              
89 357     357 1 53897 sub ensureValidHandleRegex ($handle) {
  357         765  
  357         685  
90 357 100       4140 throw InvalidHandleError(q[Handle didn't validate via regex])
91             unless $handle =~ /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
92 160 100       545 throw InvalidHandleError('Handle is too long (253 chars max)') if length $handle > 253;
93 157         512 1;
94             }
95              
96 2     2 1 4 sub normalizeHandle ($handle) {
  2         3  
  2         5  
97 2         7 lc $handle;
98             }
99              
100 2     2 1 133922 sub normalizeAndEnsureValidHandle($handle) {
  2         4  
  2         5  
101 2         6 my $normalized = normalizeHandle($handle);
102 2         7 ensureValidHandle($normalized);
103 1         9 $normalized;
104             }
105              
106 0     0 1 0 sub isValidHandle ($handle) {
  0         0  
  0         0  
107 0         0 try {
108 0         0 ensureValidHandle($handle)
109             }
110             catch ($err) { # TODO: I want this to work by checking the type of thrown error but this is perl...
111 0 0       0 if ( $err =~ /Handle/ ) {
112 0         0 return 0;
113             }
114 0         0 die $err;
115             }
116 0         0 1;
117             }
118              
119 22     22 1 30 sub isValidTld($handle) {
  22         27  
  22         31  
120 22         44 for my $tld (@DISALLOWED_TLDS) {
121 163 100       1579 return 0 if $handle =~ /${tld}$/;
122             }
123 19         58 1;
124             }
125              
126             # All fatal errors
127             register 'InvalidHandleError', (), 1;
128             register 'ReservedHandleError', (), 1;
129             register 'UnsupportedDomainError', (), 1;
130             register 'DisallowedDomainError', (), 1;
131             };
132             1;
133             __END__