blob: 6044e089721fb5108029fa45b8eee06bb967a399 [file]
#!/usr/bin/perl
use strict;
use warnings FATAL => 'all';
use lib 'blib/lib'; use lib '../blib/lib';
use Test::More;
use Encode qw();
use Mail::SpamAssassin::Message::Node;
# ---------------------------------------------------------------------------
# Direct unit tests for the UTF-16 handling in Mail::SpamAssassin::Message::Node:
#
# * detect_utf16($bytes) - returns an Encode decoder for UTF-16 input
# (BOM'd or BOM-less, either endianness).
# * _normalize($bytes, 'UTF-16', 0, 0) - transcodes UTF-16 octets to UTF-8
# octets via that decoder.
#
# Regression guard: detect_utf16() used to return undef when a BOM was present
# ("let perl figure it out from the BOM"), but its only caller only ever used the
# decoder it returned -- so BOM'd UTF-16 dropped through to the Windows-1252 last
# resort in _normalize() and was mangled. The BOM'd cases below cover that.
# ---------------------------------------------------------------------------
my $sample = "The quick brown fox jumps over the lazy dog.\n";
my $le_bom = "\xff\xfe" . Encode::encode('UTF-16LE', $sample);
my $be_bom = "\xfe\xff" . Encode::encode('UTF-16BE', $sample);
my $le_nobom = Encode::encode('UTF-16LE', $sample);
my $be_nobom = Encode::encode('UTF-16BE', $sample);
my $utf8 = Encode::encode('UTF-8', "caf\x{e9} \x{4e16}\x{754c}\n");
my $ascii = "plain ascii, no nulls here\n";
plan tests => 6;
# --- detect_utf16: returns the CORRECT decoder for every UTF-16 variant ------
# The BOM'd cases are the regression guard (before the fix these returned undef).
# A BOM declares its own endianness, so both BOM'd inputs must resolve to the
# BOM-aware 'UTF-16' codec -- NOT UTF-16LE/UTF-16BE, which would ignore the BOM
# and mis-decode the opposite endianness. BOM-less inputs must resolve to the
# specific endianness the heuristic sniffed. We check both the decoder's name
# and that it actually decodes the sample back to the original text.
my @detect = (
['BOM LE', $le_bom, 'UTF-16'],
['BOM BE', $be_bom, 'UTF-16'],
['no-BOM LE', $le_nobom, 'UTF-16LE'],
['no-BOM BE', $be_nobom, 'UTF-16BE'],
);
for my $c (@detect) {
my ($name, $bytes, $want) = @$c;
subtest "detect_utf16: $name -> $want" => sub {
plan tests => 2;
my $dec = Mail::SpamAssassin::Message::Node::detect_utf16($bytes);
is(defined $dec ? $dec->name : undef, $want,
"returns the $want decoder");
is(defined $dec ? $dec->decode($bytes, Encode::LEAVE_SRC) : undef, $sample,
"that decoder round-trips the sample text");
};
}
# --- _normalize('UTF-16'): every variant transcodes to clean UTF-8 ----------
# Result must be UTF-8 octets: the ASCII text is contiguous again (a BOM'd input
# that fell through to Windows-1252 would keep its interleaved NUL bytes and the
# mangled BOM, so both checks would fail).
subtest '_normalize decodes all UTF-16 variants to UTF-8' => sub {
plan tests => 4;
for my $c (['BOM LE', $le_bom], ['BOM BE', $be_bom],
['no-BOM LE', $le_nobom], ['no-BOM BE', $be_nobom]) {
my ($name, $bytes) = @$c;
my $out = Mail::SpamAssassin::Message::Node::_normalize($bytes, 'UTF-16', 0, 0);
ok($out =~ /\QThe quick brown fox\E/ && index($out, "\x00") < 0,
"$name decoded (BOM consumed, no NULs)");
}
};
# --- _normalize leaves already-UTF-8 and plain ASCII unchanged --------------
# Each input is declared with the charset it actually is, so we exercise the
# matching decode path rather than relying on the "try UTF-8 first" fallback.
subtest '_normalize leaves UTF-8 / ASCII unchanged' => sub {
plan tests => 2;
is(Mail::SpamAssassin::Message::Node::_normalize($utf8, 'UTF-8', 0, 0), $utf8,
'valid UTF-8 octets unchanged');
is(Mail::SpamAssassin::Message::Node::_normalize($ascii, 'us-ascii', 0, 0), $ascii,
'plain ASCII unchanged');
};