At

 view release on metacpan or  search on metacpan

lib/At/Protocol/Handle.pm  view on Meta::CPAN

package At::Protocol::Handle 1.0 {
    use v5.42;
    use At::Error qw[register throw];
    use parent -norequire => 'Exporter';
    use feature 'try';
    no warnings qw[experimental::try];
    use overload
        '""' => sub ( $s, $u, $q ) {
        $$s;
        };
    our %EXPORT_TAGS = (
        all => [
            our @EXPORT_OK
                = qw[
                ensureValidHandle ensureValidHandleRegex
                normalizeHandle normalizeAndEnsureValidHandle
                isValidHandle isValidTld]
        ]
    );
    #
    my $INVALID_HANDLE = 'handle.invalid';

    #~ Currently these are registration-time restrictions, not protocol-level
    #~ restrictions. We have a couple accounts in the wild that we need to clean up
    #~ before hard-disallow.
    #~ See also: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains
    my @DISALLOWED_TLDS = (
        '.local', '.arpa', '.invalid', '.localhost', '.internal', '.example', '.alt',

        # policy could concievably change on ".onion" some day
        '.onion',

        #~ NOTE: .test is allowed in testing and devopment. In practical terms
        #~ "should" "never" actually resolve and get registered in production
    );

    sub new( $class, $id ) {
        throw UnsupportedDomainError('invalid TLD') unless isValidTld($id);
        ensureValidHandle($id);
        ensureValidHandleRegex($id);
        CORE::state $warned //= 0;
        if ( $id =~ /\.(test)$/ && !$warned ) {
            require Carp;
            Carp::carp 'development or testing TLD used in handle: ' . $id;
            $warned = 1;
        }
        bless \$id, $class;
    }

    # Taken from https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/handle.ts
    # Handle constraints, in English:
    #  - must be a possible domain name
    #    - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696,
    #      section 2. and RFC-3986, section 3. can now have leading numbers (eg,
    #      4chan.org)
    #    - "labels" (sub-names) are made of ASCII letters, digits, hyphens
    #    - can not start or end with a hyphen
    #    - TLD (last component) should not start with a digit
    #    - can't end with a hyphen (can end with digit)
    #    - each segment must be between 1 and 63 characters (not including any periods)
    #    - overall length can't be more than 253 characters
    #    - separated by (ASCII) periods; does not start or end with period
    #    - case insensitive
    #    - domains (handles) are equal if they are the same lower-case
    #    - punycode allowed for internationalization
    #  - no whitespace, null bytes, joining chars, etc
    #  - does not validate whether domain or TLD exists, or is a reserved or
    #    special TLD (eg, .onion or .local)
    #  - does not validate punycode
    sub ensureValidHandle ($handle) {

        # check that all chars are boring ASCII
        throw InvalidHandleError('Disallowed characters in handle (ASCII letters, digits, dashes, periods only)') if $handle !~ /^[a-zA-Z0-9.-]*$/;
        #
        throw InvalidHandleError('Handle is too long (253 chars max)') if length $handle > 253;
        #
        my @labels = split /\./, $handle, -1;    # negative limit, ftw
        throw InvalidHandleError('Handle domain needs at least two parts') if scalar @labels < 2;
        for my $i ( 0 .. $#labels ) {
            my $l = $labels[$i];
            throw InvalidHandleError('Handle parts can not be empty')                             if !length $l;
            throw InvalidHandleError('Handle part too long (max 63 chars)')                       if length $l > 63;
            throw InvalidHandleError('Handle parts can not start or end with hyphens')            if $l                   =~ /^-|-$/;
            throw InvalidHandleError('Handle final component (TLD) must start with ASCII letter') if $i == $#labels && $l !~ /^[a-zA-Z]/;
        }
        1;
    }

    sub ensureValidHandleRegex ($handle) {
        throw InvalidHandleError(q[Handle didn't validate via regex])
            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])?$/;
        throw InvalidHandleError('Handle is too long (253 chars max)') if length $handle > 253;
        1;
    }

    sub normalizeHandle ($handle) {
        lc $handle;
    }

    sub normalizeAndEnsureValidHandle($handle) {
        my $normalized = normalizeHandle($handle);
        ensureValidHandle($normalized);
        $normalized;
    }

    sub isValidHandle ($handle) {
        try {
            ensureValidHandle($handle)
        }
        catch ($err) {    # TODO: I want this to work by checking the type of thrown error but this is perl...
            if ( $err =~ /Handle/ ) {
                return 0;
            }



( run in 1.309 second using v1.01-cache-2.11-cpan-39bf76dae61 )