package Zonemaster::LDNS::Packet;

use strict;
use warnings;

use Zonemaster::LDNS;
use Zonemaster::LDNS::RRList;

use MIME::Base64;

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

    return {
        'Zonemaster::LDNS::Packet' => {
            data       => encode_base64( $self->wireformat, '' ),
            answerfrom => $self->answerfrom,
            timestamp  => $self->timestamp,
            querytime  => $self->querytime,
        }
    };
}

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

    return $self->wireformat;
}

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

    my @records = sort $self->question();

    return Zonemaster::LDNS::RRList->new( \@records );
}

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

    my @records = $self->answer_unfiltered;

    for ( my $i = $#records ; $i >= 0 ; --$i ) {
        if ( !$records[$i]->check_rd_count()
            || ( $records[$i]->type() eq 'DNSKEY' && $records[$i]->keysize() == -1 ) )
        {
            splice @records, $i, 1;
        }
    }

    return @records;
}

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

    my @records = sort $self->answer();

    return Zonemaster::LDNS::RRList->new( \@records );
}

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

    my @records = $self->authority_unfiltered;

    for ( my $i = $#records ; $i >= 0 ; --$i ) {
        if ( !$records[$i]->check_rd_count() ) {
            splice @records, $i, 1;
        }
    }

    return @records;
}

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

    my @records = sort $self->authority();

    return Zonemaster::LDNS::RRList->new( \@records );
}

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

    my @records = $self->additional_unfiltered;

    for ( my $i = $#records ; $i >= 0 ; --$i ) {
        if ( !$records[$i]->check_rd_count() ) {
            splice @records, $i, 1;
        }
    }

    return @records;
}

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

    my @records = sort $self->additional();

    return Zonemaster::LDNS::RRList->new( \@records );
}

sub first_ede {
    my ( $self, @args ) = @_;
    if ( scalar @args > 0 ) {
        my ( $ede, $text ) = @args;

        # Encode string into UTF-8 byte sequence if not already done so
        if ( defined $text and utf8::is_utf8( $text ) ) {
            $text = Encode::encode( 'UTF-8', $text );
            return $self->_set_first_ede( $ede, $text );
        }
        else {
            return $self->_set_first_ede( @args );
        }
    }
    else {
        return $self->_get_first_ede();
    }
}

1;

=encoding UTF-8

=head1 NAME

Zonemaster::LDNS::Packet - objects representing DNS packets

=head1 SYNOPSIS

    my $p = $resolver->query('www.iis.se');
    foreach my $rr ($p->answer) {
        say $rr->string if $rr->type eq 'A';
    }

=head1 CLASS METHODS

=over

=item new($name, $type, $class)

Creates a new L<Zonemaster::LDNS::Packet> object, holding nothing but a query record for the provided triplet.
C<$name> corresponds to the QNAME. C<$type> and C<$class> are optional, and default to A and IN respectively.

=item new_from_wireformat($data)

Creates a new L<Zonemaster::LDNS::Packet> object from the given wireformat data, if possible. Throws an exception if not.

=back

=head1 INSTANCE METHODS

=over

=item rcode([$string])

Returns the packet RCODE. If given an argument, tries to set the RCODE to the
relevant value. If the given string isn't recognized as an RCODE, an exception
will be thrown.

=item opcode([$string])

Returns the packet OPCODE. If given an argument, tries to set the OPCODE to the
relevant value. If the given string isn't recognized as an OPCODE, an exception
will be thrown.

=item id([$value])

Returns the packet id number. If given an argument, sets the ID value to that
value.

=item qr()

=item aa()

=item tc()

=item rd()

=item cd()

=item ra()

=item ad()

=item do()

Reads and/or sets the equivalently named flags.

=item size()

Returns the length of the packet's wireformat form in octets.

=item edns_size()

Gets and/or sets the EDNS0 UDP size.

=item edns_rcode()

Gets and/or sets the EDNS0 Extended RCODE field.

=item edns_z()

Gets and/or sets the EDNS0 Z bits.

=item edns_data()

Gets and/or sets the EDNS0 RDATA. See LDNS.xs for more details.

=item needs_edns()

This method returns true if the packet has the DO flag set, an EDNS0 size set,
and EDNS0 extended RCODE set or if the OPT pseudo-RR has one or more RDATA
fields. It can fail to correctly flag a packet with an OPT pseudo-RR as having
EDNS, if the pseudo-RR specifies an UDP size of zero, an extended RCODE of zero
and the DO flag is unset. Since any UDP size less than 512 must be interpreted
as 512, packets like that should be very rare in practice if they exist at all.

Note that the OPT pseudo-RR is not visible as an RR in the packet, nor is it
included in the RR count header fields.

