This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
Return REPLACEMENT for UTF-8 overlong malformation
authorKarl Williamson <khw@cpan.org>
Sat, 10 Dec 2016 22:26:24 +0000 (15:26 -0700)
committerKarl Williamson <khw@cpan.org>
Fri, 23 Dec 2016 23:48:35 +0000 (16:48 -0700)
When perl decodes UTF-8 into a code point, it must decide what to do if
the input is malformed in some way.  When the flags passed to the decode
function indicate that a given malformation type is not acceptable, the
function returns 0 to indicate failure; on success it returns the decoded
code point (unfortunately that may require disambiguation if the
input is validly a NUL).  As perl evolved, what happened when various
allowed malformations were encountered got stricter and stricter.  This
is the final malformation that was not turned into a REPLACEMENT
CHARACTER when the malformation was allowed, and this commit changes to
return that.  Unlike most other malformations, the code point value of
an overlong is well-defined, and that is why it hadn't been changed
here-to-fore.  But it is safer to use the Unicode prescribed behavior on
all malformations, which is to replace them with the REPLACEMENT
CHARACTER.  Just in case there is code that requires the old behavior,
it is retained, but you have to search the source for the undocumented
flag that enables it.

ext/XS-APItest/t/utf8.t
pod/perldelta.pod
utf8.c
utf8.h

index e8ed76e..5fe56df 100644 (file)
@@ -98,6 +98,7 @@ my $UTF8_GOT_NON_CONTINUATION   = $UTF8_ALLOW_NON_CONTINUATION;
 my $UTF8_ALLOW_SHORT            = 0x0008;
 my $UTF8_GOT_SHORT              = $UTF8_ALLOW_SHORT;
 my $UTF8_ALLOW_LONG             = 0x0010;
+my $UTF8_ALLOW_LONG_AND_ITS_VALUE = $UTF8_ALLOW_LONG|0x0020;
 my $UTF8_GOT_LONG               = $UTF8_ALLOW_LONG;
 my $UTF8_GOT_OVERFLOW           = 0x0080;
 my $UTF8_DISALLOW_SURROGATE     = 0x0100;
@@ -1422,6 +1423,29 @@ else { # 64-bit ASCII, or EBCDIC of any size.
     }
 }
 
+# For each overlong malformation in the list, we modify it, so that there are
+# two tests.  The first one returns the replacement character given the input
+# flags, and the second test adds a flag that causes the actual code point the
+# malformation represents to be returned.
+my @added_overlongs;
+foreach my $test (@malformations) {
+    my ($testname, $bytes, $length, $allow_flags, $expected_error_flags,
+        $allowed_uv, $expected_len, $needed_to_discern_len, $message ) = @$test;
+    next unless $testname =~ /overlong/;
+
+    $test->[0] .= "; use REPLACEMENT CHAR";
+    $test->[5] = $REPLACEMENT;
+
+    push @added_overlongs,
+        [ $testname . "; use actual value",
+          $bytes, $length,
+          $allow_flags | $UTF8_ALLOW_LONG_AND_ITS_VALUE,
+          $expected_error_flags, $allowed_uv, $expected_len,
+          $needed_to_discern_len, $message
+        ];
+}
+push @malformations, @added_overlongs;
+
 foreach my $test (@malformations) {
     my ($testname, $bytes, $length, $allow_flags, $expected_error_flags,
         $allowed_uv, $expected_len, $needed_to_discern_len, $message ) = @$test;
index fde4186..1c770d2 100644 (file)
@@ -346,6 +346,15 @@ passing a string length of 0 is now asserted against in DEBUGGING
 builds, and otherwise returns the Unicode REPLACEMENT CHARACTER.   If
 you have nothing to decode, you shouldn't call the decode function.
 
+=item *
+
+The functions C<utf8n_to_uvchr> and its derivatives now return the
+Unicode REPLACEMENT CHARACTER if called with UTF-8 that has the overlong
+malformation, and that malformation is allowed by the input parameters.
+This malformation is where the UTF-8 looks valid syntactically, but
+there is a shorter sequence that yields the same code point.  This has
+been forbidden since Unicode version 3.1.
+
 =back
 
 =head1 Selected Bug Fixes
diff --git a/utf8.c b/utf8.c
index d34597b..d5e675b 100644 (file)
--- a/utf8.c
+++ b/utf8.c
@@ -875,9 +875,10 @@ is, when there is a shorter sequence that can express the same code point;
 overlong sequences are expressly forbidden in the UTF-8 standard due to
 potential security issues).  Another malformation example is the first byte of
 a character not being a legal first byte.  See F<utf8.h> for the list of such
-flags.  For allowed overlong sequences, the computed code point is returned;
-for all other allowed malformations, the Unicode REPLACEMENT CHARACTER is
-returned.
+flags.  Even if allowed, this function generally returns the Unicode
+REPLACEMENT CHARACTER when it encounters a malformation.  There are flags in
+F<utf8.h> to override this behavior for the overlong malformations, but don't
+do that except for very specialized purposes.
 
 The C<UTF8_CHECK_ONLY> flag overrides the behavior when a non-allowed (by other
 flags) malformation is found.  If this flag is set, the routine assumes that
@@ -1465,7 +1466,17 @@ Perl_utf8n_to_uvchr_error(pTHX_ const U8 *s,
                 possible_problems &= ~UTF8_GOT_LONG;
                 *errors |= UTF8_GOT_LONG;
 
-                if (! (flags & UTF8_ALLOW_LONG)) {
+                if (flags & UTF8_ALLOW_LONG) {
+
+                    /* We don't allow the actual overlong value, unless the
+                     * special extra bit is also set */
+                    if (! (flags & (   UTF8_ALLOW_LONG_AND_ITS_VALUE
+                                    & ~UTF8_ALLOW_LONG)))
+                    {
+                        uv = UNICODE_REPLACEMENT;
+                    }
+                }
+                else {
                     disallowed = TRUE;
 
                     if (ckWARN_d(WARN_UTF8) && ! (flags & UTF8_CHECK_ONLY)) {
diff --git a/utf8.h b/utf8.h
index a4cae09..3dde45a 100644 (file)
--- a/utf8.h
+++ b/utf8.h
@@ -738,8 +738,11 @@ case any call to string overloading updates the internal UTF-8 encoding flag.
 #define UTF8_ALLOW_SHORT               0x0008
 #define UTF8_GOT_SHORT                 UTF8_ALLOW_SHORT
 
-/* Overlong sequence; i.e., the code point can be specified in fewer bytes. */
+/* Overlong sequence; i.e., the code point can be specified in fewer bytes.
+ * First one will convert the overlong to the REPLACEMENT CHARACTER; second
+ * will return what the overlong evaluates to */
 #define UTF8_ALLOW_LONG                 0x0010
+#define UTF8_ALLOW_LONG_AND_ITS_VALUE   (UTF8_ALLOW_LONG|0x0020)
 #define UTF8_GOT_LONG                   UTF8_ALLOW_LONG
 
 /* Currently no way to allow overflow */