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   276833 use v5.42;
  5         20  
3 5     5   29 no warnings qw[experimental::builtin experimental::try];
  5         10  
  5         307  
4 5     5   570 use At::Error qw[register throw];
  5         11  
  5         28  
5 5     5   26 use feature 'try';
  5         10  
  5         789  
6 5     5   604 use parent -norequire => 'Exporter';
  5         486  
  5         35  
7             use overload
8 4     4   1200 '""' => sub ( $s, $u, $q ) {
  4         6  
  4         5  
  4         6  
  4         2  
9 4         28 join '.', @$s;
10 5     5   648 };
  5         52  
  5         47  
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 16 sub new( $class, $nsid ) {
  12         19  
  12         13  
  12         13  
22 12         26 ensureValidNsid($nsid);
23 12         96 bless [ split /\./, $nsid, -1 ], $class;
24             }
25              
26 12     12 1 383950 sub parse($nsid) {
  12         23  
  12         17  
27 12         36 __PACKAGE__->new($nsid);
28             }
29              
30 6     6 1 8031 sub create( $authority, $name ) {
  6         13  
  6         9  
  6         7  
31 6         38 parse join '.', reverse( split( /\./, $authority, -1 ) ), $name;
32             }
33 4     4 1 15 sub authority($s) { join '.', reverse splice( @$s, 0, -1 ); }
  4         7  
  4         4  
  4         87  
34              
35 4     4 1 7 sub name($s) {
  4         4  
  4         4  
36 4         22 @$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 277251 sub ensureValidNsid($nsid) {
  217         417  
  217         280  
51              
52             # check that all chars are boring ASCII
53 217 100       1280 throw InvalidNsidError('Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)') unless $nsid =~ /^[a-zA-Z0-9.-]*$/;
54             #
55 179 100       528 throw InvalidNsidError('NSID is too long (317 chars max)') if length $nsid > 253 + 1 + 63;
56 177         538 my @labels = split /\./, $nsid, -1; # negative length, ftw
57              
58             #
59 177 100       498 throw InvalidNsidError('NSID needs at least three parts') if scalar @labels < 3;
60             #
61 167         527 for my $i ( 0 .. $#labels ) {
62 662         805 my $l = $labels[$i];
63 662 100       951 throw InvalidNsidError('NSID parts can not be empty') unless length $l;
64 658 100       1027 throw InvalidNsidError('NSID part too long (max 63 chars)') if length $l > 63;
65 654 100       1909 throw InvalidNsidError('NSID parts can not start or end with hyphen') if $l =~ /^-|-$/;
66 650 100 100     1499 throw InvalidNsidError('NSID first part may not start with a digit') if $i == 0 && $l =~ /^[0-9]/;
67 646 100 100     2041 throw InvalidNsidError('NSID name part must be letters or digits') if $i == $#labels && $l !~ /^[a-zA-Z][a-zA-Z0-9]*$/;
68             }
69 146         547 1;
70             }
71              
72 173     173 1 20089 sub ensureValidNsidRegex ($nsid) {
  173         332  
  173         311  
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       2483 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       276 throw InvalidNsidError('NSID is too long (317 chars max)') if length $nsid > 253 + 1 + 63;
79 114         305 1;
80             }
81             #
82             register 'InvalidNsidError', 1;
83             };
84             1;
85             __END__