Skip to content

next16: report invalid_utf16 for a lone trail surrogate at the end of a range - #147

Merged
nemtrif merged 1 commit into
nemtrif:masterfrom
youdie006:next16-lone-trail-surrogate
Sep 12, 2026
Merged

nemtrif merged 1 commit into
nemtrif:masterfrom
youdie006:next16-lone-trail-surrogate

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

Problem

utf8::next16 reports a different error for the same invalid first word depending on what follows it:

const utfchar16_t a[] = {0xdc00, 0x0041};
const utfchar16_t* w = a;
next16(w, a + 2);   // throws invalid_utf16   <- asserted by test_next16 (trail_first)

const utfchar16_t b[] = {0xdc00};
w = b;
next16(w, b + 1);   // throws not_enough_room <- but 0xdc00 is invalid on its own

0xdc00 is a trail surrogate. It is invalid UTF-16 no matter how many words come after it, so no second word can make it valid and "not enough room" is not the right diagnosis.

The cause is statement order in internal::validate_next16 (source/utf8/core.h:437-441 on main): the it == end test runs before the code decides whether first_word is a lead surrogate, so the lone-trail case never reaches the INVALID_LEAD branch.

This is the one cell your own test table does not cover. 87ca49f ("next16 properly reports errors", fixing #144) wired up the mapping in checked.h:177-178 and added a table to tests/test_checked_api.h:

first word followed by expected
lead 0xd800 nothing not_enough_room
lead 0xd800 0x0041 invalid_utf16
trail 0xdc00 0x0041 invalid_utf16
trail 0xdc00 nothing not tested

The fourth row is the one that is still wrong.

API_REFERENCE.md:209 scopes not_enough_room to "if it gets equal to end during the extraction of a code point", and API_REFERENCE.md:225 says an invalid UTF-16 sequence throws invalid_utf16. With a lone trail surrogate there is no code point being extracted, so the second sentence is the applicable one. utf16to8 agrees already: checked.h:247-248 throws invalid_utf16 for a lone trail surrogate regardless of position.

Fix

Move the is_lead_surrogate test above the end-of-range test. One else if branch, no new logic. A lead surrogate at the end of a range still returns NOT_ENOUGH_ROOM, since that one really is truncated.

Verification

g++ 13.3.0, linux/amd64, base 30e55c2, built with the flags from tests/CMakeLists.txt (-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion, per-target -std).

  • Green: all six suites pass - negative 12, cpp11 10, cpp17 9, cpp20 8, apitests 35 (34 before, +1 new), noexceptionstests 15. No new warnings.
  • Red: with core.h reverted to main and the new test in place, apitests fails exactly one test, CheckedAPITests.test_next16_lone_trail_surrogate.
  • Over-correction: mutating the other direction (it == end -> INVALID_LEAD, so a lead surrogate at the end is reported as invalid too) fails exactly one test, and it is the pre-existing CheckedAPITests.test_next16. The two failure sets are disjoint, so the boundary is pinned from both sides by the repo's own table plus the new case.

The new test only adds the missing row; I deliberately did not duplicate the three rows test_next16 already covers.


Disclosure: I used an AI assistant while preparing this change. I read, built, ran and verified everything above myself. If you would rather keep the current behaviour and simply document it, I am happy to close this.

… a range

validate_next16 returns NOT_ENOUGH_ROOM whenever the second word is missing,
without first asking whether the first word is a lead surrogate. A trail
surrogate is invalid UTF-16 on its own, so no second word can rescue it:

    {0xdc00, 0x0041}  ->  invalid_utf16   (test_next16, trail_first)
    {0xdc00}          ->  not_enough_room

Move the is_lead_surrogate test above the end-of-range test so the reported
error depends on the first word rather than on what happens to follow it.
NOT_ENOUGH_ROOM is still returned for a lead surrogate at the end of the
range, which is a genuinely truncated sequence.
@nemtrif
nemtrif merged commit 32e3ed1 into nemtrif:master Sep 12, 2026
3 checks passed
@nemtrif

nemtrif commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants