Encode-Wide
view release on metacpan or search on metacpan
t/edge_cases.t view on Meta::CPAN
#!/usr/bin/env perl
# Destructive, boundary, and security edge-case tests for Encode::Wide.
#
# Strategy: feed hostile, malformed, or pathological inputs to every code
# path and verify the module either handles them gracefully (no crash, no
# state corruption, pure-ASCII output) or dies with a clearly documented
# error. All tests assert INTENDED behaviour per the POD, not accidental
# behaviour. Coverage already in edge.t / unit.t / integration.t is not
# repeated here.
use strict;
use warnings;
use Test::Most;
use Test::Mockingbird qw(mock restore_all);
use Readonly;
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
Readonly::Scalar my $MODULE => 'Encode::Wide';
Readonly::Scalar my $E_ACUTE => "\x{00E9}"; # U+00E9 e-acute
Readonly::Scalar my $A_GRAVE => "\x{00E0}"; # U+00A0 a-grave
Readonly::Scalar my $NBSP => "\x{00A0}"; # U+00A0 non-breaking space
Readonly::Scalar my $BOM => "\x{FEFF}"; # U+FEFF byte-order mark
Readonly::Scalar my $ELLIPSIS => "\x{2026}"; # U+2026 horizontal ellipsis
Readonly::Scalar my $BULLET => "\x{25CF}"; # U+25CF black circle
Readonly::Scalar my $LSQUO => "\x{2018}"; # U+2018 left single quotation mark
Readonly::Scalar my $RSQUO => "\x{2019}"; # U+2019 right single quotation mark
Readonly::Scalar my $LDQUO => "\x{201C}"; # U+201C left double quotation mark
Readonly::Scalar my $RDQUO => "\x{201D}"; # U+201D right double quotation mark
Readonly::Scalar my $PUA => "\x{E000}"; # U+E000 private-use (unmapped -> BUG die)
Readonly::Scalar my $LARGE_N => 10_000; # repeat count for stress tests
use_ok($MODULE, qw(wide_to_html wide_to_xml));
# ===========================================================================
# 1. Numerically-false but defined scalars
# ---------------------------------------------------------------------------
# The POD says string must be *defined*. "0" and integer 0 are defined and
# must pass through without dying, returning pure ASCII.
# ===========================================================================
subtest 'Numerically false string "0" is not treated as undef' => sub {
# If the module checked `if(!$string)` rather than `if(!defined($string))`
# it would incorrectly die for "0".
lives_ok { wide_to_html(string => '0') } 'html: string "0" does not die';
lives_ok { wide_to_xml(string => '0') } 'xml: string "0" does not die';
is(wide_to_html(string => '0'), '0', 'html: string "0" returns "0"');
is(wide_to_xml(string => '0'), '0', 'xml: string "0" returns "0"');
};
subtest 'Integer 0 as string argument is not treated as undef' => sub {
lives_ok { wide_to_html(string => 0) } 'html: integer 0 does not die';
lives_ok { wide_to_xml(string => 0) } 'xml: integer 0 does not die';
is(wide_to_html(string => 0), '0', 'html: integer 0 returns "0"');
is(wide_to_xml(string => 0), '0', 'xml: integer 0 returns "0"');
};
subtest 'Single-space string is not treated as undef' => sub {
is(wide_to_html(string => ' '), ' ', 'html: space string returns " "');
is(wide_to_xml(string => ' '), ' ', 'xml: space string returns " "');
};
# ===========================================================================
# 2. NUL and DEL characters (ASCII boundary codepoints)
# ---------------------------------------------------------------------------
# U+0000 (NUL) and U+007F (DEL) are ASCII. They are not wide characters,
# so neither pipeline should alter them.
# ===========================================================================
subtest 'NUL byte (U+0000) passes through unchanged' => sub {
my $nul = "\x{0000}";
is(wide_to_html(string => $nul), $nul, 'html: bare NUL unchanged');
is(wide_to_xml(string => $nul), $nul, 'xml: bare NUL unchanged');
is(wide_to_html(string => "a${nul}b"), "a${nul}b", 'html: NUL in middle unchanged');
is(wide_to_xml(string => "a${nul}b"), "a${nul}b", 'xml: NUL in middle unchanged');
};
subtest 'DEL character (U+007F) passes through unchanged' => sub {
my $del = "\x{007F}";
is(wide_to_html(string => $del), $del, 'html: DEL unchanged');
is(wide_to_xml(string => $del), $del, 'xml: DEL unchanged');
};
# ===========================================================================
# 3. Byte-Order Mark (U+FEFF)
# ---------------------------------------------------------------------------
# BOM is non-ASCII and not in any known byte_map entry. It should reach the
# HTML::Entities::encode_entities_numeric fallback and become a numeric entity.
# At minimum the output must be pure ASCII.
# ===========================================================================
subtest 'BOM (U+FEFF) is handled per documented pipeline behaviour' => sub {
# HTML: encode_entities_numeric covers U+FEFF -> numeric entity -> pure ASCII.
my $html_bom;
open(local *STDERR, '>', '/dev/null') or die;
local $SIG{__WARN__} = sub { };
lives_ok { $html_bom = wide_to_html(string => $BOM) } 'html: BOM does not die';
unlike($html_bom, qr/[^[:ascii:]]/, 'html: BOM result is pure ASCII');
diag "html BOM: $html_bom" if $ENV{TEST_VERBOSE};
# XML: U+FEFF is not in any XML byte_map entry; the documented BUG path fires.
open(local *STDERR, '>', '/dev/null') or die;
local $SIG{__WARN__} = sub { };
throws_ok { wide_to_xml(string => $BOM) }
qr/BUG: wide_to_xml/,
'xml: BOM (unmapped in XML maps) triggers documented BUG die';
};
# ===========================================================================
# 4. Hostile reference types
# ---------------------------------------------------------------------------
# Passing ARRAYREF, HASHREF, or CODEREF as string is outside the POD contract.
# The module must not produce non-ASCII output, must not execute injected code,
# and must not hang or corrupt memory. It may die.
# ===========================================================================
subtest 'ARRAYREF as string: terminates, no non-ASCII output' => sub {
my $aref = [1, 2, 3];
for my $fn (qw(wide_to_html wide_to_xml)) {
no strict 'refs';
my $result;
eval { $result = $fn->(string => $aref) };
if($@) {
diag "$fn: arrayref caused die (acceptable): $@" if $ENV{TEST_VERBOSE};
pass("$fn: arrayref input terminated (die acceptable)");
} else {
unlike($result, qr/[^[:ascii:]]/, "$fn: arrayref stringification is ASCII");
}
}
};
subtest 'HASHREF as string: terminates, no non-ASCII output' => sub {
my $href = { key => 'val' };
for my $fn (qw(wide_to_html wide_to_xml)) {
no strict 'refs';
my $result;
eval { $result = $fn->(string => $href) };
if($@) {
pass("$fn: hashref input terminated (die acceptable)");
} else {
unlike($result, qr/[^[:ascii:]]/, "$fn: hashref stringification is ASCII");
}
}
};
subtest 'CODEREF as string: terminates, no non-ASCII output' => sub {
my $cref = sub { 42 };
for my $fn (qw(wide_to_html wide_to_xml)) {
no strict 'refs';
my $result;
eval { $result = $fn->(string => $cref) };
if($@) {
pass("$fn: coderef input terminated (die acceptable)");
} else {
unlike($result, qr/[^[:ascii:]]/, "$fn: coderef stringification is ASCII");
}
}
};
# ===========================================================================
# 5. Circular scalar reference (must not infinite-loop)
# ---------------------------------------------------------------------------
# A scalar variable that references itself is hostile input that could cause
# an infinite deref loop in naive code. The module must terminate within a
# reasonable time because it only follows one level of SCALARREF dereference.
# ===========================================================================
( run in 0.860 second using v1.01-cache-2.11-cpan-8dfa8b56332 )