File Coverage

lib/At/Protocol/NSID.pm
Criterion Covered Total %
statement 67 74 90.5
branch 20 20 100.0
condition 6 6 100.0
subroutine 14 15 93.3
pod 8 8 100.0
total 115 123 93.5


line stmt bran cond sub pod time code
1             package At::Protocol::NSID 1.0 { # https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/nsid.ts
2 5     5   212369 use v5.42;
  5         18  
3 5     5   55 no warnings qw[experimental::builtin experimental::try];
  5         8  
  5         341  
4 5     5   475 use At::Error qw[register throw];
  5         11  
  5         30  
5 5     5   33 use feature 'try';
  5         9  
  5         818  
6 5     5   553 use parent -norequire => 'Exporter';
  5         373  
  5         84  
7             use overload
8 4     4   1309 '""' => sub ( $s, $u, $q ) {
  4         6  
  4         6  
  4         6  
  4         3  
9 4         28 join '.', @$s;
10 5     5   746 };
  5         12  
  5         56  
11             our %EXPORT_TAGS = ( all => [ our @EXPORT_OK = qw[parse create isValid ensureValidNsid ensureValidNsidRegex] ] );
12              
13             #~ Grammar:
14             #~ 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"
15             #~ number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0"
16             #~ delim = "."
17             #~ segment = alpha *( alpha / number / "-" )
18             #~ authority = segment *( delim segment )
19             #~ name = alpha *( alpha / number )
20             #~ nsid = authority delim name
21 12     12 1 19 sub new( $class, $nsid ) {
  12         18  
  12         15  
  12         14  
22 12         31 ensureValidNsid($nsid);
23 12         126 bless [ split /\./, $nsid, -1 ], $class;
24             }
25              
26 12     12 1 350351 sub parse($nsid) {
  12         21  
  12         16  
27 12         62 __PACKAGE__->new($nsid);
28             }
29              
30 6     6 1 8028 sub create( $authority, $name ) {
  6         14  
  6         11  
  6         8  
31 6         41 parse join '.', reverse( split( /\./, $authority, -1 ) ), $name;
32             }
33 4     4 1 7 sub authority($s) { join '.', reverse splice( @$s, 0, -1 ); }
  4         4  
  4         5  
  4         72  
34              
35 4     4 1 6 sub name($s) {
  4         7  
  4         5  
36 4         25 @$s[-1];
37             }
38              
39 0     0 1 0 sub isValid($nsid) {
  0         0  
  0         0  
40 0         0 try {
41 0         0 parse($nsid);
42 0         0 return 1;
43             }
44 0         0 catch ($err) { return 0; }
45             }
46              
47             #~ Human readable constraints on NSID:
48             #~ - a valid domain in reversed notation
49             #~ - followed by an additional period-separated name, which is camel-case letters
50 217     217 1 415062 sub ensureValidNsid($nsid) {
  217         390  
  217         327  
51              
52             # check that all chars are boring ASCII
53 217 100       1335 throw InvalidNsidError('Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)') unless $nsid =~ /^[a-zA-Z0-9.-]*$/;
54             #
55 179 100       1146 throw InvalidNsidError('NSID is too long (317 chars max)') if length $nsid > 253 + 1 + 63;
56 177         554 my @labels = split /\./, $nsid, -1; # negative length, ftw
57              
58             #
59 177 100       494 throw InvalidNsidError('NSID needs at least three parts') if scalar @labels < 3;
60             #
61 167         596 for my $i ( 0 .. $#labels ) {
62 662         1074 my $l = $labels[$i];
63 662 100       1189 throw InvalidNsidError('NSID parts can not be empty') unless length $l;
64 658 100       1188 throw InvalidNsidError('NSID part too long (max 63 chars)') if length $l > 63;
65 654 100       2346 throw InvalidNsidError('NSID parts can not start or end with hyphen') if $l =~ /^-|-$/;
66 650 100 100     1749 throw InvalidNsidError('NSID first part may not start with a digit') if $i == 0 && $l =~ /^[0-9]/;
67 646 100 100     2183 throw InvalidNsidError('NSID name part must be letters or digits') if $i == $#labels && $l !~ /^[a-zA-Z][a-zA-Z0-9]*$/;
68             }
69 146         715 1;
70             }
71              
72 173     173 1 36022 sub ensureValidNsidRegex ($nsid) {
  173         306  
  173         215  
73              
74             #~ simple regex to enforce most constraints via just regex and length.
75             #~ hand wrote this regex based on above constraints
76 173 100       2651 throw InvalidNsidError(q[NSID didn't validate via regex])
77             unless $nsid =~ /^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$/;
78 116 100       375 throw InvalidNsidError('NSID is too long (317 chars max)') if length $nsid > 253 + 1 + 63;
79 114         318 1;
80             }
81             #
82             register 'InvalidNsidError', 1;
83             };
84             1;
85             __END__