ICU: backport ICU-23110 (percent formatRange scaled twice on approximately path) - #374
ICU: backport ICU-23110 (percent formatRange scaled twice on approximately path)#374robobun wants to merge 2 commits into
Conversation
…ately path)
NumberRangeFormatterImpl::formatApproximately re-runs preProcess on
data.quantity1 after the caller already preProcessed it, so any scale
multiplier (scale/100 for style:"percent") is applied twice on the
identity/approximately path. Intl.NumberFormat({style:"percent"})
.formatRange(0.5, 0.5) returns "~5,000%" instead of "~50%".
Backports unicode-org/icu@757be359 (ICU-23110, fixed in ICU 78.1) as a
build-time patch applied in every ICU-building Dockerfile. macOS is
unaffected as it links Apple's system ICU, which already renders
correctly.
Verified the patch applies to both 75.1 (linux/musl/android/freebsd)
and 73.2 (windows), and that a patched 75.1 build produces "~50%".
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
Comment |
ubuntu:24.04 does not ship patch and these images previously had no patch step; the new numrange patch needs it (same as Dockerfile.windows already does for the udata hook).
There was a problem hiding this comment.
LGTM — mechanical backport of an upstream ICU fix, applied via the same patch -p1 pattern already used for udata-decompress-hook.patch. The inline note about build-icu.ps1 is a pre-existing gap on a debug-only artifact and not blocking.
Also checked:
- android/freebsd correctly add the
patchpackage andCOPYonly the single patch file (those images don't use the decompress hook); patch is applied to the target build, host build is tools-only so leaving it unpatched is fine. - Dropping
resetExponent()is correct sincequantityBackupis copied before the firstpreProcessand was never exponent-adjusted. patch -p1runs from the extracted ICU root and the hunk paths aresource/i18n/..., matching all five images.
Extended reasoning...
Overview
Adds icu/icu-23110-numrange-percent.patch (backport of unicode-org/icu@757be359, fixed upstream in ICU 78.1) and applies it in the five Dockerfiles that build ICU from source: Dockerfile, Dockerfile.musl, Dockerfile.windows (one new line each, next to the existing udata-decompress-hook.patch step), and Dockerfile.android / Dockerfile.freebsd (first ICU patch for those images — adds patch to apt, a targeted COPY of the one patch file, and the patch -p1 invocation between tar extraction and cd source). The patch itself takes a copy of data.quantity1 before formatterImpl1.preProcess and hands that unscaled copy to formatApproximately, so the scale/100 multiplier is applied once instead of twice on the identity/approximately path.
Security risks
None. This is a build-time source patch to a vendored dependency; no new network fetches, no credential handling, no auth/crypto surface. The ICU tarball checksums and download URLs are unchanged.
Level of scrutiny
Low–moderate. The change is build-infrastructure only and follows an established pattern in this repo (udata-decompress-hook.patch is applied the same way in the same places). The C++ change is a verbatim upstream backport with a linked ICU ticket, upstream commit hash, and an explicit drop condition (ICU >= 78) in the patch header. If the patch failed to apply on either 75.1 or 73.2 the Docker build would fail loudly, and the PR description reports it was verified against both.
Other factors
- The one finding (native
build-icu.ps1path unpatched) is a pre-existing infrastructure gap that also skips the other ICU patch, affects onlybun-webkit-windows-arm64-debug, and is correctly scoped as a follow-up nit rather than a defect introduced here. Dockerfile.macosis intentionally untouched (links Apple's systemlibicucore, already correct per the description).- Android/FreeBSD host ICU builds are left unpatched, which is fine — they only produce
icupkg/pkgdatatools; the target-archlibicuinthat ships is patched. - Test coverage is stated to land in oven-sh/bun with the
WEBKIT_VERSIONbump, which is the normal flow for this repo.
| patch -p1 -d /icu-src < /icu-bun/udata-decompress-hook.patch && \ | ||
| patch -p1 -d /icu-src < /icu-bun/icu-23110-numrange-percent.patch |
There was a problem hiding this comment.
🟡 Note that the native Windows build path (windows-release.ps1 → build-icu.ps1) — still used by CI for the bun-webkit-windows-arm64-debug artifact — downloads icu4c-73_2-src.tgz and builds via MSBuild without applying any .patch to the ICU C++ source, so that artifact will still show formatRange(0.5, 0.5) → "~5,000%". This is a pre-existing gap (udata-decompress-hook.patch is also skipped there) and only affects a debug-only artifact, so probably fine as a follow-up rather than blocking this PR.
Extended reasoning...
What this is
The PR applies icu/icu-23110-numrange-percent.patch in every Dockerfile that builds ICU from source — Dockerfile, Dockerfile.musl, Dockerfile.android, Dockerfile.freebsd, and Dockerfile.windows — exactly as the description says. However, there is one more ICU-from-source build path in the repo that CI still exercises: the native Windows runner job, which goes windows-release.ps1 → build-icu.ps1. That path is not touched by this PR, so the artifact it produces will still contain unpatched ICU 73.2 with the double-scaling bug.
Code path
In .github/workflows/build-reusable.yml:
- Lines 151–153 note that most Windows variants moved to
Dockerfile.windows, but the nativewindowsjob is kept for arm64-debug, andwindows-release.ps1"remains for building on a real Windows machine". - Lines 247–254: the
windowsjob matrix has a single entry —runner: windows-11-arm,build-type: Debug,label: bun-webkit-windows-arm64-debug. - Line 298: that job runs
./windows-release.ps1 -Platform ARM64. - Lines 712/806/894: the resulting
bun-webkit-windows-arm64-debugartifact is downloaded and published to theautobuild-{sha}release alongside all the Docker-built artifacts.
In build-icu.ps1:
- Line 32: downloads
icu4c-73_2-src.tgzfrom the upstream release. - Line 85 onward: the only "patching" is text replacement on
.vcxproj/.props/makedata.makbuild-config files. No.patchfromicu/is ever applied tosource/i18n/numrange_impl.cppornumrange_impl.h.
So sicuin.lib in bun-webkit-windows-arm64-debug is compiled from unmodified ICU 73.2 numrange_impl.cpp.
Why nothing existing prevents it
Dockerfile.windows (lines 209–210) does apply the patch, but that file only produces the cross-compiled x64 / arm64 release/LTO/asan artifacts. The native windows job doesn't use Docker at all — it invokes build-icu.ps1 directly on the runner, and that script has never had a step that reads icu/*.patch.
Step-by-step proof
- CI runs the
windowsjob onwindows-11-arm→./windows-release.ps1 -Platform ARM64. windows-release.ps1callsbuild-icu.ps1, which fetchesicu4c-73_2-src.tgzand extracts it.build-icu.ps1edits vcxproj/props for/MTstatic, then runs MSBuild.source/i18n/numrange_impl.cppis untouched, so it still containsdata.quantity1.resetExponent(); fApproximatelyFormatter.preProcess(data.quantity1, ...)on the approximately path.- The resulting
sicuin.libis packed intobun-webkit-windows-arm64-debug.tar.gzand published. - A Bun built against that artifact evaluates
new Intl.NumberFormat("en", {style:"percent"}).formatRange(0.5, 0.5): JSC callsunumrf_formatDoubleRange(0.5, 0.5), ICU'sformatterImpl1.preProcessscales 0.5 → 50, the identity check picksUNUM_IDENTITY_FALLBACK_APPROXIMATELY,formatApproximatelycallsfApproximatelyFormatter.preProcesson the same already-scaled quantity → 5000, and the output is"~5,000%"instead of"~50%".
Impact
Low. Only the bun-webkit-windows-arm64-debug artifact is affected — a Debug build, not something end users install. All release/LTO Windows artifacts come from Dockerfile.windows and are correctly patched. Also, this is a pre-existing infrastructure gap: build-icu.ps1 already skips udata-decompress-hook.patch, so "ICU source patches are not applied on the native Windows path" predates this PR rather than being introduced by it. The PR description scoped itself to "every Dockerfile that builds ICU from source", and it delivers exactly that.
Fix
As a follow-up (not blocking): have build-icu.ps1 apply icu/*.patch after extraction — e.g. git apply (git is on the runner), or patch.exe from Git for Windows, or a targeted PowerShell -replace on numrange_impl.cpp/.h. Alternatively, note the limitation in the PR / patch header so the arm64-debug divergence is documented.
Preview Builds
|
Problem
With
style: "percent",Intl.NumberFormat.prototype.formatRange(andformatRangeToParts) applies thescale/100multiplier twice whenever the range collapses to a single approximate value (equal endpoints, or endpoints that round to the same formatted number):Reproduces in every locale and through every input path (double, BigInt, string). Distinct ranges,
style: "unit"/"currency"/"decimal"collapses, and macOS are all unaffected.Cause
ICU-23110.
NumberRangeFormatterImpl::formatcallsformatterImpl1.preProcess(data.quantity1, ...)(which applies the skeleton'sscale/100), then on theUNUM_IDENTITY_FALLBACK_APPROXIMATELYpath hands the already-scaleddata.quantity1toformatApproximately, which callsfApproximatelyFormatter.preProcess(data.quantity1, ...)again. TheresetExponent()call in between only clears compact/scientific exponent state, not the multiplier, so 0.5 → 50 → 5000.JSC builds the range formatter from the same skeleton as
format()(" percent scale/100") and passes raw inputs tounumrf_formatDoubleRange/unumrf_formatDecimalRange, which is also what V8 does; Node is correct only because it ships ICU 78.Fix
Backport unicode-org/icu@757be359 (first released in ICU 78.1) as a build-time patch and apply it in every Dockerfile that builds ICU from source:
Dockerfile/Dockerfile.musl/Dockerfile.windows: applied alongside the existingudata-decompress-hook.patch.Dockerfile.android/Dockerfile.freebsd: first ICU patch for these images; added aCOPYfor the patch file and applied it in the target build after extraction.Dockerfile.macoslinks Apple'slibicucoreand is already correct, so no change there.The patch takes a copy of
data.quantity1before the firstpreProcessand passes that copy toformatApproximately, so the approximately formatter starts from the original quantity.Verification
unumrf_formatDoubleRangedirectly:Test coverage lands in oven-sh/bun alongside the
WEBKIT_VERSIONbump.