=item has_edns()

An alias for needs_edns().

=item edns_version($version)

Get or set the EDNS version in the packet. For incoming packets, returns 0 if
the packet does not have an OPT pseudo-RR and 0 if it's an EDNS0 packet. It's
thus rather pointless until such time as EDNS1 is defined.

=item set_edns_present

Set edns_present flag to true.

This flag can be set when creating a packet with EDNS corner cases data that
could not be detected by need_edns/has_edns methods.

When set, need_edns/has_edns methods return true value.

=item unset_edns_present

Set edns_present flag to false.

=item querytime([$value])

Returns the time the query this packet is the answer to took to execute, in
milliseconds. If given a value, sets the querytime to that value.

=item answerfrom($ipaddr)

Returns and optionally sets the IP address the packet was received from. If an attempt is made to set it to a string that cannot be parsed as an
IPv4 or IPv6 address, an exception is thrown.

=item timestamp($time)

The time when the query was sent or received (the ldns docs don't specify), as a floating-point value on the Unix time_t scale (that is, the same
kind of value used by L<Time::HiRes::time()>). Conversion effects between floating-point and C<struct timeval> means that the precision of the
value is probably not reliable at the microsecond level, even if you computer's clock happen to be.

=item all()

Returns a L<Zonemaster::LDNS::RRList> object, containing the resource records from all sections except for the question section.

=item question()

Returns a list of objects representing the RRs in the question section. They will be of classes appropriate to their types, but all will have
L<Zonemaster::LDNS::RR> as a base class.

=item question_rrlist()

Similar to L<question()>, but instead returns a single (and sorted) L<Zonemaster::LDNS::RRList> object.

=item answer()

Similar to L<answer_unfiltered()>, but ignores incomplete resource records.

=item answer_rrlist()

Similar to L<answer()>, but instead returns a single (and sorted) L<Zonemaster::LDNS::RRList> object.

=item answer_unfiltered()

Returns a list of objects representing the RRs in the answer section. They will be of classes appropriate to their types, but all will have
L<Zonemaster::LDNS::RR> as a base class.

=item authority()

Similar to L<authority_unfiltered()>, but ignores incomplete resource records.

=item authority_rrlist()

Similar to L<authority()>, but instead returns a single (and sorted) L<Zonemaster::LDNS::RRList> object.

=item authority_unfiltered()

Returns a list of objects representing the RRs in the authority section. They will be of classes appropriate to their types, but all will have
L<Zonemaster::LDNS::RR> as a base class.

=item additional()

Similar to L<additional_unfiltered()>, but ignores incomplete resource records.

=item additional_rrlist()

Similar to L<additional()>, but instead returns a single (and sorted) L<Zonemaster::LDNS::RRList> object.

=item additional_unfiltered()

Returns a list of objects representing the RRs in the additional section. They will be of classes appropriate to their types, but all will have
L<Zonemaster::LDNS::RR> as a base class.

=item unique_push($section, $rr)

Push an RR object into the given section, if an identical RR isn't already present. If the section isn't one of "question", "answer", "authority"
or "additional" an exception will be thrown. C<$rr> must be a L<Zonemaster::LDNS::RR> subclass.

=item string()

Returns a string with the packet and its contents in common presentation format.

=item wireformat()

Returns a Perl string holding the packet in wire format.

=item type()

Returns the ldns library's guess as to the content of the packet. One of the strings C<question>, C<referral>, C<answer>, C<nxdomain>, C<nodata> or C<unknown>.

=item first_ede( [ $error_code, [ $extra_text ] ] )

Gets (if called without any arguments) or sets (if called with one or two
arguments) the first Extended DNS Error (EDE) in the packet.

As a setter, always returns C<undef>.

As a getter, the return value depends on the context.

In scalar context, returns the value of the EDE INFO-CODE field, or C<undef>
if the packet contains no EDE.

In list context, returns:

=over

=item *

an empty list if there is no EDE;

=item *

a list of one item, the EDE INFO-CODE, if there is an EDE without EXTRA-TEXT;

=item *

a list of two items, the EDE INFO-CODE and the EXTRA-TEXT, if there is an EDE with EXTRA-TEXT.

=back

This list can be unpacked into a pair of variables, like this:

    my ( $code, $extra_text ) = $packet->first_ede();

The EDE EXTRA-TEXT is specified in RFC 8914 to be UTF-8 encoded text. When
retrieving the EXTRA-TEXT from a packet, UTF-8 valid text is returned as a
Unicode character string, and UTF-8 invalid text, although forbidden, is
returned as a (binary) byte string. Trailing NUL bytes are preserved both when
setting the EXTRA-TEXT in a packet or when getting a packet’s EXTRA-TEXT.



=back
