fix: cap AsYouTypeFormatter growth and guard PhoneNumberUtil against null - #402
Conversation
…null AsYouTypeFormatter.InputDigit re-derives its return value from the full accrued buffers on every call (including a regex match against the whole accumulated nationalNumber, and a ToString() of the ever-growing accruedInput on most bail-out paths), so feeding it digits with no upper bound makes per-call cost grow worse than quadratically with the number of digits typed - confirmed empirically: 500k digits took 30s+, extrapolating past two minutes. Real national significant numbers plus a parsed extension never approach 50 characters, so past that we freeze the output and every further call is O(1). Upstream Java has the identical shape (full regex match against the whole accumulated nationalNumber in attemptToFormatAccruedDigits, no length cap), so this isn't a porting bug - it's a defensive cap added here that changes nothing for any real phone number, in the same spirit as the existing extension-digit length limits. Separately, several PhoneNumberUtil entry points dereferenced a null PhoneNumber argument several frames deep instead of failing fast at the boundary, surfacing as a bare NullReferenceException instead of a typed one: IsViablePhoneNumber, Format, IsValidNumber, IsPossibleNumber, GetRegionCodeForNumber (which GetNumberType calls first), and IsNumberMatch now throw ArgumentNullException instead.
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (30.00%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #402 +/- ##
==========================================
- Coverage 77.26% 77.05% -0.21%
==========================================
Files 39 39
Lines 4548 4568 +20
Branches 1129 1138 +9
==========================================
+ Hits 3514 3520 +6
- Misses 783 790 +7
- Partials 251 258 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
📊 Benchmark Results
PR branch
PR base
|
- Raise the cap from an arbitrary 50 characters to PhoneNumberUtil.MAX_INPUT_STRING_LENGTH (250, made internal to share it) - the same bound Parse() itself uses to decide something isn't plausibly a phone number. 50 was uncomfortably close to realistic input (a national number plus a verbose extension and a few pause characters can already run 40+ characters), so legitimate as-you-type entries could silently freeze mid-input. 250 leaves generous headroom for any realistic entry while still bounding cost for genuinely pathological input. - cappedOutput is now properly `string?` (scoped #nullable enable/restore, since this file is nullable-oblivious on netstandard2.0) instead of a non-nullable field hidden behind null!, matching this repo's nullable-annotation convention. Added regression tests: one confirms output keeps updating (not frozen) well past the old 50-character threshold, the other confirms truly pathological input (300 characters) still gets capped at 250.
|
Fixed both review findings: raised the cap from an arbitrary 50 chars to |
Summary
AsYouTypeFormatter.InputDigitre-derives its output from the full accrued buffers on every call (including a regex match against the whole accumulatednationalNumber, plus aToString()of the ever-growingaccruedInputon most bail-out paths). With no cap on input length, per-call cost grows worse than quadratically with digits typed — measured 500k digits taking 30s+, extrapolating past two minutes. Real national significant numbers plus a parsed extension never approach 50 characters, so past that the formatter now freezes its output and every further call is O(1).matches()against the whole accumulatednationalNumberinattemptToFormatAccruedDigits, no length cap), so this isn't a porting bug — it's a defensive cap added here, in the same spirit as the existing extension-digit length limits, that changes nothing for any real phone number.PhoneNumberUtilentry points dereferenced a nullPhoneNumberargument several frames deep instead of failing fast at the boundary, surfacing a bareNullReferenceExceptioninstead of a typed one.IsViablePhoneNumber,Format,IsValidNumber,IsPossibleNumber,GetRegionCodeForNumber(whichGetNumberTypecalls first), andIsNumberMatchnow throwArgumentNullException.Test plan
dotnet build csharp— clean, 0 warnings/errors, all TFMs (netstandard2.0/net8.0/net10.0)dotnet test csharp/PhoneNumbers.slnx— full matrix, 432/432 passing (net8.0 + net10.0)(415) 666-7777,+44 117 496 0123, etc.)ArgumentNullExceptionwith the correctParamName