Skip to content

Guard AllNumberGroupsRemainGrouped against indexOf/find(-1) for the country-code skip (Java + C++) - #4067

Open
twcclegg wants to merge 2 commits into
google:masterfrom
twcclegg:fix/matcher-country-code-indexof-guard
Open

Guard AllNumberGroupsRemainGrouped against indexOf/find(-1) for the country-code skip (Java + C++)#4067
twcclegg wants to merge 2 commits into
google:masterfrom
twcclegg:fix/matcher-country-code-indexof-guard

Conversation

@twcclegg

@twcclegg twcclegg commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Issue

https://issuetracker.google.com/issues/552025944

What's wrong

In PhoneNumberMatcher.allNumberGroupsRemainGrouped (Java), when the country calling code isn't a literal substring of the candidate text, indexOf returns -1, and:

fromIndex = normalizedCandidate.indexOf(countryCode) + countryCode.length();

leaves fromIndex at a small, wrong, non-negative value instead of "not found." This desyncs the subsequent group-matching loop, which can make a grouping check that should succeed silently return false instead — i.e. a valid phone number embedded in free text can fail to match, with no exception. The very next indexOf call in the same method (one line below, inside the loop) already guards against exactly this with if (fromIndex < 0) { return false; }; this fix applies the same guard to the country-code skip.

In practice this is probably rare, since a matched candidate's text usually does contain the country code's digits somewhere. Confirmed with a constructed case (country code 44, candidate text that doesn't contain "44") added as a new unit test.

Fix

Guard the indexOf result before using it, leaving fromIndex at its default (0, i.e. "don't skip anything") when the country code isn't found as a literal substring, rather than propagating a bogus offset.

Scope across languages

Per the contributing guide's note to check whether a fix applies in C++, Java, and JS:

  • Java — fixed (this is the primary fix).
  • C++ (cpp/src/phonenumbers/phonenumbermatcher.cc) — has the exact same pattern with std::string::find, and it's arguably worse there: find returns npos (size_t max) on a miss, and the unguarded + country_code.size() wraps around via unsigned overflow into a small, wrong, non-negative from_index instead of crashing loudly. Fixed with the same guard.
  • JS — not applicable. The JS port has no PhoneNumberMatcher (find-numbers-in-text) implementation at all; allNumberGroupsRemainGrouped/AllNumberGroupsRemainGrouped doesn't exist there. Verified by grepping javascript/i18n/phonenumbers/ for the function and for any matcher module — only asyoutypeformatter.js and phonenumberutil.js (parse/format/validate) exist, no text-scanning matcher.

Testing

No local JDK/C++ toolchain was available to me to run the test suites, so I traced both fixes by hand and I'd appreciate a maintainer/CI double-check.

  • Java: testAllNumberGroupsRemainGroupedCountryCodeNotInCandidate calls the package-private allNumberGroupsRemainGrouped directly with a PhoneNumber whose countryCodeSource is FROM_NUMBER_WITH_PLUS_SIGN (country code 44) and a candidate string that never contains the literal digits "44". Before the fix this returns false (bug); after the fix it returns true.
  • C++: no test added. AllNumberGroupsRemainGrouped is in an anonymous namespace in phonenumbermatcher.cc, so unlike the Java package-private method it isn't reachable from the test binary in a different translation unit for a direct unit test, and I didn't want to change its linkage just to test this. Happy to take a suggestion from a maintainer on how this is normally exercised (e.g. via PhoneNumberMatcher::Match/Find) if a regression test is wanted here.

…(-1) for the country-code skip

When the country calling code isn't a literal substring of the
candidate text, StringBuilder.indexOf returns -1 and fromIndex became
-1 + countryCode.length() instead of a proper "not found" value,
silently desyncing the subsequent group-matching loop. Guard it the
same way the check one line below already guards its own indexOf
call.
@twcclegg
twcclegg requested a review from a team as a code owner August 24, 2026 23:20
Same bug as the Java fix, applied to AllNumberGroupsRemainGrouped in
phonenumbermatcher.cc: std::string::find returns string::npos on a
miss, and the unguarded "+ country_code.size()" wrapped that around
via unsigned overflow into a small, wrong, non-negative from_index
instead of leaving it as "not found."

AllNumberGroupsRemainGrouped is in an anonymous namespace here, so
unlike the Java package-private method it isn't reachable from the
test binary for a direct unit test; JS has no PhoneNumberMatcher
(find-numbers-in-text) implementation at all, so no fix is needed
there.
@twcclegg twcclegg changed the title Guard allNumberGroupsRemainGrouped against indexOf(-1) for the country-code skip Guard AllNumberGroupsRemainGrouped against indexOf/find(-1) for the country-code skip (Java + C++) Aug 24, 2026
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.

1 participant