From c30799614b579c1070c84c1a10e093820b20d43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Mon, 21 Feb 2022 16:35:00 +0100 Subject: [PATCH 01/26] add new basic00 implementation --- lib/Zonemaster/Engine/Sanitization.pm | 91 ++++++++ lib/Zonemaster/Engine/Sanitization/Errors.pm | 211 +++++++++++++++++++ t/sanitization.t | 105 +++++++++ 3 files changed, 407 insertions(+) create mode 100644 lib/Zonemaster/Engine/Sanitization.pm create mode 100644 lib/Zonemaster/Engine/Sanitization/Errors.pm create mode 100644 t/sanitization.t diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm new file mode 100644 index 000000000..9481ad2ba --- /dev/null +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -0,0 +1,91 @@ +package Zonemaster::Engine::Sanitization; + +use 5.014002; + +use strict; +use warnings; + +use Encode; +use Data::Dumper; +use Zonemaster::LDNS; + +use Zonemaster::Engine::Sanitization::Errors; + +my $VALID_ASCII = q/^[A-Za-z0-9\/\-_]+$/; +my $FULL_STOP = q/\x{002E}/; + +sub sanitize_label { + my ($label) = shift @_; + + my $alabel = eval { + Encode::encode('ascii', $label, Encode::FB_CROAK); + }; + + # Not ascii string, assume u-label + if ($@) { + $alabel = eval { + Zonemaster::LDNS::to_idn($label) + }; + # TODO: handle when libidn not installed? + if ($@) { + die Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel->new({ dlabel => Encode::encode_utf8($label) }); + } + } else { + if ( $alabel !~ $VALID_ASCII) { + die Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii->new({ dlabel => $alabel }); + } + $alabel = lc $alabel; + } + + if ( length($alabel) > 63) { + die Zonemaster::Engine::Exception::DomainSanitization::LabelTooLong->new({ dlabel => $alabel }); + } + return $alabel; +} + +sub sanitize_name { + my ($name) = shift @_; + + # TODO: Allow for different encoding? + my $uname = eval { + Encode::decode('UTF-8', $name, Encode::FB_CROAK); + }; + + if ($@) { + die Zonemaster::Engine::Exception::DomainSanitization::InvalidEncoding->new(); + } + + if (length($uname) == 0) { + die Zonemaster::Engine::Exception::DomainSanitization::EmptyDomainName->new(); + } + + # Replace fullwidth full stop, ideographic full stop and halfwidth ideographic full stop with full stop + $uname =~ s/[\x{FF0E}\x{3002}\x{FF61}]/\x{002E}/g; + + if ( $uname eq '.' ) { + return $uname; + } + + if ($uname =~ /^${FULL_STOP}/) { + die Zonemaster::Engine::Exception::DomainSanitization::InitialDot->new(); + } + + if ($uname =~ /${FULL_STOP}{2,}/ ) { + die Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots->new(); + } + + $uname =~ s/${FULL_STOP}$//g; + + my @labels = split $FULL_STOP, $uname; + @labels = map { sanitize_label($_) } @labels; + + my $final_name = join '.', @labels; + + if (length($final_name) > 253) { + die Zonemaster::Engine::Exception::DomainSanitization::DomainNameTooLong->new(); + } + + return $final_name; +} + +1; diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm new file mode 100644 index 000000000..bbb79f00c --- /dev/null +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -0,0 +1,211 @@ +package Zonemaster::Engine::Exception::DomainSanitizationError; + +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception/); +use Zonemaster::Engine::Exception; + + +has 'type' => ( is => 'ro', isa => 'Str' ); + +sub new { + my $proto = shift; + my $obj = __PACKAGE__->SUPER::new(@_); + my $class = ref $proto || $proto; + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::InitialDot; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'INITIAL_DOT'; + $params->{message} = 'Domain name starts with dot.'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'REPEATED_DOTS'; + $params->{message} = 'Domain name has repeated dots.'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + +has 'dlabel' => ( is => 'ro', isa => 'Str' ); + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'INVALID_ASCII'; + $params->{message} = 'Domain name has an ASCII label with a character not permitted.'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::InvalidEncoding; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + +has 'dlabel' => ( is => 'ro', isa => 'Str' ); + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'INVALID_ENCODING'; + $params->{message} = 'Domain name has characters that can not be decoded'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + +has 'dlabel' => ( is => 'ro', isa => 'Str' ); + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'INVALID_U_LABEL'; + $params->{message} = 'Domain name has a non-ASCII label which is not a valid U-label.'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::LabelTooLong; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + +has 'dlabel' => ( is => 'ro', isa => 'Str' ); + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'LABEL_TOO_LONG'; + $params->{message} = 'Domain name has a label that is too long (more than 63 characters).'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::DomainNameTooLong; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'DOMAIN_NAME_TOO_LONG'; + $params->{message} = 'Domain name is too long (more than 253 characters with no final dot).'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +package Zonemaster::Engine::Exception::DomainSanitization::EmptyDomainName; +use 5.014002; + +use strict; +use warnings; + +use Class::Accessor "antlers"; + +extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); + + +sub new { + my $proto = shift; + my $params = shift; + + $params->{tag} = 'EMPTY_DOMAIN_NAME'; + $params->{message} = 'Domain name is empty.'; + + my $class = ref $proto || $proto; + my $obj = __PACKAGE__->SUPER::new($params); + return bless $obj, $class; +} + +1; diff --git a/t/sanitization.t b/t/sanitization.t new file mode 100644 index 000000000..bf265d789 --- /dev/null +++ b/t/sanitization.t @@ -0,0 +1,105 @@ +use Test::More; +use Test::Exception; + +use Data::Dumper; + +BEGIN { use_ok( 'Zonemaster::Engine::Sanitization' ); } + +subtest 'Valid domains' => sub { + my %input_domains = ( + # Roots + '.' => '.', # Full stop + '.' => '.', # Fullwidth full stop + '。' => '.', # Ideographic full stop + '。' => '.', # Halfwidth ideographic full stop + + # Mixed dots with trailing dot + 'example。com.' => 'example.com', + 'example。com.' => 'example.com', + 'sub.example.com。' => 'sub.example.com', + 'sub.example.com。' => 'sub.example.com', + + # Mixed dots without trailing dot + 'example。com' => 'example.com', + 'example。com' => 'example.com', + 'sub.example.com' => 'sub.example.com', + 'sub.example.com' => 'sub.example.com', + + # Domains with U-Labels + 'café.example.com' => 'xn--caf-dma.example.com', + 'エグザンプル。example。com' => 'xn--ickqs6k2dyb.example.com', + '🦈.example.com' => 'xn--7s9h.example.com', + 'αβγδε.example.com' => 'xn--mxacdef.example.com', + + # Domains with uppercase unicode + 'CafÉ.example.com' => 'xn--caf-dma.example.com', + 'ΑβΓΔε.example.com' => 'xn--mxacdef.example.com', + # TODO: special I case + + # All ascii domains (lowercase) + 'example.com' => 'example.com', + '0/28.2.0.192.example.com' => '0/28.2.0.192.example.com', + '_http._tcp.example.com.' => '_http._tcp.example.com', + 'sub-domain.example.com' => 'sub-domain.example.com', + + # All ascii domains with uppercase characters + 'suB-doMaIN.ExamPlE.cOm' => 'sub-domain.example.com', + + # Single label domains + 'test' => 'test', + 'テスト' => 'xn--zckzah', + + # Length limits + "a" x 63 . ".example.com" => "a" x 63 . ".example.com", + # this is 253 characters + ("a" x 15 . ".") x 15 . "b" . ".example.com" => ("a" x 15 . ".") x 15 . "b" . ".example.com", + ); + + while (($domain, $expected_output) = each (%input_domains)) { + subtest 'Domain: ' . $domain => sub { + my $output; + lives_ok(sub { + $output = Zonemaster::Engine::Sanitization::sanitize_name($domain); + }, 'correct domain should live'); + is($output, $expected_output, 'Match expected domain') or diag($output); + } + } +}; + +subtest 'Bad domains' => sub { + my %input_domains = ( + # Bad UTF-8, taken from the examples listed at https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + "\xfc\x80\x80\x80\x80\xaf" => 'Zonemaster::Engine::Exception::DomainSanitization::InvalidEncoding', + + # Empty labels + '.。.' => 'Zonemaster::Engine::Exception::DomainSanitization::InitialDot', + 'example。.com.' => 'Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots', + 'example。com.。' => 'Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots', + '..example。com' => 'Zonemaster::Engine::Exception::DomainSanitization::InitialDot', + + # Bad ascii + 'bad:%;!$.example.com.' => 'Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii', + + # Label to long + "a" x 64 . ".example.com" => 'Zonemaster::Engine::Exception::DomainSanitization::LabelTooLong', + # Length too long after idn conversion (libidn fails) + 'チョコレート' x 8 . 'a' . '.example.com' => 'Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel', + + # Domain to long + # this is 254 characters + ("a" x 15 . ".") x 15 . "bc" . ".example.com" => 'Zonemaster::Engine::Exception::DomainSanitization::DomainNameTooLong', + + # Empty domain + '' => 'Zonemaster::Engine::Exception::DomainSanitization::EmptyDomainName', + ); + + while (($domain, $error) = each (%input_domains)) { + subtest "Domain: $domain ($error)" => sub { + throws_ok (sub { + Zonemaster::Engine::Sanitization::sanitize_name($domain); + }, $error, 'invalid domain should throw' ); + + print Dumper "$@", $@->{dlabel}; + } + } +}; From 94ce900947b9c8529c0b8228655d0ba30297f1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Mon, 21 Feb 2022 16:57:05 +0100 Subject: [PATCH 02/26] fix tests --- t/sanitization.t | 2 ++ 1 file changed, 2 insertions(+) diff --git a/t/sanitization.t b/t/sanitization.t index bf265d789..9f1356980 100644 --- a/t/sanitization.t +++ b/t/sanitization.t @@ -103,3 +103,5 @@ subtest 'Bad domains' => sub { } } }; + +done_testing; From ede669a3d2e1ef3916f8b3006ff40296ff480833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 1 Mar 2022 17:02:40 +0100 Subject: [PATCH 03/26] make globals readonly --- lib/Zonemaster/Engine/Sanitization.pm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm index 9481ad2ba..100cbbeb7 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -6,16 +6,16 @@ use strict; use warnings; use Encode; -use Data::Dumper; +use Readonly; use Zonemaster::LDNS; use Zonemaster::Engine::Sanitization::Errors; -my $VALID_ASCII = q/^[A-Za-z0-9\/\-_]+$/; -my $FULL_STOP = q/\x{002E}/; +Readonly my $VALID_ASCII => q/^[A-Za-z0-9\/\-_]+$/; +Readonly my $FULL_STOP => q/\x{002E}/; sub sanitize_label { - my ($label) = shift @_; + my ( $label ) = @_; my $alabel = eval { Encode::encode('ascii', $label, Encode::FB_CROAK); @@ -44,7 +44,7 @@ sub sanitize_label { } sub sanitize_name { - my ($name) = shift @_; + my ( $name ) = @_; # TODO: Allow for different encoding? my $uname = eval { From 73e8c8e9745e06998befda22c1231931a4eb9faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 1 Mar 2022 17:03:44 +0100 Subject: [PATCH 04/26] assume input is decoded fix tests --- lib/Zonemaster/Engine/Sanitization.pm | 11 +-------- lib/Zonemaster/Engine/Sanitization/Errors.pm | 24 -------------------- t/sanitization.t | 5 ++-- 3 files changed, 3 insertions(+), 37 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm index 100cbbeb7..a8c24e14c 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -44,16 +44,7 @@ sub sanitize_label { } sub sanitize_name { - my ( $name ) = @_; - - # TODO: Allow for different encoding? - my $uname = eval { - Encode::decode('UTF-8', $name, Encode::FB_CROAK); - }; - - if ($@) { - die Zonemaster::Engine::Exception::DomainSanitization::InvalidEncoding->new(); - } + my ( $uname ) = @_; if (length($uname) == 0) { die Zonemaster::Engine::Exception::DomainSanitization::EmptyDomainName->new(); diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index bbb79f00c..073a0cf00 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -90,30 +90,6 @@ sub new { return bless $obj, $class; } -package Zonemaster::Engine::Exception::DomainSanitization::InvalidEncoding; -use 5.014002; - -use strict; -use warnings; - -use Class::Accessor "antlers"; - -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); - -has 'dlabel' => ( is => 'ro', isa => 'Str' ); - -sub new { - my $proto = shift; - my $params = shift; - - $params->{tag} = 'INVALID_ENCODING'; - $params->{message} = 'Domain name has characters that can not be decoded'; - - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); - return bless $obj, $class; -} - package Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel; use 5.014002; diff --git a/t/sanitization.t b/t/sanitization.t index 9f1356980..2c21b86cb 100644 --- a/t/sanitization.t +++ b/t/sanitization.t @@ -3,6 +3,8 @@ use Test::Exception; use Data::Dumper; +use utf8; + BEGIN { use_ok( 'Zonemaster::Engine::Sanitization' ); } subtest 'Valid domains' => sub { @@ -68,9 +70,6 @@ subtest 'Valid domains' => sub { subtest 'Bad domains' => sub { my %input_domains = ( - # Bad UTF-8, taken from the examples listed at https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt - "\xfc\x80\x80\x80\x80\xaf" => 'Zonemaster::Engine::Exception::DomainSanitization::InvalidEncoding', - # Empty labels '.。.' => 'Zonemaster::Engine::Exception::DomainSanitization::InitialDot', 'example。.com.' => 'Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots', From cb40bd8f9dde1aef925de80a9d9ba15b7278225a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 1 Mar 2022 17:07:26 +0100 Subject: [PATCH 05/26] swap use and extends --- lib/Zonemaster/Engine/Sanitization/Errors.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index 073a0cf00..61ccf0387 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -7,8 +7,8 @@ use warnings; use Class::Accessor "antlers"; -extends(qw/Zonemaster::Engine::Exception/); use Zonemaster::Engine::Exception; +extends(qw/Zonemaster::Engine::Exception/); has 'type' => ( is => 'ro', isa => 'Str' ); From ff4a1342a9c0dfadf29b50db2de09f1501869bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 1 Mar 2022 17:12:24 +0100 Subject: [PATCH 06/26] remove debugging --- t/sanitization.t | 4 ---- 1 file changed, 4 deletions(-) diff --git a/t/sanitization.t b/t/sanitization.t index 2c21b86cb..f6fa30cbf 100644 --- a/t/sanitization.t +++ b/t/sanitization.t @@ -1,8 +1,6 @@ use Test::More; use Test::Exception; -use Data::Dumper; - use utf8; BEGIN { use_ok( 'Zonemaster::Engine::Sanitization' ); } @@ -97,8 +95,6 @@ subtest 'Bad domains' => sub { throws_ok (sub { Zonemaster::Engine::Sanitization::sanitize_name($domain); }, $error, 'invalid domain should throw' ); - - print Dumper "$@", $@->{dlabel}; } } }; From c2a2791f888319b955fd9bdbc3cb6f06157bbe64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 1 Mar 2022 17:29:59 +0100 Subject: [PATCH 07/26] use standard class accessor interface --- lib/Zonemaster/Engine/Sanitization/Errors.pm | 42 ++++++++++---------- t/sanitization.t | 1 + 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index 61ccf0387..476761ab0 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -5,13 +5,12 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; - use Zonemaster::Engine::Exception; -extends(qw/Zonemaster::Engine::Exception/); +use base qw(Class::Accessor Zonemaster::Engine::Exception); +__PACKAGE__->follow_best_practice; +__PACKAGE__->mk_ro_accessors(qw(type)); -has 'type' => ( is => 'ro', isa => 'Str' ); sub new { my $proto = shift; @@ -26,9 +25,8 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; - -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); +use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); +__PACKAGE__->follow_best_practice; sub new { @@ -49,9 +47,9 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; +use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); +__PACKAGE__->follow_best_practice; sub new { @@ -72,11 +70,11 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; +use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); +__PACKAGE__->follow_best_practice; +__PACKAGE__->mk_ro_accessors(qw(dlabel)); -has 'dlabel' => ( is => 'ro', isa => 'Str' ); sub new { my $proto = shift; @@ -96,11 +94,11 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; +use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); +__PACKAGE__->follow_best_practice; +__PACKAGE__->mk_ro_accessors(qw(dlabel)); -has 'dlabel' => ( is => 'ro', isa => 'Str' ); sub new { my $proto = shift; @@ -120,11 +118,11 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; +use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); +__PACKAGE__->follow_best_practice; +__PACKAGE__->mk_ro_accessors(qw(dlabel)); -has 'dlabel' => ( is => 'ro', isa => 'Str' ); sub new { my $proto = shift; @@ -144,9 +142,9 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; +use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); +__PACKAGE__->follow_best_practice; sub new { @@ -167,9 +165,9 @@ use 5.014002; use strict; use warnings; -use Class::Accessor "antlers"; +use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -extends(qw/Zonemaster::Engine::Exception::DomainSanitizationError/); +__PACKAGE__->follow_best_practice; sub new { diff --git a/t/sanitization.t b/t/sanitization.t index f6fa30cbf..f9c737346 100644 --- a/t/sanitization.t +++ b/t/sanitization.t @@ -95,6 +95,7 @@ subtest 'Bad domains' => sub { throws_ok (sub { Zonemaster::Engine::Sanitization::sanitize_name($domain); }, $error, 'invalid domain should throw' ); + note "$@"; } } }; From 9a2f429b836843ec4b15b82683ffbdb6c6a50089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Wed, 2 Mar 2022 13:33:01 +0100 Subject: [PATCH 08/26] refactor sanitize_label --- lib/Zonemaster/Engine/Sanitization.pm | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm index a8c24e14c..46d2be9bc 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -5,36 +5,36 @@ use 5.014002; use strict; use warnings; +use Carp; use Encode; use Readonly; +use Try::Tiny; use Zonemaster::LDNS; +use Data::Dumper; use Zonemaster::Engine::Sanitization::Errors; -Readonly my $VALID_ASCII => q/^[A-Za-z0-9\/\-_]+$/; -Readonly my $FULL_STOP => q/\x{002E}/; +Readonly my $ASCII => qr/^[[:ascii:]]+$/; +Readonly my $VALID_ASCII => qr/^[A-Za-z0-9\/\-_]+$/; +Readonly my $FULL_STOP => qr/\x{002E}/; sub sanitize_label { my ( $label ) = @_; - my $alabel = eval { - Encode::encode('ascii', $label, Encode::FB_CROAK); - }; - - # Not ascii string, assume u-label - if ($@) { - $alabel = eval { - Zonemaster::LDNS::to_idn($label) - }; - # TODO: handle when libidn not installed? - if ($@) { + my $alabel = ""; + + if ( $label =~ $VALID_ASCII ) { + $alabel = lc $label; + } elsif ( $label =~ $ASCII ) { + die Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii->new({ dlabel => $label }); + } elsif (Zonemaster::LDNS::has_idn) { + try { + $alabel = Zonemaster::LDNS::to_idn($label); + } catch { die Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel->new({ dlabel => Encode::encode_utf8($label) }); } } else { - if ( $alabel !~ $VALID_ASCII) { - die Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii->new({ dlabel => $alabel }); - } - $alabel = lc $alabel; + croak 'The domain name contains non-ascii characters and IDNA is not installed'; } if ( length($alabel) > 63) { From 84adfdd3698fb811993371b2a5519f0242ced4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Thu, 3 Mar 2022 12:11:00 +0100 Subject: [PATCH 09/26] prefer single method --- lib/Zonemaster/Engine/Sanitization/Errors.pm | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index 476761ab0..78f6e87d6 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -8,7 +8,6 @@ use warnings; use Zonemaster::Engine::Exception; use base qw(Class::Accessor Zonemaster::Engine::Exception); -__PACKAGE__->follow_best_practice; __PACKAGE__->mk_ro_accessors(qw(type)); @@ -26,7 +25,6 @@ use strict; use warnings; use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -__PACKAGE__->follow_best_practice; sub new { @@ -49,8 +47,6 @@ use warnings; use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -__PACKAGE__->follow_best_practice; - sub new { my $proto = shift; @@ -72,7 +68,6 @@ use warnings; use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -__PACKAGE__->follow_best_practice; __PACKAGE__->mk_ro_accessors(qw(dlabel)); @@ -96,7 +91,6 @@ use warnings; use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -__PACKAGE__->follow_best_practice; __PACKAGE__->mk_ro_accessors(qw(dlabel)); @@ -120,7 +114,6 @@ use warnings; use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -__PACKAGE__->follow_best_practice; __PACKAGE__->mk_ro_accessors(qw(dlabel)); @@ -144,8 +137,6 @@ use warnings; use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -__PACKAGE__->follow_best_practice; - sub new { my $proto = shift; @@ -167,8 +158,6 @@ use warnings; use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); -__PACKAGE__->follow_best_practice; - sub new { my $proto = shift; From f7bf3c1ca71a17d5579a4c529fdaccf5092f0513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Thu, 7 Apr 2022 10:54:23 +0200 Subject: [PATCH 10/26] update error messages --- lib/Zonemaster/Engine/Sanitization.pm | 2 +- lib/Zonemaster/Engine/Sanitization/Errors.pm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm index 46d2be9bc..9916eec95 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -34,7 +34,7 @@ sub sanitize_label { die Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel->new({ dlabel => Encode::encode_utf8($label) }); } } else { - croak 'The domain name contains non-ascii characters and IDNA is not installed'; + croak 'The domain name contains at least one non-ASCII character and this installation of Zonemaster has no support for IDNA.'; } if ( length($alabel) > 63) { diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index 78f6e87d6..d45958efa 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -76,7 +76,7 @@ sub new { my $params = shift; $params->{tag} = 'INVALID_ASCII'; - $params->{message} = 'Domain name has an ASCII label with a character not permitted.'; + $params->{message} = 'Domain name has an ASCII label with an ASCII character not permitted.'; my $class = ref $proto || $proto; my $obj = __PACKAGE__->SUPER::new($params); From b00402d9d3200f566eff8cb13bde19cb195c7dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Thu, 7 Apr 2022 12:28:10 +0200 Subject: [PATCH 11/26] use utf8 in sanitization files --- lib/Zonemaster/Engine/Sanitization.pm | 1 + lib/Zonemaster/Engine/Sanitization/Errors.pm | 1 + t/sanitization.t | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm index 9916eec95..c298710be 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -2,6 +2,7 @@ package Zonemaster::Engine::Sanitization; use 5.014002; +use utf8; use strict; use warnings; diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index d45958efa..c39cfbf17 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -2,6 +2,7 @@ package Zonemaster::Engine::Exception::DomainSanitizationError; use 5.014002; +use utf8; use strict; use warnings; diff --git a/t/sanitization.t b/t/sanitization.t index f9c737346..1966ae2b6 100644 --- a/t/sanitization.t +++ b/t/sanitization.t @@ -34,7 +34,6 @@ subtest 'Valid domains' => sub { # Domains with uppercase unicode 'CafÉ.example.com' => 'xn--caf-dma.example.com', 'ΑβΓΔε.example.com' => 'xn--mxacdef.example.com', - # TODO: special I case # All ascii domains (lowercase) 'example.com' => 'example.com', From a1331cfb8be3e036836bd355abd0f0332fc1ebf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Thu, 7 Apr 2022 12:34:19 +0200 Subject: [PATCH 12/26] rename accessor --- lib/Zonemaster/Engine/Sanitization/Errors.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index c39cfbf17..dfb7d9aee 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -9,7 +9,7 @@ use warnings; use Zonemaster::Engine::Exception; use base qw(Class::Accessor Zonemaster::Engine::Exception); -__PACKAGE__->mk_ro_accessors(qw(type)); +__PACKAGE__->mk_ro_accessors(qw(tag)); sub new { From 784514f797c212ea24f0114824060124f129d08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Fri, 19 Aug 2022 14:50:28 +0200 Subject: [PATCH 13/26] use returned error instead of exception --- lib/Zonemaster/Engine/Sanitization.pm | 46 ++-- lib/Zonemaster/Engine/Sanitization/Errors.pm | 208 +++++-------------- t/sanitization.t | 50 +++-- 3 files changed, 121 insertions(+), 183 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm index c298710be..a4aa12721 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -19,65 +19,87 @@ Readonly my $ASCII => qr/^[[:ascii:]]+$/; Readonly my $VALID_ASCII => qr/^[A-Za-z0-9\/\-_]+$/; Readonly my $FULL_STOP => qr/\x{002E}/; + sub sanitize_label { my ( $label ) = @_; + my @messages; my $alabel = ""; if ( $label =~ $VALID_ASCII ) { $alabel = lc $label; } elsif ( $label =~ $ASCII ) { - die Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii->new({ dlabel => $label }); + push @messages, Zonemaster::Engine::Sanitization::Errors->new('INVALID_ASCII' => {dlabel => $label}); + + return \@messages, undef; } elsif (Zonemaster::LDNS::has_idn) { try { $alabel = Zonemaster::LDNS::to_idn($label); } catch { - die Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel->new({ dlabel => Encode::encode_utf8($label) }); + push @messages, Zonemaster::Engine::Sanitization::Errors->new('INVALID_U_LABEL' => {dlabel => $label}); + + return \@messages, undef; } } else { croak 'The domain name contains at least one non-ASCII character and this installation of Zonemaster has no support for IDNA.'; } if ( length($alabel) > 63) { - die Zonemaster::Engine::Exception::DomainSanitization::LabelTooLong->new({ dlabel => $alabel }); + push @messages, Zonemaster::Engine::Sanitization::Errors->new('LABEL_TOO_LONG' => {dlabel => $label}); + return \@messages, undef; } - return $alabel; + + return \@messages, $alabel; } sub sanitize_name { my ( $uname ) = @_; + my @messages; if (length($uname) == 0) { - die Zonemaster::Engine::Exception::DomainSanitization::EmptyDomainName->new(); + push @messages, Zonemaster::Engine::Sanitization::Errors->new('EMPTY_DOMAIN_NAME'); + return \@messages, undef; } # Replace fullwidth full stop, ideographic full stop and halfwidth ideographic full stop with full stop $uname =~ s/[\x{FF0E}\x{3002}\x{FF61}]/\x{002E}/g; if ( $uname eq '.' ) { - return $uname; + return \@messages, $uname; } if ($uname =~ /^${FULL_STOP}/) { - die Zonemaster::Engine::Exception::DomainSanitization::InitialDot->new(); + push @messages, Zonemaster::Engine::Sanitization::Errors->new('INITIAL_DOT'); + return \@messages, undef; } if ($uname =~ /${FULL_STOP}{2,}/ ) { - die Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots->new(); + push @messages, Zonemaster::Engine::Sanitization::Errors->new('REPEATED_DOTS'); + return \@messages, undef; } $uname =~ s/${FULL_STOP}$//g; my @labels = split $FULL_STOP, $uname; - @labels = map { sanitize_label($_) } @labels; + my @label_results = map { [ sanitize_label($_) ] } @labels; + my @label_errors = map { @{$_->[0]} } @label_results; + + push @messages, @label_errors; + + if ( @messages ) { + return \@messages, undef; + } + + my @label_ok = map { $_->[1] } @label_results; - my $final_name = join '.', @labels; + my $final_name = join '.', @label_ok; if (length($final_name) > 253) { - die Zonemaster::Engine::Exception::DomainSanitization::DomainNameTooLong->new(); + push @messages, Zonemaster::Engine::Sanitization::Errors->new('DOMAIN_NAME_TOO_LONG'); + return \@messages, undef; } - return $final_name; + return \@messages, $final_name; } 1; diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Sanitization/Errors.pm index dfb7d9aee..ed2fa1a1c 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Sanitization/Errors.pm @@ -1,175 +1,79 @@ -package Zonemaster::Engine::Exception::DomainSanitizationError; +package Zonemaster::Engine::Sanitization::Errors; -use 5.014002; - -use utf8; use strict; use warnings; -use Zonemaster::Engine::Exception; -use base qw(Class::Accessor Zonemaster::Engine::Exception); - -__PACKAGE__->mk_ro_accessors(qw(tag)); - +use Carp; +use Readonly; +use Locale::TextDomain qw[Zonemaster-Engine]; + +use overload '""' => \&string; + + +Readonly my %ERRORS => ( + EMPTY_DOMAIN_NAME => { + message => N__ 'Domain name is empty.' + }, + INITIAL_DOT => { + message => N__ 'Domain name starts with dot.' + }, + REPEATED_DOTS => { + message => N__ 'Domain name has repeated dots.' + }, + INVALID_ASCII => { + message => N__ 'Domain name has an ASCII label ("{dlabel}") with a character not permitted.', + arguments => [ 'dlabel' ] + }, + INVALID_U_LABEL => { + message => N__ 'Domain name has a non-ASCII label ("{dlabel}") which is not a valid U-label.', + arguments => [ 'dlabel' ] + }, + LABEL_TOO_LONG => { + message => N__ 'Domain name has a label that is too long (more than 63 characters), "{dlabel}".', + arguments => [ 'dlabel' ] + }, + DOMAIN_NAME_TOO_LONG => { + message => N__ 'Domain name is too long (more than 253 characters with no final dot).', + }, +); sub new { - my $proto = shift; - my $obj = __PACKAGE__->SUPER::new(@_); + my ( $proto, $tag, $params ) = @_; my $class = ref $proto || $proto; - return bless $obj, $class; -} -package Zonemaster::Engine::Exception::DomainSanitization::InitialDot; -use 5.014002; + if (!exists $ERRORS{$tag}) { + croak 'Unknown error tag.'; + } -use strict; -use warnings; - -use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); - - -sub new { - my $proto = shift; - my $params = shift; + my $obj = { tag => $tag, params => {} }; - $params->{tag} = 'INITIAL_DOT'; - $params->{message} = 'Domain name starts with dot.'; + if (exists $ERRORS{$tag}->{arguments}) { + foreach my $arg ( @{$ERRORS{$tag}->{arguments}} ) { + if (!exists $params->{$arg} ) { + croak "Missing arguments $arg."; + } + $obj->{params}->{$arg} = $params->{$arg}; + } + } - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); return bless $obj, $class; } -package Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots; -use 5.014002; - -use strict; -use warnings; - -use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); - - -sub new { - my $proto = shift; - my $params = shift; - - $params->{tag} = 'REPEATED_DOTS'; - $params->{message} = 'Domain name has repeated dots.'; - - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); - return bless $obj, $class; +sub message { + my ( $self ) = @_; + return __x $ERRORS{$self->{tag}}->{message}, %{$self->{params}}; } -package Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii; -use 5.014002; - -use strict; -use warnings; - -use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); - -__PACKAGE__->mk_ro_accessors(qw(dlabel)); +sub tag { + my ( $self ) = @_; - -sub new { - my $proto = shift; - my $params = shift; - - $params->{tag} = 'INVALID_ASCII'; - $params->{message} = 'Domain name has an ASCII label with an ASCII character not permitted.'; - - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); - return bless $obj, $class; + return $self->{tag}; } -package Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel; -use 5.014002; - -use strict; -use warnings; - -use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); - -__PACKAGE__->mk_ro_accessors(qw(dlabel)); - - -sub new { - my $proto = shift; - my $params = shift; - - $params->{tag} = 'INVALID_U_LABEL'; - $params->{message} = 'Domain name has a non-ASCII label which is not a valid U-label.'; +sub string { + my ( $self ) = @_; - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); - return bless $obj, $class; -} - -package Zonemaster::Engine::Exception::DomainSanitization::LabelTooLong; -use 5.014002; - -use strict; -use warnings; - -use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); - -__PACKAGE__->mk_ro_accessors(qw(dlabel)); - - -sub new { - my $proto = shift; - my $params = shift; - - $params->{tag} = 'LABEL_TOO_LONG'; - $params->{message} = 'Domain name has a label that is too long (more than 63 characters).'; - - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); - return bless $obj, $class; -} - -package Zonemaster::Engine::Exception::DomainSanitization::DomainNameTooLong; -use 5.014002; - -use strict; -use warnings; - -use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); - - -sub new { - my $proto = shift; - my $params = shift; - - $params->{tag} = 'DOMAIN_NAME_TOO_LONG'; - $params->{message} = 'Domain name is too long (more than 253 characters with no final dot).'; - - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); - return bless $obj, $class; -} - -package Zonemaster::Engine::Exception::DomainSanitization::EmptyDomainName; -use 5.014002; - -use strict; -use warnings; - -use base qw(Class::Accessor Zonemaster::Engine::Exception::DomainSanitizationError); - - -sub new { - my $proto = shift; - my $params = shift; - - $params->{tag} = 'EMPTY_DOMAIN_NAME'; - $params->{message} = 'Domain name is empty.'; - - my $class = ref $proto || $proto; - my $obj = __PACKAGE__->SUPER::new($params); - return bless $obj, $class; + return $self->message; } 1; diff --git a/t/sanitization.t b/t/sanitization.t index 1966ae2b6..409087e03 100644 --- a/t/sanitization.t +++ b/t/sanitization.t @@ -13,6 +13,9 @@ subtest 'Valid domains' => sub { '。' => '.', # Ideographic full stop '。' => '.', # Halfwidth ideographic full stop + # Trailing and leading white spaces + #' example.com. ' => 'example.com', + # Mixed dots with trailing dot 'example。com.' => 'example.com', 'example。com.' => 'example.com', @@ -28,7 +31,6 @@ subtest 'Valid domains' => sub { # Domains with U-Labels 'café.example.com' => 'xn--caf-dma.example.com', 'エグザンプル。example。com' => 'xn--ickqs6k2dyb.example.com', - '🦈.example.com' => 'xn--7s9h.example.com', 'αβγδε.example.com' => 'xn--mxacdef.example.com', # Domains with uppercase unicode @@ -52,15 +54,19 @@ subtest 'Valid domains' => sub { "a" x 63 . ".example.com" => "a" x 63 . ".example.com", # this is 253 characters ("a" x 15 . ".") x 15 . "b" . ".example.com" => ("a" x 15 . ".") x 15 . "b" . ".example.com", + + # Special I case + #'İ.example.com' => 'i.example.com', ); while (($domain, $expected_output) = each (%input_domains)) { - subtest 'Domain: ' . $domain => sub { - my $output; + subtest "Domain: '$domain'" => sub { + my $errors, $final_domain; lives_ok(sub { - $output = Zonemaster::Engine::Sanitization::sanitize_name($domain); + ($errors, $final_domain) = Zonemaster::Engine::Sanitization::sanitize_name($domain); }, 'correct domain should live'); - is($output, $expected_output, 'Match expected domain') or diag($output); + is(scalar @{$errors}, 0, 'No error returned') or diag(@{$errors}); + is($final_domain, $expected_output, 'Match expected domain') or diag($final_domain); } } }; @@ -68,33 +74,39 @@ subtest 'Valid domains' => sub { subtest 'Bad domains' => sub { my %input_domains = ( # Empty labels - '.。.' => 'Zonemaster::Engine::Exception::DomainSanitization::InitialDot', - 'example。.com.' => 'Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots', - 'example。com.。' => 'Zonemaster::Engine::Exception::DomainSanitization::RepeatedDots', - '..example。com' => 'Zonemaster::Engine::Exception::DomainSanitization::InitialDot', + '.。.' => 'INITIAL_DOT', + 'example。.com.' => 'REPEATED_DOTS', + 'example。com.。' => 'REPEATED_DOTS', + '..example。com' => 'INITIAL_DOT', # Bad ascii - 'bad:%;!$.example.com.' => 'Zonemaster::Engine::Exception::DomainSanitization::InvalidAscii', + 'bad:%;!$.example.com.' => 'INVALID_ASCII', # Label to long - "a" x 64 . ".example.com" => 'Zonemaster::Engine::Exception::DomainSanitization::LabelTooLong', + "a" x 64 . ".example.com" => 'LABEL_TOO_LONG', # Length too long after idn conversion (libidn fails) - 'チョコレート' x 8 . 'a' . '.example.com' => 'Zonemaster::Engine::Exception::DomainSanitization::InvalidULabel', + 'チョコレート' x 8 . 'a' . '.example.com' => 'INVALID_U_LABEL', + # Emoji in names are invalid as per IDNA2008 + '🦈.example.com' => 'INVALID_U_LABEL', # Domain to long # this is 254 characters - ("a" x 15 . ".") x 15 . "bc" . ".example.com" => 'Zonemaster::Engine::Exception::DomainSanitization::DomainNameTooLong', + ("a" x 15 . ".") x 15 . "bc" . ".example.com" => 'DOMAIN_NAME_TOO_LONG', # Empty domain - '' => 'Zonemaster::Engine::Exception::DomainSanitization::EmptyDomainName', + '' => 'EMPTY_DOMAIN_NAME', ); while (($domain, $error) = each (%input_domains)) { - subtest "Domain: $domain ($error)" => sub { - throws_ok (sub { - Zonemaster::Engine::Sanitization::sanitize_name($domain); - }, $error, 'invalid domain should throw' ); - note "$@"; + subtest "Domain: '$domain' ($error)" => sub { + my $output, $messages, $domain; + lives_ok(sub { + ($errors, $final_domain) = Zonemaster::Engine::Sanitization::sanitize_name($domain); + }, 'incorrect domain should live'); + + is($final_domain, undef, 'No domain returned') or diag($final_domain); + is($errors->[0]->tag, $error, 'Correct error is returned') or diag($errors[0]); + note($errors->[0]) } } }; From a4208c64914e8bf48aa0c98f061dceb1b3984539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Fri, 19 Aug 2022 15:58:57 +0200 Subject: [PATCH 14/26] trim whitespaces --- lib/Zonemaster/Engine/Sanitization.pm | 49 +++++++++++++++++++++++---- t/sanitization.t | 3 +- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Sanitization.pm index a4aa12721..62202c117 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Sanitization.pm @@ -17,8 +17,41 @@ use Zonemaster::Engine::Sanitization::Errors; Readonly my $ASCII => qr/^[[:ascii:]]+$/; Readonly my $VALID_ASCII => qr/^[A-Za-z0-9\/\-_]+$/; -Readonly my $FULL_STOP => qr/\x{002E}/; +Readonly my $ASCII_FULL_STOP => "\x{002E}"; +Readonly my $ASCII_FULL_STOP_RE => qr/\x{002E}/; +Readonly my %FULL_STOPS => ( + FULLWIDTH_FULL_STOP => q/\x{FF0E}/, + IDEOGRAPHIC_FULL_STOP => q/\x{3002}/, + HALFWIDTH_IDEOGRAPHIC_FULL_STOP => q/\x{FF61}/ +); +Readonly my $FULL_STOPS_RE => (sub { + my $re = '[' . (join '', values %FULL_STOPS) . ']'; + return qr/$re/; +})->(); +Readonly my %WHITE_SPACES => ( + SPACE => q/\x{0020}/, + CHARACTER_TABULATION => q/\x{0009}/, + NO_BREAK_SPACE => q/\x{00A0}/, + EN_QUAD => q/\x{2000}/, + EM_QUAD => q/\x{2001}/, + EN_SPACE => q/\x{2002}/, + EM_SPACE => q/\x{2003}/, + THREE_PER_EM_SPACE => q/\x{2004}/, + FOUR_PER_EM_SPACE => q/\x{2005}/, + SIX_PER_EM_SPACE => q/\x{2006}/, + FIGURE_SPACE => q/\x{2007}/, + PUNCTUATION_SPACE => q/\x{2008}/, + THIN_SPACE => q/\x{2009}/, + HAIR_SPACE => q/\x{200A}/, + MEDIUM_MATHEMATICAL_SPACE => q/\x{205F}/, + IDEOGRAPHIC_SPACE => q/\x{3000}/, + OGHAM_SPACE_MARK => q/\x{1680}/, +); +Readonly my $WHITE_SPACES_RE => (sub { + my $re = '[' . (join '', values %WHITE_SPACES) . ']'; + return qr/$re/; +})->(); sub sanitize_label { my ( $label ) = @_; @@ -56,31 +89,33 @@ sub sanitize_name { my ( $uname ) = @_; my @messages; + $uname =~ s/^${$WHITE_SPACES_RE}+//; + $uname =~ s/${WHITE_SPACES_RE}+$//; + if (length($uname) == 0) { push @messages, Zonemaster::Engine::Sanitization::Errors->new('EMPTY_DOMAIN_NAME'); return \@messages, undef; } - # Replace fullwidth full stop, ideographic full stop and halfwidth ideographic full stop with full stop - $uname =~ s/[\x{FF0E}\x{3002}\x{FF61}]/\x{002E}/g; + $uname =~ s/${FULL_STOPS_RE}/${ASCII_FULL_STOP}/g; if ( $uname eq '.' ) { return \@messages, $uname; } - if ($uname =~ /^${FULL_STOP}/) { + if ($uname =~ m/^${ASCII_FULL_STOP_RE}/) { push @messages, Zonemaster::Engine::Sanitization::Errors->new('INITIAL_DOT'); return \@messages, undef; } - if ($uname =~ /${FULL_STOP}{2,}/ ) { + if ($uname =~ m/${ASCII_FULL_STOP_RE}{2,}/ ) { push @messages, Zonemaster::Engine::Sanitization::Errors->new('REPEATED_DOTS'); return \@messages, undef; } - $uname =~ s/${FULL_STOP}$//g; + $uname =~ s/${ASCII_FULL_STOP_RE}$//g; - my @labels = split $FULL_STOP, $uname; + my @labels = split $ASCII_FULL_STOP_RE, $uname; my @label_results = map { [ sanitize_label($_) ] } @labels; my @label_errors = map { @{$_->[0]} } @label_results; diff --git a/t/sanitization.t b/t/sanitization.t index 409087e03..5753a7ffb 100644 --- a/t/sanitization.t +++ b/t/sanitization.t @@ -14,7 +14,7 @@ subtest 'Valid domains' => sub { '。' => '.', # Halfwidth ideographic full stop # Trailing and leading white spaces - #' example.com. ' => 'example.com', + " \x{205F} example.com. \x{0009}" => 'example.com', # Mixed dots with trailing dot 'example。com.' => 'example.com', @@ -95,6 +95,7 @@ subtest 'Bad domains' => sub { # Empty domain '' => 'EMPTY_DOMAIN_NAME', + ' ' => 'EMPTY_DOMAIN_NAME', ); while (($domain, $error) = each (%input_domains)) { From 91832c0a95648b0fd8a8a022d370a3a4482c476f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Fri, 19 Aug 2022 16:07:28 +0200 Subject: [PATCH 15/26] rename module to better match spec --- .../{Sanitization.pm => Normalization.pm} | 24 +++++++++---------- .../{Sanitization => Normalization}/Errors.pm | 2 +- t/{sanitization.t => normalization.t} | 6 ++--- 3 files changed, 16 insertions(+), 16 deletions(-) rename lib/Zonemaster/Engine/{Sanitization.pm => Normalization.pm} (77%) rename lib/Zonemaster/Engine/{Sanitization => Normalization}/Errors.pm (97%) rename t/{sanitization.t => normalization.t} (93%) diff --git a/lib/Zonemaster/Engine/Sanitization.pm b/lib/Zonemaster/Engine/Normalization.pm similarity index 77% rename from lib/Zonemaster/Engine/Sanitization.pm rename to lib/Zonemaster/Engine/Normalization.pm index 62202c117..760710d38 100644 --- a/lib/Zonemaster/Engine/Sanitization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -1,4 +1,4 @@ -package Zonemaster::Engine::Sanitization; +package Zonemaster::Engine::Normalization; use 5.014002; @@ -13,7 +13,7 @@ use Try::Tiny; use Zonemaster::LDNS; use Data::Dumper; -use Zonemaster::Engine::Sanitization::Errors; +use Zonemaster::Engine::Normalization::Errors; Readonly my $ASCII => qr/^[[:ascii:]]+$/; Readonly my $VALID_ASCII => qr/^[A-Za-z0-9\/\-_]+$/; @@ -53,7 +53,7 @@ Readonly my $WHITE_SPACES_RE => (sub { return qr/$re/; })->(); -sub sanitize_label { +sub normalize_label { my ( $label ) = @_; my @messages; @@ -62,14 +62,14 @@ sub sanitize_label { if ( $label =~ $VALID_ASCII ) { $alabel = lc $label; } elsif ( $label =~ $ASCII ) { - push @messages, Zonemaster::Engine::Sanitization::Errors->new('INVALID_ASCII' => {dlabel => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_ASCII' => {dlabel => $label}); return \@messages, undef; } elsif (Zonemaster::LDNS::has_idn) { try { $alabel = Zonemaster::LDNS::to_idn($label); } catch { - push @messages, Zonemaster::Engine::Sanitization::Errors->new('INVALID_U_LABEL' => {dlabel => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_U_LABEL' => {dlabel => $label}); return \@messages, undef; } @@ -78,14 +78,14 @@ sub sanitize_label { } if ( length($alabel) > 63) { - push @messages, Zonemaster::Engine::Sanitization::Errors->new('LABEL_TOO_LONG' => {dlabel => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new('LABEL_TOO_LONG' => {dlabel => $label}); return \@messages, undef; } return \@messages, $alabel; } -sub sanitize_name { +sub normalize_name { my ( $uname ) = @_; my @messages; @@ -93,7 +93,7 @@ sub sanitize_name { $uname =~ s/${WHITE_SPACES_RE}+$//; if (length($uname) == 0) { - push @messages, Zonemaster::Engine::Sanitization::Errors->new('EMPTY_DOMAIN_NAME'); + push @messages, Zonemaster::Engine::Normalization::Errors->new('EMPTY_DOMAIN_NAME'); return \@messages, undef; } @@ -104,19 +104,19 @@ sub sanitize_name { } if ($uname =~ m/^${ASCII_FULL_STOP_RE}/) { - push @messages, Zonemaster::Engine::Sanitization::Errors->new('INITIAL_DOT'); + push @messages, Zonemaster::Engine::Normalization::Errors->new('INITIAL_DOT'); return \@messages, undef; } if ($uname =~ m/${ASCII_FULL_STOP_RE}{2,}/ ) { - push @messages, Zonemaster::Engine::Sanitization::Errors->new('REPEATED_DOTS'); + push @messages, Zonemaster::Engine::Normalization::Errors->new('REPEATED_DOTS'); return \@messages, undef; } $uname =~ s/${ASCII_FULL_STOP_RE}$//g; my @labels = split $ASCII_FULL_STOP_RE, $uname; - my @label_results = map { [ sanitize_label($_) ] } @labels; + my @label_results = map { [ normalize_label($_) ] } @labels; my @label_errors = map { @{$_->[0]} } @label_results; push @messages, @label_errors; @@ -130,7 +130,7 @@ sub sanitize_name { my $final_name = join '.', @label_ok; if (length($final_name) > 253) { - push @messages, Zonemaster::Engine::Sanitization::Errors->new('DOMAIN_NAME_TOO_LONG'); + push @messages, Zonemaster::Engine::Normalization::Errors->new('DOMAIN_NAME_TOO_LONG'); return \@messages, undef; } diff --git a/lib/Zonemaster/Engine/Sanitization/Errors.pm b/lib/Zonemaster/Engine/Normalization/Errors.pm similarity index 97% rename from lib/Zonemaster/Engine/Sanitization/Errors.pm rename to lib/Zonemaster/Engine/Normalization/Errors.pm index ed2fa1a1c..1383aafe9 100644 --- a/lib/Zonemaster/Engine/Sanitization/Errors.pm +++ b/lib/Zonemaster/Engine/Normalization/Errors.pm @@ -1,4 +1,4 @@ -package Zonemaster::Engine::Sanitization::Errors; +package Zonemaster::Engine::Normalization::Errors; use strict; use warnings; diff --git a/t/sanitization.t b/t/normalization.t similarity index 93% rename from t/sanitization.t rename to t/normalization.t index 5753a7ffb..b6fee91ed 100644 --- a/t/sanitization.t +++ b/t/normalization.t @@ -3,7 +3,7 @@ use Test::Exception; use utf8; -BEGIN { use_ok( 'Zonemaster::Engine::Sanitization' ); } +BEGIN { use_ok( 'Zonemaster::Engine::Normalization' ); } subtest 'Valid domains' => sub { my %input_domains = ( @@ -63,7 +63,7 @@ subtest 'Valid domains' => sub { subtest "Domain: '$domain'" => sub { my $errors, $final_domain; lives_ok(sub { - ($errors, $final_domain) = Zonemaster::Engine::Sanitization::sanitize_name($domain); + ($errors, $final_domain) = Zonemaster::Engine::Normalization::normalize_name($domain); }, 'correct domain should live'); is(scalar @{$errors}, 0, 'No error returned') or diag(@{$errors}); is($final_domain, $expected_output, 'Match expected domain') or diag($final_domain); @@ -102,7 +102,7 @@ subtest 'Bad domains' => sub { subtest "Domain: '$domain' ($error)" => sub { my $output, $messages, $domain; lives_ok(sub { - ($errors, $final_domain) = Zonemaster::Engine::Sanitization::sanitize_name($domain); + ($errors, $final_domain) = Zonemaster::Engine::Normalization::normalize_name($domain); }, 'incorrect domain should live'); is($final_domain, undef, 'No domain returned') or diag($final_domain); From 1dadd10bc3b5d1c89827a33a6dae0d28d70ee965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 6 Sep 2022 11:52:53 +0200 Subject: [PATCH 16/26] handle ambigyous downcasing --- lib/Zonemaster/Engine/Normalization.pm | 20 +++++++++++-- lib/Zonemaster/Engine/Normalization/Errors.pm | 28 +++++++++++-------- t/normalization.t | 8 +++--- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/lib/Zonemaster/Engine/Normalization.pm b/lib/Zonemaster/Engine/Normalization.pm index 760710d38..82f96d3d8 100644 --- a/lib/Zonemaster/Engine/Normalization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -52,6 +52,9 @@ Readonly my $WHITE_SPACES_RE => (sub { my $re = '[' . (join '', values %WHITE_SPACES) . ']'; return qr/$re/; })->(); +Readonly my %AMBIGUOUS_CHARACTERS => ( + "LATIN CAPITAL LETTER I WITH DOT ABOVE" => q/\x{0130}/, +); sub normalize_label { my ( $label ) = @_; @@ -62,14 +65,14 @@ sub normalize_label { if ( $label =~ $VALID_ASCII ) { $alabel = lc $label; } elsif ( $label =~ $ASCII ) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_ASCII' => {dlabel => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_ASCII' => {label => $label}); return \@messages, undef; } elsif (Zonemaster::LDNS::has_idn) { try { $alabel = Zonemaster::LDNS::to_idn($label); } catch { - push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_U_LABEL' => {dlabel => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_U_LABEL' => {label => $label}); return \@messages, undef; } @@ -78,7 +81,7 @@ sub normalize_label { } if ( length($alabel) > 63) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('LABEL_TOO_LONG' => {dlabel => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new('LABEL_TOO_LONG' => {label => $label}); return \@messages, undef; } @@ -97,6 +100,17 @@ sub normalize_name { return \@messages, undef; } + foreach my $char_name (keys %AMBIGUOUS_CHARACTERS) { + my $char = $AMBIGUOUS_CHARACTERS{$char_name}; + if ($uname =~ m/${char}/) { + push @messages, Zonemaster::Engine::Normalization::Errors->new(AMBIGUOUS_DOWNCASING => { unicode_name => $char_name }); + } + } + + if ( @messages ) { + return \@messages, undef; + } + $uname =~ s/${FULL_STOPS_RE}/${ASCII_FULL_STOP}/g; if ( $uname eq '.' ) { diff --git a/lib/Zonemaster/Engine/Normalization/Errors.pm b/lib/Zonemaster/Engine/Normalization/Errors.pm index 1383aafe9..228cc274e 100644 --- a/lib/Zonemaster/Engine/Normalization/Errors.pm +++ b/lib/Zonemaster/Engine/Normalization/Errors.pm @@ -11,29 +11,33 @@ use overload '""' => \&string; Readonly my %ERRORS => ( + AMBIGUOUS_DOWNCASING => { + message => N__ 'Ambiguous downcaseing of character "{unicode_name}" in the domain name. Use all lower case instead.', + arguments => [ 'unicode_name' ] + }, + DOMAIN_NAME_TOO_LONG => { + message => N__ 'Domain name is too long (more than 253 characters with no final dot).', + }, EMPTY_DOMAIN_NAME => { message => N__ 'Domain name is empty.' }, INITIAL_DOT => { message => N__ 'Domain name starts with dot.' }, - REPEATED_DOTS => { - message => N__ 'Domain name has repeated dots.' - }, INVALID_ASCII => { - message => N__ 'Domain name has an ASCII label ("{dlabel}") with a character not permitted.', - arguments => [ 'dlabel' ] + message => N__ 'Domain name has an ASCII label ("{label}") with a character not permitted.', + arguments => [ 'label' ] }, INVALID_U_LABEL => { - message => N__ 'Domain name has a non-ASCII label ("{dlabel}") which is not a valid U-label.', - arguments => [ 'dlabel' ] + message => N__ 'Domain name has a non-ASCII label ("{label}") which is not a valid U-label.', + arguments => [ 'label' ] }, - LABEL_TOO_LONG => { - message => N__ 'Domain name has a label that is too long (more than 63 characters), "{dlabel}".', - arguments => [ 'dlabel' ] + REPEATED_DOTS => { + message => N__ 'Domain name has repeated dots.' }, - DOMAIN_NAME_TOO_LONG => { - message => N__ 'Domain name is too long (more than 253 characters with no final dot).', + LABEL_TOO_LONG => { + message => N__ 'Domain name has a label that is too long (more than 63 characters), "{label}".', + arguments => [ 'label' ] }, ); diff --git a/t/normalization.t b/t/normalization.t index b6fee91ed..f76de1ddf 100644 --- a/t/normalization.t +++ b/t/normalization.t @@ -54,9 +54,6 @@ subtest 'Valid domains' => sub { "a" x 63 . ".example.com" => "a" x 63 . ".example.com", # this is 253 characters ("a" x 15 . ".") x 15 . "b" . ".example.com" => ("a" x 15 . ".") x 15 . "b" . ".example.com", - - # Special I case - #'İ.example.com' => 'i.example.com', ); while (($domain, $expected_output) = each (%input_domains)) { @@ -87,7 +84,7 @@ subtest 'Bad domains' => sub { # Length too long after idn conversion (libidn fails) 'チョコレート' x 8 . 'a' . '.example.com' => 'INVALID_U_LABEL', # Emoji in names are invalid as per IDNA2008 - '🦈.example.com' => 'INVALID_U_LABEL', + '❤️.example.com' => 'INVALID_U_LABEL', # Domain to long # this is 254 characters @@ -96,6 +93,9 @@ subtest 'Bad domains' => sub { # Empty domain '' => 'EMPTY_DOMAIN_NAME', ' ' => 'EMPTY_DOMAIN_NAME', + + # Ambiguous downcasing + 'İ.example.com' => 'AMBIGUOUS_DOWNCASING', ); while (($domain, $error) = each (%input_domains)) { From 0179944413ef2828e10d5c7870bc72b0de5967ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 20 Sep 2022 09:47:30 +0200 Subject: [PATCH 17/26] make regex more readable --- lib/Zonemaster/Engine/Normalization.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Zonemaster/Engine/Normalization.pm b/lib/Zonemaster/Engine/Normalization.pm index 82f96d3d8..cf0e809d5 100644 --- a/lib/Zonemaster/Engine/Normalization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -16,7 +16,7 @@ use Data::Dumper; use Zonemaster::Engine::Normalization::Errors; Readonly my $ASCII => qr/^[[:ascii:]]+$/; -Readonly my $VALID_ASCII => qr/^[A-Za-z0-9\/\-_]+$/; +Readonly my $VALID_ASCII => qr(^[A-Za-z0-9/_-]+$); Readonly my $ASCII_FULL_STOP => "\x{002E}"; Readonly my $ASCII_FULL_STOP_RE => qr/\x{002E}/; From 9f144f8ff89fe40379c2c84d7db99ed3283dbbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 20 Sep 2022 09:51:21 +0200 Subject: [PATCH 18/26] improve code consistency --- lib/Zonemaster/Engine/Normalization.pm | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/Zonemaster/Engine/Normalization.pm b/lib/Zonemaster/Engine/Normalization.pm index cf0e809d5..8fd816420 100644 --- a/lib/Zonemaster/Engine/Normalization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -65,14 +65,14 @@ sub normalize_label { if ( $label =~ $VALID_ASCII ) { $alabel = lc $label; } elsif ( $label =~ $ASCII ) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_ASCII' => {label => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new(INVALID_ASCII => {label => $label}); return \@messages, undef; } elsif (Zonemaster::LDNS::has_idn) { try { $alabel = Zonemaster::LDNS::to_idn($label); } catch { - push @messages, Zonemaster::Engine::Normalization::Errors->new('INVALID_U_LABEL' => {label => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new(INVALID_U_LABEL => {label => $label}); return \@messages, undef; } @@ -81,7 +81,7 @@ sub normalize_label { } if ( length($alabel) > 63) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('LABEL_TOO_LONG' => {label => $label}); + push @messages, Zonemaster::Engine::Normalization::Errors->new(LABEL_TOO_LONG => {label => $label}); return \@messages, undef; } @@ -96,7 +96,7 @@ sub normalize_name { $uname =~ s/${WHITE_SPACES_RE}+$//; if (length($uname) == 0) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('EMPTY_DOMAIN_NAME'); + push @messages, Zonemaster::Engine::Normalization::Errors->new(EMPTY_DOMAIN_NAME => {}); return \@messages, undef; } @@ -113,17 +113,17 @@ sub normalize_name { $uname =~ s/${FULL_STOPS_RE}/${ASCII_FULL_STOP}/g; - if ( $uname eq '.' ) { + if ( $uname eq $ASCII_FULL_STOP ) { return \@messages, $uname; } if ($uname =~ m/^${ASCII_FULL_STOP_RE}/) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('INITIAL_DOT'); + push @messages, Zonemaster::Engine::Normalization::Errors->new(INITIAL_DOT => {}); return \@messages, undef; } if ($uname =~ m/${ASCII_FULL_STOP_RE}{2,}/ ) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('REPEATED_DOTS'); + push @messages, Zonemaster::Engine::Normalization::Errors->new(REPEATED_DOTS => {}); return \@messages, undef; } @@ -144,7 +144,7 @@ sub normalize_name { my $final_name = join '.', @label_ok; if (length($final_name) > 253) { - push @messages, Zonemaster::Engine::Normalization::Errors->new('DOMAIN_NAME_TOO_LONG'); + push @messages, Zonemaster::Engine::Normalization::Errors->new(DOMAIN_NAME_TOO_LONG => {}); return \@messages, undef; } From acecfa179aa66aeca807c43d7fafcd98a54d3f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Tue, 20 Sep 2022 11:08:27 +0200 Subject: [PATCH 19/26] add pod documentation --- lib/Zonemaster/Engine/Normalization.pm | 56 ++++++++++++++++ lib/Zonemaster/Engine/Normalization/Errors.pm | 65 +++++++++++++++++++ t/normalization.t | 4 +- 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/lib/Zonemaster/Engine/Normalization.pm b/lib/Zonemaster/Engine/Normalization.pm index 8fd816420..0a92a6a73 100644 --- a/lib/Zonemaster/Engine/Normalization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -6,6 +6,8 @@ use utf8; use strict; use warnings; +use parent 'Exporter'; + use Carp; use Encode; use Readonly; @@ -15,6 +17,27 @@ use Data::Dumper; use Zonemaster::Engine::Normalization::Errors; + +=head1 NAME + +Zonemaster::Engine::Normalization - utility functions for names normalization + + +=head1 SYNOPSIS + + use Zonemaster::Engine::Normalization; + + my ($errors, $final_domain) = normalize_name($domain); + +=head1 EXPORTED FUNCTIONS + +=over +=cut + + +our @EXPORT = qw[ normalize_name ]; +our @EXPORT_OK = qw[ normalize_name normalize_label ]; + Readonly my $ASCII => qr/^[[:ascii:]]+$/; Readonly my $VALID_ASCII => qr(^[A-Za-z0-9/_-]+$); @@ -56,6 +79,22 @@ Readonly my %AMBIGUOUS_CHARACTERS => ( "LATIN CAPITAL LETTER I WITH DOT ABOVE" => q/\x{0130}/, ); + + +=item normalize_label($label) + +Normalize a single label from a DNS name. + +If the label is ASCII only, it is untouched, else it is converted according to IDNA2008. + +The downcasing, unicode normalization and conversion is performed by libidn2 using L. + +Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Errors], $alabel: String)>. + +In case of errors, the returned label will be undefined. If the method succeeded an empty error array is returned. + +=cut + sub normalize_label { my ( $label ) = @_; my @messages; @@ -88,6 +127,19 @@ sub normalize_label { return \@messages, $alabel; } +=item normalize_name($name) + +Normalize a DNS name. + + +The normalization process is detailed in the L. + +Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Errors], $name: String)>. + +In case of errors, the returned name will be undefined. If the method succeeded an empty error array is returned. + +=cut + sub normalize_name { my ( $uname ) = @_; my @messages; @@ -151,4 +203,8 @@ sub normalize_name { return \@messages, $final_name; } + +=back +=cut + 1; diff --git a/lib/Zonemaster/Engine/Normalization/Errors.pm b/lib/Zonemaster/Engine/Normalization/Errors.pm index 228cc274e..3a3caaffc 100644 --- a/lib/Zonemaster/Engine/Normalization/Errors.pm +++ b/lib/Zonemaster/Engine/Normalization/Errors.pm @@ -10,6 +10,20 @@ use Locale::TextDomain qw[Zonemaster-Engine]; use overload '""' => \&string; +=head1 NAME + +Zonemaster::Engine::Normalization::Errors - normalization error class + + +=head1 SYNOPSIS + + use Zonemaster::Engine::Normalization::Errors; + + my $error = Zonemaster::Engine::Normalization::Errors->new(LABEL_TOO_LONG => {label => $label}); + +=cut + + Readonly my %ERRORS => ( AMBIGUOUS_DOWNCASING => { message => N__ 'Ambiguous downcaseing of character "{unicode_name}" in the domain name. Use all lower case instead.', @@ -41,6 +55,31 @@ Readonly my %ERRORS => ( }, ); +=head1 ATTRIBUTES + +=over + +=item tag + +The message tag asscociated to the error. + +=item params + +The error message parameters to use in the message string. + +=back + +=head1 METHODS + +=over + +=item new($tag, $params) + +Creates and returns a new error object. +This function will croak if there is a missing parameter for the given tag. + +=cut + sub new { my ( $proto, $tag, $params ) = @_; my $class = ref $proto || $proto; @@ -63,21 +102,47 @@ sub new { return bless $obj, $class; } + +=item message + +Returns the transated error message using the parameters given when creating the object. + +=cut + sub message { my ( $self ) = @_; return __x $ERRORS{$self->{tag}}->{message}, %{$self->{params}}; } + +=item tag + +Returns the message tag asscociated to the error. + +=cut + sub tag { my ( $self ) = @_; return $self->{tag}; } + +=item string + +Returns a string representation of the error object, equivalent to message. + +=cut + sub string { my ( $self ) = @_; return $self->message; } + +=back + +=cut + 1; diff --git a/t/normalization.t b/t/normalization.t index f76de1ddf..3c82235c3 100644 --- a/t/normalization.t +++ b/t/normalization.t @@ -60,7 +60,7 @@ subtest 'Valid domains' => sub { subtest "Domain: '$domain'" => sub { my $errors, $final_domain; lives_ok(sub { - ($errors, $final_domain) = Zonemaster::Engine::Normalization::normalize_name($domain); + ($errors, $final_domain) = normalize_name($domain); }, 'correct domain should live'); is(scalar @{$errors}, 0, 'No error returned') or diag(@{$errors}); is($final_domain, $expected_output, 'Match expected domain') or diag($final_domain); @@ -102,7 +102,7 @@ subtest 'Bad domains' => sub { subtest "Domain: '$domain' ($error)" => sub { my $output, $messages, $domain; lives_ok(sub { - ($errors, $final_domain) = Zonemaster::Engine::Normalization::normalize_name($domain); + ($errors, $final_domain) = normalize_name($domain); }, 'incorrect domain should live'); is($final_domain, undef, 'No domain returned') or diag($final_domain); From d8a8162155b5934af964a57225595b8e4f01bc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Wed, 21 Sep 2022 18:32:07 +0200 Subject: [PATCH 20/26] update pod documentation for normalization module --- lib/Zonemaster/Engine/Normalization.pm | 17 +++++++++++------ lib/Zonemaster/Engine/Normalization/Errors.pm | 3 +-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/Zonemaster/Engine/Normalization.pm b/lib/Zonemaster/Engine/Normalization.pm index 0a92a6a73..3c903b1e3 100644 --- a/lib/Zonemaster/Engine/Normalization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -83,15 +83,19 @@ Readonly my %AMBIGUOUS_CHARACTERS => ( =item normalize_label($label) -Normalize a single label from a DNS name. +Normalize a single label from a domain name. -If the label is ASCII only, it is untouched, else it is converted according to IDNA2008. +If the label is ASCII only, it is down cased, else it is converted according +to IDNA2008. -The downcasing, unicode normalization and conversion is performed by libidn2 using L. +Downcasing of upper case non-ASCII characters, normalization to the Unicode +NFC format and conversion from U-label to A-label is performed by libidn2 +using L. Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Errors], $alabel: String)>. -In case of errors, the returned label will be undefined. If the method succeeded an empty error array is returned. +In case of errors, the returned label will be undefined. If the method +succeeded an empty error array is returned. =cut @@ -129,14 +133,15 @@ sub normalize_label { =item normalize_name($name) -Normalize a DNS name. +Normalize a domain name. The normalization process is detailed in the L. Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Errors], $name: String)>. -In case of errors, the returned name will be undefined. If the method succeeded an empty error array is returned. +In case of errors, the returned name will be undefined. If the method succeeded +an empty error array is returned. =cut diff --git a/lib/Zonemaster/Engine/Normalization/Errors.pm b/lib/Zonemaster/Engine/Normalization/Errors.pm index 3a3caaffc..e99b8e413 100644 --- a/lib/Zonemaster/Engine/Normalization/Errors.pm +++ b/lib/Zonemaster/Engine/Normalization/Errors.pm @@ -105,7 +105,7 @@ sub new { =item message -Returns the transated error message using the parameters given when creating the object. +Returns the translated error message using the parameters given when creating the object. =cut @@ -127,7 +127,6 @@ sub tag { return $self->{tag}; } - =item string Returns a string representation of the error object, equivalent to message. From 35f9c4ba5d2a14d96d540c2fd1ed8473722bda5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Wed, 21 Sep 2022 18:40:42 +0200 Subject: [PATCH 21/26] add test for nfc conversion --- t/normalization.t | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/t/normalization.t b/t/normalization.t index 3c82235c3..ee3971210 100644 --- a/t/normalization.t +++ b/t/normalization.t @@ -54,6 +54,16 @@ subtest 'Valid domains' => sub { "a" x 63 . ".example.com" => "a" x 63 . ".example.com", # this is 253 characters ("a" x 15 . ".") x 15 . "b" . ".example.com" => ("a" x 15 . ".") x 15 . "b" . ".example.com", + + # NFC conversion (for each group first is non-NFC, second is equivalent NFC) + "d\x{006F}\x{0308}d" => 'xn--dd-fka', + 'död' => 'xn--dd-fka', + + "aq\x{0307}\x{0323}a" => 'xn--aqa-9dc3l', + "aq\x{0323}\x{0307}a" => 'xn--aqa-9dc3l', + + "aḋ\x{0323}a" => 'xn--aa-rub587y', + "aḍ\x{0307}a" => 'xn--aa-rub587y', ); while (($domain, $expected_output) = each (%input_domains)) { From 0d54295303d70446914186e77931710a0404b2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Wed, 21 Sep 2022 18:42:50 +0200 Subject: [PATCH 22/26] add new files to manifest --- MANIFEST | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MANIFEST b/MANIFEST index 679c4d06e..a2c814b05 100644 --- a/MANIFEST +++ b/MANIFEST @@ -26,6 +26,8 @@ lib/Zonemaster/Engine/Logger.pm lib/Zonemaster/Engine/Logger/Entry.pm lib/Zonemaster/Engine/Nameserver.pm lib/Zonemaster/Engine/Nameserver/Cache.pm +lib/Zonemaster/Engine/Normalization.pm +lib/Zonemaster/Engine/Normalization/Errors.pm lib/Zonemaster/Engine/Net/IP.pm lib/Zonemaster/Engine/NSArray.pm lib/Zonemaster/Engine/Overview.pod @@ -74,6 +76,7 @@ t/nameserver-axfr.data t/nameserver-axfr.t t/nameserver.data t/nameserver.t +t/normalization.t t/old-bugs.data t/old-bugs.t t/pod-coverage.t From 61f5d9be20ac84a897ddcfc8d2e022e2927cbc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Wed, 21 Sep 2022 18:45:05 +0200 Subject: [PATCH 23/26] rename normalization error module --- MANIFEST | 2 +- lib/Zonemaster/Engine/Normalization.pm | 22 +++++++++---------- .../Normalization/{Errors.pm => Error.pm} | 8 +++---- 3 files changed, 16 insertions(+), 16 deletions(-) rename lib/Zonemaster/Engine/Normalization/{Errors.pm => Error.pm} (90%) diff --git a/MANIFEST b/MANIFEST index a2c814b05..461bff6b2 100644 --- a/MANIFEST +++ b/MANIFEST @@ -27,7 +27,7 @@ lib/Zonemaster/Engine/Logger/Entry.pm lib/Zonemaster/Engine/Nameserver.pm lib/Zonemaster/Engine/Nameserver/Cache.pm lib/Zonemaster/Engine/Normalization.pm -lib/Zonemaster/Engine/Normalization/Errors.pm +lib/Zonemaster/Engine/Normalization/Error.pm lib/Zonemaster/Engine/Net/IP.pm lib/Zonemaster/Engine/NSArray.pm lib/Zonemaster/Engine/Overview.pod diff --git a/lib/Zonemaster/Engine/Normalization.pm b/lib/Zonemaster/Engine/Normalization.pm index 3c903b1e3..50e1046cc 100644 --- a/lib/Zonemaster/Engine/Normalization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -15,7 +15,7 @@ use Try::Tiny; use Zonemaster::LDNS; use Data::Dumper; -use Zonemaster::Engine::Normalization::Errors; +use Zonemaster::Engine::Normalization::Error; =head1 NAME @@ -92,7 +92,7 @@ Downcasing of upper case non-ASCII characters, normalization to the Unicode NFC format and conversion from U-label to A-label is performed by libidn2 using L. -Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Errors], $alabel: String)>. +Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Error], $alabel: String)>. In case of errors, the returned label will be undefined. If the method succeeded an empty error array is returned. @@ -108,14 +108,14 @@ sub normalize_label { if ( $label =~ $VALID_ASCII ) { $alabel = lc $label; } elsif ( $label =~ $ASCII ) { - push @messages, Zonemaster::Engine::Normalization::Errors->new(INVALID_ASCII => {label => $label}); + push @messages, Zonemaster::Engine::Normalization::Error->new(INVALID_ASCII => {label => $label}); return \@messages, undef; } elsif (Zonemaster::LDNS::has_idn) { try { $alabel = Zonemaster::LDNS::to_idn($label); } catch { - push @messages, Zonemaster::Engine::Normalization::Errors->new(INVALID_U_LABEL => {label => $label}); + push @messages, Zonemaster::Engine::Normalization::Error->new(INVALID_U_LABEL => {label => $label}); return \@messages, undef; } @@ -124,7 +124,7 @@ sub normalize_label { } if ( length($alabel) > 63) { - push @messages, Zonemaster::Engine::Normalization::Errors->new(LABEL_TOO_LONG => {label => $label}); + push @messages, Zonemaster::Engine::Normalization::Error->new(LABEL_TOO_LONG => {label => $label}); return \@messages, undef; } @@ -138,7 +138,7 @@ Normalize a domain name. The normalization process is detailed in the L. -Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Errors], $name: String)>. +Returns a tuple C<($errors: ArrayRef[Zonemaster::Engine::Normalization::Error], $name: String)>. In case of errors, the returned name will be undefined. If the method succeeded an empty error array is returned. @@ -153,14 +153,14 @@ sub normalize_name { $uname =~ s/${WHITE_SPACES_RE}+$//; if (length($uname) == 0) { - push @messages, Zonemaster::Engine::Normalization::Errors->new(EMPTY_DOMAIN_NAME => {}); + push @messages, Zonemaster::Engine::Normalization::Error->new(EMPTY_DOMAIN_NAME => {}); return \@messages, undef; } foreach my $char_name (keys %AMBIGUOUS_CHARACTERS) { my $char = $AMBIGUOUS_CHARACTERS{$char_name}; if ($uname =~ m/${char}/) { - push @messages, Zonemaster::Engine::Normalization::Errors->new(AMBIGUOUS_DOWNCASING => { unicode_name => $char_name }); + push @messages, Zonemaster::Engine::Normalization::Error->new(AMBIGUOUS_DOWNCASING => { unicode_name => $char_name }); } } @@ -175,12 +175,12 @@ sub normalize_name { } if ($uname =~ m/^${ASCII_FULL_STOP_RE}/) { - push @messages, Zonemaster::Engine::Normalization::Errors->new(INITIAL_DOT => {}); + push @messages, Zonemaster::Engine::Normalization::Error->new(INITIAL_DOT => {}); return \@messages, undef; } if ($uname =~ m/${ASCII_FULL_STOP_RE}{2,}/ ) { - push @messages, Zonemaster::Engine::Normalization::Errors->new(REPEATED_DOTS => {}); + push @messages, Zonemaster::Engine::Normalization::Error->new(REPEATED_DOTS => {}); return \@messages, undef; } @@ -201,7 +201,7 @@ sub normalize_name { my $final_name = join '.', @label_ok; if (length($final_name) > 253) { - push @messages, Zonemaster::Engine::Normalization::Errors->new(DOMAIN_NAME_TOO_LONG => {}); + push @messages, Zonemaster::Engine::Normalization::Error->new(DOMAIN_NAME_TOO_LONG => {}); return \@messages, undef; } diff --git a/lib/Zonemaster/Engine/Normalization/Errors.pm b/lib/Zonemaster/Engine/Normalization/Error.pm similarity index 90% rename from lib/Zonemaster/Engine/Normalization/Errors.pm rename to lib/Zonemaster/Engine/Normalization/Error.pm index e99b8e413..95f99f5ba 100644 --- a/lib/Zonemaster/Engine/Normalization/Errors.pm +++ b/lib/Zonemaster/Engine/Normalization/Error.pm @@ -1,4 +1,4 @@ -package Zonemaster::Engine::Normalization::Errors; +package Zonemaster::Engine::Normalization::Error; use strict; use warnings; @@ -12,14 +12,14 @@ use overload '""' => \&string; =head1 NAME -Zonemaster::Engine::Normalization::Errors - normalization error class +Zonemaster::Engine::Normalization::Error - normalization error class =head1 SYNOPSIS - use Zonemaster::Engine::Normalization::Errors; + use Zonemaster::Engine::Normalization::Error; - my $error = Zonemaster::Engine::Normalization::Errors->new(LABEL_TOO_LONG => {label => $label}); + my $error = Zonemaster::Engine::Normalization::Error->new(LABEL_TOO_LONG => {label => $label}); =cut From a354e4c3c947297b806143d8ad6051fecc8864f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Mon, 21 Nov 2022 11:55:25 +0100 Subject: [PATCH 24/26] fix coding style --- lib/Zonemaster/Engine/Normalization.pm | 17 ++++++++--------- lib/Zonemaster/Engine/Normalization/Error.pm | 6 +++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/Zonemaster/Engine/Normalization.pm b/lib/Zonemaster/Engine/Normalization.pm index 50e1046cc..b0bbf4324 100644 --- a/lib/Zonemaster/Engine/Normalization.pm +++ b/lib/Zonemaster/Engine/Normalization.pm @@ -13,7 +13,6 @@ use Encode; use Readonly; use Try::Tiny; use Zonemaster::LDNS; -use Data::Dumper; use Zonemaster::Engine::Normalization::Error; @@ -111,7 +110,7 @@ sub normalize_label { push @messages, Zonemaster::Engine::Normalization::Error->new(INVALID_ASCII => {label => $label}); return \@messages, undef; - } elsif (Zonemaster::LDNS::has_idn) { + } elsif ( Zonemaster::LDNS::has_idn ) { try { $alabel = Zonemaster::LDNS::to_idn($label); } catch { @@ -123,7 +122,7 @@ sub normalize_label { croak 'The domain name contains at least one non-ASCII character and this installation of Zonemaster has no support for IDNA.'; } - if ( length($alabel) > 63) { + if ( length($alabel) > 63 ) { push @messages, Zonemaster::Engine::Normalization::Error->new(LABEL_TOO_LONG => {label => $label}); return \@messages, undef; } @@ -152,14 +151,14 @@ sub normalize_name { $uname =~ s/^${$WHITE_SPACES_RE}+//; $uname =~ s/${WHITE_SPACES_RE}+$//; - if (length($uname) == 0) { + if ( length($uname) == 0 ) { push @messages, Zonemaster::Engine::Normalization::Error->new(EMPTY_DOMAIN_NAME => {}); return \@messages, undef; } - foreach my $char_name (keys %AMBIGUOUS_CHARACTERS) { + foreach my $char_name ( keys %AMBIGUOUS_CHARACTERS ) { my $char = $AMBIGUOUS_CHARACTERS{$char_name}; - if ($uname =~ m/${char}/) { + if ( $uname =~ m/${char}/) { push @messages, Zonemaster::Engine::Normalization::Error->new(AMBIGUOUS_DOWNCASING => { unicode_name => $char_name }); } } @@ -174,12 +173,12 @@ sub normalize_name { return \@messages, $uname; } - if ($uname =~ m/^${ASCII_FULL_STOP_RE}/) { + if ( $uname =~ m/^${ASCII_FULL_STOP_RE}/ ) { push @messages, Zonemaster::Engine::Normalization::Error->new(INITIAL_DOT => {}); return \@messages, undef; } - if ($uname =~ m/${ASCII_FULL_STOP_RE}{2,}/ ) { + if ( $uname =~ m/${ASCII_FULL_STOP_RE}{2,}/ ) { push @messages, Zonemaster::Engine::Normalization::Error->new(REPEATED_DOTS => {}); return \@messages, undef; } @@ -200,7 +199,7 @@ sub normalize_name { my $final_name = join '.', @label_ok; - if (length($final_name) > 253) { + if ( length($final_name) > 253 ) { push @messages, Zonemaster::Engine::Normalization::Error->new(DOMAIN_NAME_TOO_LONG => {}); return \@messages, undef; } diff --git a/lib/Zonemaster/Engine/Normalization/Error.pm b/lib/Zonemaster/Engine/Normalization/Error.pm index 95f99f5ba..b8b05d04f 100644 --- a/lib/Zonemaster/Engine/Normalization/Error.pm +++ b/lib/Zonemaster/Engine/Normalization/Error.pm @@ -84,15 +84,15 @@ sub new { my ( $proto, $tag, $params ) = @_; my $class = ref $proto || $proto; - if (!exists $ERRORS{$tag}) { + if ( !exists $ERRORS{$tag} ) { croak 'Unknown error tag.'; } my $obj = { tag => $tag, params => {} }; - if (exists $ERRORS{$tag}->{arguments}) { + if ( exists $ERRORS{$tag}->{arguments} ) { foreach my $arg ( @{$ERRORS{$tag}->{arguments}} ) { - if (!exists $params->{$arg} ) { + if ( !exists $params->{$arg} ) { croak "Missing arguments $arg."; } $obj->{params}->{$arg} = $params->{$arg}; From f9095e1bf1789ebff191af4cfd10bf95fd3c696a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Mon, 21 Nov 2022 11:58:49 +0100 Subject: [PATCH 25/26] fix typo in doc --- lib/Zonemaster/Engine/Normalization/Error.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Zonemaster/Engine/Normalization/Error.pm b/lib/Zonemaster/Engine/Normalization/Error.pm index b8b05d04f..671d8ec80 100644 --- a/lib/Zonemaster/Engine/Normalization/Error.pm +++ b/lib/Zonemaster/Engine/Normalization/Error.pm @@ -61,7 +61,7 @@ Readonly my %ERRORS => ( =item tag -The message tag asscociated to the error. +The message tag associated to the error. =item params @@ -129,7 +129,7 @@ sub tag { =item string -Returns a string representation of the error object, equivalent to message. +Returns a string representation of the error object. Euivalent to message(). =cut From 9b248a674b7e9088d4376ef986a8b1ebb9c28edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Thu, 24 Nov 2022 11:34:43 +0100 Subject: [PATCH 26/26] fix typo --- lib/Zonemaster/Engine/Normalization/Error.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Zonemaster/Engine/Normalization/Error.pm b/lib/Zonemaster/Engine/Normalization/Error.pm index 671d8ec80..ba30fd4c7 100644 --- a/lib/Zonemaster/Engine/Normalization/Error.pm +++ b/lib/Zonemaster/Engine/Normalization/Error.pm @@ -129,7 +129,7 @@ sub tag { =item string -Returns a string representation of the error object. Euivalent to message(). +Returns a string representation of the error object. Equivalent to message(). =cut