package MaxMind::DB::Verifier;
{
$MaxMind::DB::Verifier::VERSION = '0.040003';
}
BEGIN {
$MaxMind::DB::Verifier::AUTHORITY = 'cpan:TJMATHER';
}
use strict;
use warnings;
use namespace::autoclean;
use autodie;
use Getopt::Long;
use IO::File;
use MaxMind::DB::Common qw( DATA_SECTION_SEPARATOR_SIZE );
use MaxMind::DB::Metadata;
use MaxMind::DB::Types qw( Bool HashRef Int Str );
use Try::Tiny;
use Moo;
use MooX::StrictConstructor;
with 'MaxMind::DB::Reader::Role::NodeReader',
'MaxMind::DB::Reader::Role::HasDecoder';
has file => (
is => 'ro',
isa => Str,
required => 1,
);
has quiet => (
is => 'ro',
isa => Bool,
default => 0,
);
has _max_pointer_in_search_tree => (
is => 'ro',
isa => Int,
init_arg => undef,
lazy => 1,
builder => '_build_max_pointer_in_search_tree',
);
has _search_tree_data_pointers => (
is => 'ro',
isa => HashRef,
init_arg => undef,
default => sub { {} },
);
has _error_count => (
is => 'rw',
writer => '_set_error_count',
isa => Int,
init_arg => undef,
default => 0,
);
sub new_with_options {
my $class = shift;
my %opts;
GetOptions(
\%opts,
'file:s', 'quiet'
);
return $class->new(%opts);
}
sub run {
my $self = shift;
$self->verify();
}
sub verify {
my $self = shift;
STDOUT->autoflush(1);
for my $thing (qw( metadata search_tree data_section )) {
my $display_name = $thing =~ s/_/ /gr;
$self->_output("Verifying $display_name ...");
my $method = '_verify_' . $thing;
$self->$method();
if ( $self->_error_count() ) {
warn "Cannot continue verification with bad $display_name\n";
return 0;
}
else {
$self->_output(' ok');
}
}
return 1;
}
sub _verify_metadata {
my $self = shift;
my $node_count = $self->node_count();
unless ( $node_count > 0 ) {
$self->_verification_error(
"The metadata specified the node count was $node_count - we expect a positive number"
);
}
my $record_size = $self->record_size();
unless ( $record_size >= 24
&& $record_size <= 64
&& $record_size % 4 == 0 ) {
$self->_verification_error(
"The metadata specified a record size of $record_size - we expect a number from 24 to 64 that is divisible by 4"
);
}
}
sub _verify_search_tree {
my $self = shift;
$self->_output(' looking for data section separator');
$self->_verify_data_section_separator();
$self->_output(' checking all nodes');
$self->_verify_all_nodes();
return;
}
my $DataSectionSeparator = "\0" x 16;
sub _verify_data_section_separator {
my $self = shift;
my $marker;
$self->_read( \$marker, $self->_search_tree_size(), 16 );
unless ( $marker eq $DataSectionSeparator ) {
$self->_verification_error(
'Did not find the data section start marker at the expected place'
);
}
}
sub _verify_all_nodes {
my $self = shift;
my $expected_count = $self->node_count();
my $node_num = 0;
while ( $node_num < $expected_count ) {
$self->_verify_node($node_num);
$node_num++;
if ( $node_num % 10000 == 0 ) {
$self->_output(" checked $node_num nodes out of $expected_count");
}
}
}
{
my @directions = ( 'left', 'right' );
sub _verify_node {
my $self = shift;
my $node_num = shift;
my $node_count = $self->node_count();
my %records;
@records{@directions} = $self->_read_node($node_num);
for my $dir (@directions) {
if ( $records{$dir} == 0 ) {
$self->_verification_error(
"Node $node_num, $dir record == 0");
}
next if $records{$dir} <= $self->node_count();
my $resolved
= ( $records{$dir} - $self->node_count() )
+ $self->_search_tree_size();
if ( $resolved <= $self->_max_pointer_in_search_tree() ) {
$self->_search_tree_data_pointers()->{$resolved}
= [ $node_num, $dir ];
}
else {
$self->_verification_error(
"Node $node_num, $dir record points past the end of the data section"
);
}
}
return;
}
}
sub _verify_data_section {
my $self = shift;
my $pointers = $self->_search_tree_data_pointers();
my $pointer_count = scalar keys %{$pointers};
my $decoder = $self->_decoder();
my $data_section_start
= $self->_search_tree_size() + DATA_SECTION_SEPARATOR_SIZE;
my $offset = $data_section_start;
my $data_section_end = $self->_data_section_end();
while ( $offset < $data_section_end ) {
my ( $data, $new_offset );
try {
( $data, $new_offset ) = $self->_decoder()->decode($offset);
}
catch {
$self->_verification_error(
"Error stepping through the data section at offset $offset - $_"
);
};
last unless $data;
if ( $new_offset <= $offset ) {
$self->_verification_error(
"Something weird happened in the decoder - the offset went from $offset to $new_offset"
);
}
if ( $pointers->{$offset} ) {
delete $pointers->{$offset};
}
else {
$self->_verification_error(
"Found a chunk of data in the section (file offset $offset) that the search tree does not point to"
);
}
$offset = $new_offset;
}
if ( my $final_count = keys %{$pointers} ) {
$self->_verification_error(
"Found $final_count pointers (of $pointer_count) in the search tree"
. " that we didn't see while stepping through the data section"
);
}
}
sub _verification_error {
my $self = shift;
my $error = shift;
$self->_set_error_count( $self->_error_count() + 1 );
warn "$error\n";
}
sub _output {
my $self = shift;
return if $self->quiet();
print "$_[0]\n";
}
sub _build_data_source {
my $self = shift;
open my $fh, '<:raw', $self->file();
return $fh;
}
sub _build_max_pointer_in_search_tree {
my $self = shift;
# We should not find something that resolves past the last byte of the
# data section.
return $self->_data_section_end() - 1;
}
__PACKAGE__->meta()->make_immutable();
1;