package Crypt::Perl::X509::Extensions;

use strict;
use warnings;

=encoding utf-8

=head1 NAME

Crypt::Perl::X509::Extensions - extensions list for X.509 certificates

=head1 SYNOPSIS

    #Each object passed should be an instance of a subclass of
    #Crypt::Perl::X509::Extension
    my $exreq = Crypt::Perl::X509::Extensions->new( @EXTN_OBJS );

    #...or:

    my $exreq = Crypt::Perl::X509::Extensions->new(
        [ $extn_type1 => @args1 ],
        [ $extn_type2 => @args2 ],
    );

    #...for example:

    my $exreq = Crypt::Perl::X509::Extensions->new(
        [ 'subjectAltName',
            [ dNSName => 'foo.com' ],
            [ dNSName => 'haha.tld' ],
        ],
    );

=head1 DESCRIPTION

Instances of this class represent the list of extensions in an X.509 (SSL)
certificate.

You probably don’t need to
instantiate this class directly; instead, you can instantiate it
implicitly by listing out arguments to
L<Crypt::Perl::X509v3>’s constructor. See that module’s
L<SYNOPSIS|Crypt::Perl::X509v3/SYNOPSIS> for an example.

Look in the L<Crypt::Perl> distribution’s
C<Crypt::Perl::X509::Extension> namespace for supported extensions.

=cut

use Try::Tiny;

use Module::Load ();

use Crypt::Perl::ASN1 ();

use parent qw(
    Crypt::Perl::ASN1::Encodee
);

use constant OID => '1.2.840.113549.1.9.14';

use constant ASN1 => <<END;
    Extension ::= SEQUENCE {
      extnID    OBJECT IDENTIFIER,
      critical  BOOLEAN OPTIONAL,
      extnValue OCTET STRING
    }

    Extensions ::= SEQUENCE OF Extension
END

my $EXT_BASE = 'Crypt::Perl::X509::Extension';

sub new {
    my ($class, @extensions) = @_;

    if (!@extensions) {
        die Crypt::Perl::X::create('Generic', "Empty “extensions”!");
    }

    for my $ext (@extensions) {
        if (!try { $ext->isa($EXT_BASE) }) {
            if ( 'HASH' eq ref $ext ) {
                if ( !try { $ext->{'extension'}->isa($EXT_BASE) }) {
                    if ( 'ARRAY' ne ref $ext->{'extension'} ) {
                        die Crypt::Perl::X::create('Generic', "“extension” in HASH reference must be ARRAY reference or instance of $EXT_BASE, not “$ext”!");
                    }
                }
            }
            elsif ( 'ARRAY' ne ref $ext ) {
                die Crypt::Perl::X::create('Generic', "Extension must be HASH reference, ARRAY reference, or instance of $EXT_BASE, not “$ext”!");
            }
        }
    }

    return bless \@extensions, $class;
}

sub _new_parse_arrayref {
    my ($ext) = @_;
    my $module = $ext->[0];

    # For the acmeValdation-v1 extension …
    $module =~ tr<-><_>;

    my $class = "Crypt::Perl::X509::Extension::$module";
    Module::Load::load($class);
    return $class->new( @{$ext}[ 1 .. $#$ext ] );
}

sub _encode_params {
    my ($self) = @_;

    my @exts_asn1;

    for my $ext ( @$self ) {
        my ($critical, $real_ext);
        if ('HASH' eq ref $ext) {
            ($critical, $real_ext) = @{$ext}{ qw(critical extension) };
        }
        else {
            $real_ext = $ext;
        }

        if ('ARRAY' eq ref $real_ext) {
            $real_ext = _new_parse_arrayref($real_ext);
        }

        if (!defined $critical) {
            $critical = $real_ext->can('CRITICAL');
            $critical &&= $critical->();
        }

        push @exts_asn1, {
            extnID => $real_ext->OID(),
            ($critical ? (critical => Crypt::Perl::ASN1->ASN_BOOLEAN()) : ()),
            extnValue => $real_ext->encode(),
        },
    };

    return \@exts_asn1;
}

1;