From eaeb5476a97e0146d344a4b83e27d5835d39a04f Mon Sep 17 00:00:00 2001 From: Nikolas Zimmermann Date: Thu, 14 May 2026 00:53:23 -0700 Subject: [PATCH 001/424] [GTK][WPE] Update pinned wkdev-sdk container version https://bugs.webkit.org/show_bug.cgi?id=314797 Reviewed by Claudio Saavedra. Bump required wkdev-sdk version. * .wkdev-sdk-version: Canonical link: https://commits.webkit.org/313227@main --- .wkdev-sdk-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.wkdev-sdk-version b/.wkdev-sdk-version index b5c719b9b4e7..c686e6a1de01 100644 --- a/.wkdev-sdk-version +++ b/.wkdev-sdk-version @@ -1 +1 @@ -2.53-v2-2352e15 +2.53-v3-bf0ef5d From 6bebe6dfcd60d2cc90898ef049e4e9ba8fcc8916 Mon Sep 17 00:00:00 2001 From: Nikolas Zimmermann Date: Thu, 14 May 2026 01:14:08 -0700 Subject: [PATCH 002/424] [GTK][WPE] Always warn on wkdev-sdk container version mismatch, regardless of auto-enter https://bugs.webkit.org/show_bug.cgi?id=314800 Reviewed by Claudio Saavedra. The version-mismatch warning in maybe_enter_webkit_container_sdk() previously fired only when WEBKIT_CONTAINER_SDK_ENABLE_AUTOENTER=1, and never at all from Perl wrappers (build-webkit, run-javascriptcore-tests, ...) because their fast- path mirror in webkitdirs.pm short-circuited when WEBKIT_CONTAINER_SDK==1. As a result, a developer running an outdated wkdev-sdk container without auto-enter got no signal that their container had drifted from .wkdev-sdk-version. Reorder the Python helper so the in-container version-mismatch check runs before the auto-enter opt-in gate (only host-side auto-enter remains gated on WEBKIT_CONTAINER_SDK_ENABLE_AUTOENTER and WEBKIT_CROSS_TARGET). Update the Perl mirror so it invokes the helper when inside the container regardless of opt-in, keeping the host-side opt-out as the only fast-path skip. Canonical link: https://commits.webkit.org/313228@main --- Tools/Scripts/webkitdirs.pm | 7 +++-- .../port/linux_container_sdk_utils.py | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Tools/Scripts/webkitdirs.pm b/Tools/Scripts/webkitdirs.pm index 1949b8eb5869..a7e7b00810f1 100755 --- a/Tools/Scripts/webkitdirs.pm +++ b/Tools/Scripts/webkitdirs.pm @@ -2564,13 +2564,16 @@ sub maybeEnterWebKitContainerSDK() # Mirror the cheap opt-out checks from maybe_enter_webkit_container_sdk so # that the overwhelmingly common no-op case avoids forking a Python # interpreter on every wrapper invocation. Keep in sync with that function. - return if ($ENV{'WEBKIT_CONTAINER_SDK_ENABLE_AUTOENTER'} // '') ne '1'; - return if ($ENV{'WEBKIT_CONTAINER_SDK'} // '') eq '1'; return if ($ENV{'WEBKIT_FLATPAK'} // '') eq '1'; return if ($ENV{'WEBKIT_JHBUILD'} // '') eq '1'; return if ($ENV{'WEBKIT_CONTAINER_SDK_INSIDE_MOUNT_NAMESPACE'} // '') eq '1'; return unless -f File::Spec->catfile(sourceDir(), ".wkdev-sdk-version"); + # Auto-enter is opt-in on the host. Inside the container we always invoke + # the helper so the version-mismatch warning fires regardless of opt-in. + return if (($ENV{'WEBKIT_CONTAINER_SDK'} // '') ne '1' + and ($ENV{'WEBKIT_CONTAINER_SDK_ENABLE_AUTOENTER'} // '') ne '1'); + # Delegate to the Python helper. It either replaces this process with # `podman exec ...` (never returning), or exits with # $AUTOENTER_DECLINED_EXIT_CODE to signal "continue on the host". diff --git a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py index b98bec4b7d66..a48a2c5c84bf 100644 --- a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py +++ b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py @@ -390,7 +390,10 @@ def maybe_enter_webkit_container_sdk(argv=None): on first use using the version pinned in .wkdev-sdk-version. When already running inside a wkdev-sdk container, verify the running SDK - version matches the pinned one and warn loudly if not, but continue. + version matches the pinned one and warn loudly if not, but continue. The + in-container version check fires regardless of + WEBKIT_CONTAINER_SDK_ENABLE_AUTOENTER so users always learn when the + running container is out of sync with .wkdev-sdk-version. `argv` defaults to `sys.argv` when omitted; pass it explicitly when the caller is a wrapper (e.g. container-sdk-autoenter) whose own argv differs @@ -410,21 +413,15 @@ def maybe_enter_webkit_container_sdk(argv=None): if any(os.environ.get(e) == '1' for e in ('WEBKIT_FLATPAK', 'WEBKIT_JHBUILD', 'WEBKIT_CONTAINER_SDK_INSIDE_MOUNT_NAMESPACE')): return - # Auto-enter is opt-in: bots and users flip it on via the environment. - if os.environ.get('WEBKIT_CONTAINER_SDK_ENABLE_AUTOENTER') != '1': - return - - # Cross-target builds use their own toolchain wrapper (see webkitdirs.pm's - # runInCrossTargetEnvironment); don't double-wrap them in the SDK container. - if os.environ.get('WEBKIT_CROSS_TARGET'): - return - source_dir = _source_dir() pinned_version = _read_pinned_sdk_version(source_dir) if not pinned_version: return - # Inside container: version-match check (warn, continue). + # Inside container: version-match check (warn, continue). Performed + # unconditionally -- the auto-enter opt-in below only gates host-side + # behavior, since by the time we are inside the container the user has + # already chosen the SDK they want to use. if os.environ.get('WEBKIT_CONTAINER_SDK') == '1': running_version = _read_running_sdk_version() if running_version and running_version != pinned_version: @@ -438,6 +435,15 @@ def maybe_enter_webkit_container_sdk(argv=None): ]) return + # Auto-enter is opt-in: bots and users flip it on via the environment. + if os.environ.get('WEBKIT_CONTAINER_SDK_ENABLE_AUTOENTER') != '1': + return + + # Cross-target builds use their own toolchain wrapper (see webkitdirs.pm's + # runInCrossTargetEnvironment); don't double-wrap them in the SDK container. + if os.environ.get('WEBKIT_CROSS_TARGET'): + return + # Host side: podman is the only prerequisite. if not shutil.which('podman'): _print_prominent_warning([ From 24feebda7c8a93ad410c32f9468b802020043f34 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 14 May 2026 01:33:12 -0700 Subject: [PATCH 003/424] Introduce HyperlinkElementUtils IDL mixin https://bugs.webkit.org/show_bug.cgi?id=314727 Reviewed by Anne van Kesteren. Adds the HyperlinkElementUtils mixin and move most members from HTMLHyperlinkElementUtils into it. Also move target from HTMLAnchorElement and HTMLAreaElement into HTMLHyperlinkElementUtils per recent spec change. This patch does NOT include the new mixin in SVGAElement. No behaviour changes so no test expectations have changed. * Source/WebCore/CMakeLists.txt: * Source/WebCore/DerivedSources-input.xcfilelist: * Source/WebCore/DerivedSources-output.xcfilelist: * Source/WebCore/DerivedSources.make: * Source/WebCore/Sources.txt: * Source/WebCore/WebCore.xcodeproj/project.pbxproj: * Source/WebCore/html/HTMLAnchorElement.idl: * Source/WebCore/html/HTMLAreaElement.idl: * Source/WebCore/html/HTMLHyperlinkElementUtils.idl: * Source/WebCore/html/HyperlinkElementUtils.idl: Copied from Source/WebCore/html/HTMLHyperlinkElementUtils.idl. Canonical link: https://commits.webkit.org/313229@main --- Source/WebCore/CMakeLists.txt | 1 + .../WebCore/DerivedSources-input.xcfilelist | 1 + .../WebCore/DerivedSources-output.xcfilelist | 2 + Source/WebCore/DerivedSources.make | 1 + Source/WebCore/Sources.txt | 1 + .../WebCore/WebCore.xcodeproj/project.pbxproj | 2 + Source/WebCore/html/HTMLAnchorElement.idl | 2 +- Source/WebCore/html/HTMLAreaElement.idl | 2 +- .../html/HTMLHyperlinkElementUtils.idl | 11 +----- Source/WebCore/html/HyperlinkElementUtils.idl | 38 +++++++++++++++++++ 10 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 Source/WebCore/html/HyperlinkElementUtils.idl diff --git a/Source/WebCore/CMakeLists.txt b/Source/WebCore/CMakeLists.txt index 27a591e4530b..374a9fa3f945 100644 --- a/Source/WebCore/CMakeLists.txt +++ b/Source/WebCore/CMakeLists.txt @@ -2081,6 +2081,7 @@ set(WebCore_SUPPLEMENTAL_IDL_FILES html/HTMLOrSVGOrMathMLElement.idl html/HTMLVideoElement+CaptionDisplaySettings.idl html/HTMLVideoElement+RequestVideoFrameCallback.idl + html/HyperlinkElementUtils.idl html/PopoverInvokerElement.idl html/canvas/CanvasCompositing.idl diff --git a/Source/WebCore/DerivedSources-input.xcfilelist b/Source/WebCore/DerivedSources-input.xcfilelist index 92b11803befb..2b14c5afe918 100644 --- a/Source/WebCore/DerivedSources-input.xcfilelist +++ b/Source/WebCore/DerivedSources-input.xcfilelist @@ -1719,6 +1719,7 @@ $(PROJECT_DIR)/html/HTMLUnknownElement.idl $(PROJECT_DIR)/html/HTMLVideoElement+CaptionDisplaySettings.idl $(PROJECT_DIR)/html/HTMLVideoElement+RequestVideoFrameCallback.idl $(PROJECT_DIR)/html/HTMLVideoElement.idl +$(PROJECT_DIR)/html/HyperlinkElementUtils.idl $(PROJECT_DIR)/html/ImageBitmap.idl $(PROJECT_DIR)/html/ImageBitmapOptions.idl $(PROJECT_DIR)/html/ImageData.idl diff --git a/Source/WebCore/DerivedSources-output.xcfilelist b/Source/WebCore/DerivedSources-output.xcfilelist index c3e70308be01..32166c01615e 100644 --- a/Source/WebCore/DerivedSources-output.xcfilelist +++ b/Source/WebCore/DerivedSources-output.xcfilelist @@ -1716,6 +1716,8 @@ $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSHkdfParams.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSHkdfParams.h $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSHmacKeyParams.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSHmacKeyParams.h +$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSHyperlinkElementUtils.cpp +$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSHyperlinkElementUtils.h $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSIDBCursor.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSIDBCursor.h $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSIDBCursorDirection.cpp diff --git a/Source/WebCore/DerivedSources.make b/Source/WebCore/DerivedSources.make index b1eab817d0d7..f5c727aab58a 100644 --- a/Source/WebCore/DerivedSources.make +++ b/Source/WebCore/DerivedSources.make @@ -1386,6 +1386,7 @@ JS_BINDING_IDLS := \ $(WebCore)/html/HTMLVideoElement.idl \ $(WebCore)/html/HTMLVideoElement+CaptionDisplaySettings.idl \ $(WebCore)/html/HTMLVideoElement+RequestVideoFrameCallback.idl \ + $(WebCore)/html/HyperlinkElementUtils.idl \ $(WebCore)/html/ImageBitmap.idl \ $(WebCore)/html/ImageBitmapOptions.idl \ $(WebCore)/html/ImageData.idl \ diff --git a/Source/WebCore/Sources.txt b/Source/WebCore/Sources.txt index 610787db79db..af250eb2390e 100644 --- a/Source/WebCore/Sources.txt +++ b/Source/WebCore/Sources.txt @@ -4459,6 +4459,7 @@ JSHighlightRegistry.cpp JSHistory.cpp JSHkdfParams.cpp JSHmacKeyParams.cpp +JSHyperlinkElementUtils.cpp JSIDBCursor.cpp JSIDBCursorDirection.cpp JSIDBCursorWithValue.cpp diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj index 8af1c1d8edd4..bb1375d7909a 100644 --- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj +++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj @@ -15452,6 +15452,7 @@ 83274FCD252E26A8001E35DC /* AudioWorkletProcessorConstructionData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AudioWorkletProcessorConstructionData.h; sourceTree = ""; }; 8329A4171EC25B2B008ED4BE /* DocumentAndElementEventHandlers.idl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = DocumentAndElementEventHandlers.idl; sourceTree = ""; }; 8329DCC21C7A6AE300730B33 /* HTMLHyperlinkElementUtils.idl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = HTMLHyperlinkElementUtils.idl; sourceTree = ""; }; + 8329DCC21C7A6AE300730B34 /* HyperlinkElementUtils.idl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = HyperlinkElementUtils.idl; sourceTree = ""; }; 832B843319D8E55100B26055 /* SVGAnimateElementBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SVGAnimateElementBase.h; sourceTree = ""; }; 832B843519D8E57400B26055 /* SVGAnimateElementBase.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SVGAnimateElementBase.cpp; sourceTree = ""; }; 833AE52624D386E100017C4B /* OfflineAudioCompletionEventInit.idl */ = {isa = PBXFileReference; lastKnownFileType = text; path = OfflineAudioCompletionEventInit.idl; sourceTree = ""; }; @@ -33275,6 +33276,7 @@ CD3570EB2EC50D2B0054E843 /* HTMLVideoElementCaptionDisplaySettings.h */, 839AAFEA1A0C0C8D00605F99 /* HTMLWBRElement.cpp */, 839AAFEB1A0C0C8D00605F99 /* HTMLWBRElement.h */, + 8329DCC21C7A6AE300730B34 /* HyperlinkElementUtils.idl */, 31D26BC21F86D18C008FF255 /* ImageBitmap.cpp */, 31D26BBF1F86D189008FF255 /* ImageBitmap.h */, 31D26BC11F86D18B008FF255 /* ImageBitmap.idl */, diff --git a/Source/WebCore/html/HTMLAnchorElement.idl b/Source/WebCore/html/HTMLAnchorElement.idl index f64a2cdbdc3c..dc785df07d92 100644 --- a/Source/WebCore/html/HTMLAnchorElement.idl +++ b/Source/WebCore/html/HTMLAnchorElement.idl @@ -33,7 +33,6 @@ [CEReactions=NotNeeded, Reflect] attribute DOMString rel; [CEReactions=NotNeeded, Reflect] attribute DOMString rev; [CEReactions=NotNeeded, Reflect] attribute DOMString shape; - [CEReactions=NotNeeded, Reflect] attribute DOMString target; [CEReactions=NotNeeded, Reflect] attribute DOMString type; [CEReactions=Needed] attribute DOMString text; @@ -43,4 +42,5 @@ [CEReactions=NotNeeded, ImplementedAs=referrerPolicyForBindings, ReflectSetter] attribute [AtomString] DOMString referrerPolicy; }; +HTMLAnchorElement includes HyperlinkElementUtils; HTMLAnchorElement includes HTMLHyperlinkElementUtils; diff --git a/Source/WebCore/html/HTMLAreaElement.idl b/Source/WebCore/html/HTMLAreaElement.idl index c791dc446f98..1122b644b076 100644 --- a/Source/WebCore/html/HTMLAreaElement.idl +++ b/Source/WebCore/html/HTMLAreaElement.idl @@ -27,7 +27,6 @@ [CEReactions=NotNeeded, Reflect] attribute USVString ping; [CEReactions=NotNeeded, Reflect] attribute DOMString rel; [CEReactions=NotNeeded, Reflect] attribute DOMString shape; - [CEReactions=NotNeeded, Reflect] attribute DOMString target; [CEReactions=NotNeeded, EnabledBySetting=DownloadAttributeEnabled, Reflect] attribute DOMString download; [CEReactions=NotNeeded, ImplementedAs=referrerPolicyForBindings, ReflectSetter] attribute [AtomString] DOMString referrerPolicy; @@ -35,4 +34,5 @@ [PutForwards=value] readonly attribute DOMTokenList relList; }; +HTMLAreaElement includes HyperlinkElementUtils; HTMLAreaElement includes HTMLHyperlinkElementUtils; diff --git a/Source/WebCore/html/HTMLHyperlinkElementUtils.idl b/Source/WebCore/html/HTMLHyperlinkElementUtils.idl index 7b3b9114ab9e..11159ee064f1 100644 --- a/Source/WebCore/html/HTMLHyperlinkElementUtils.idl +++ b/Source/WebCore/html/HTMLHyperlinkElementUtils.idl @@ -26,14 +26,5 @@ // https://html.spec.whatwg.org/multipage/links.html#htmlhyperlinkelementutils interface mixin HTMLHyperlinkElementUtils { [CEReactions=NotNeeded, ReflectURL] stringifier attribute USVString href; - readonly attribute USVString origin; - [CEReactions=NotNeeded] attribute USVString protocol; - [CEReactions=NotNeeded] attribute USVString username; - [CEReactions=NotNeeded] attribute USVString password; - [CEReactions=NotNeeded] attribute USVString host; - [CEReactions=NotNeeded] attribute USVString hostname; - [CEReactions=NotNeeded] attribute USVString port; - [CEReactions=NotNeeded] attribute USVString pathname; - [CEReactions=NotNeeded] attribute USVString search; - [CEReactions=NotNeeded] attribute USVString hash; + [CEReactions=NotNeeded, Reflect] attribute DOMString target; }; diff --git a/Source/WebCore/html/HyperlinkElementUtils.idl b/Source/WebCore/html/HyperlinkElementUtils.idl new file mode 100644 index 000000000000..417592992c18 --- /dev/null +++ b/Source/WebCore/html/HyperlinkElementUtils.idl @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +// https://html.spec.whatwg.org/multipage/links.html#hyperlinkelementutils +interface mixin HyperlinkElementUtils { + readonly attribute USVString origin; + [CEReactions=NotNeeded] attribute USVString protocol; + [CEReactions=NotNeeded] attribute USVString username; + [CEReactions=NotNeeded] attribute USVString password; + [CEReactions=NotNeeded] attribute USVString host; + [CEReactions=NotNeeded] attribute USVString hostname; + [CEReactions=NotNeeded] attribute USVString port; + [CEReactions=NotNeeded] attribute USVString pathname; + [CEReactions=NotNeeded] attribute USVString search; + [CEReactions=NotNeeded] attribute USVString hash; +}; From 8f742098b444303d409e7b87ea10cf42cafaa5a7 Mon Sep 17 00:00:00 2001 From: Yusuke Suzuki Date: Thu, 14 May 2026 01:36:00 -0700 Subject: [PATCH 004/424] [JSC] Support Inlined InternalMicrotask reaction with Promise https://bugs.webkit.org/show_bug.cgi?id=314788 rdar://177040942 Reviewed by Sosuke Suzuki. 313220@main now offers yet another JSCell* space in JSPromise, thus we can just store JSPromise* as well when adding inline InternalMicrotask reaction. This patch supports it and now all InternalMicrotask reaction can be inlined when it is only one reaction in the promise. Test: JSTests/stress/dynamic-import-inline-microtask-result-promise.js * JSTests/stress/dynamic-import-inline-microtask-result-promise.js: Added. (shouldBe): (async const): (catch): (err.null.throw.new.Error): (then): * Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp: (JSC::CyclicModuleRecord::executeAsync): * Source/JavaScriptCore/runtime/JSMicrotask.cpp: (JSC::runInternalMicrotask): * Source/JavaScriptCore/runtime/JSModuleLoader.cpp: (JSC::JSModuleLoader::loadModule): (JSC::JSModuleLoader::innerModuleLoading): * Source/JavaScriptCore/runtime/JSPromise.cpp: (JSC::JSPromise::setInlineMicrotaskReaction): (JSC::JSPromise::spillInlineReaction): (JSC::JSPromise::performPromiseThenWithInternalMicrotask): (JSC::JSPromise::settleInlineInternalMicrotask): (JSC::JSPromise::resolveWithInternalMicrotaskForAsyncAwait): * Source/JavaScriptCore/runtime/JSPromise.h: * Source/JavaScriptCore/wasm/js/JSWebAssembly.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): Canonical link: https://commits.webkit.org/313230@main --- ...-import-inline-microtask-result-promise.js | 92 +++++++++++++++++++ .../runtime/CyclicModuleRecord.cpp | 2 +- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 2 +- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 4 +- Source/JavaScriptCore/runtime/JSPromise.cpp | 28 +++--- Source/JavaScriptCore/runtime/JSPromise.h | 6 +- .../JavaScriptCore/wasm/js/JSWebAssembly.cpp | 4 +- 7 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 JSTests/stress/dynamic-import-inline-microtask-result-promise.js diff --git a/JSTests/stress/dynamic-import-inline-microtask-result-promise.js b/JSTests/stress/dynamic-import-inline-microtask-result-promise.js new file mode 100644 index 000000000000..b12dbca9f899 --- /dev/null +++ b/JSTests/stress/dynamic-import-inline-microtask-result-promise.js @@ -0,0 +1,92 @@ +// Regression test for performPromiseThenWithInternalMicrotask taking the +// inline-reaction fast path when a result JSPromise* is supplied. The result +// promise is stashed in JSPromise's payloadCell and must be recovered in +// settleInlineInternalMicrotask (and spillInlineReaction) so the result +// promise observed by JS resolves/rejects with the right value. +// +// The user-visible path is JSModuleLoader::loadModule wiring up +// ModuleLoadLinkEvaluateSettled — i.e. every dynamic import() call. + +var abort = $vm.abort; + +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error("expected " + String(expected) + ", got " + String(actual)); +} + +(async function () { + { + const m = await import('./import-tests/cocoa.js'); + shouldBe(m.hello(), 42); + shouldBe(typeof m.Cocoa, 'function'); + } + + // Re-import: a fresh resultPromise is allocated and routed through the + // inline path on every call, even when the underlying module is cached. + { + const a = await import('./import-tests/cocoa.js'); + const b = await import('./import-tests/cocoa.js'); + const c = await import('./import-tests/cocoa.js'); + shouldBe(a, b); + shouldBe(b, c); + } + + // Many concurrent imports of the same module: each gets its own pending + // intermediate promise + inline reaction. If the inline payloadCell were + // aliased or overwritten across imports, some awaits would see undefined + // or the wrong namespace. + { + const promises = []; + for (let i = 0; i < 64; ++i) + promises.push(import('./import-tests/cocoa.js')); + const results = await Promise.all(promises); + for (const r of results) + shouldBe(r.hello(), 42); + } + + // Concurrent imports of distinct modules — the per-import resultPromise + // stashed in payloadCell must not leak across imports. + { + const [cocoa, should] = await Promise.all([ + import('./import-tests/cocoa.js'), + import('./import-tests/should.js'), + ]); + shouldBe(cocoa.hello(), 42); + shouldBe(typeof should.shouldBe, 'function'); + } + + // Nested dynamic import (import() invoked from inside an imported module) + // — exercises the inline path on a different referrer. + { + const multiple = await import('./import-tests/multiple.js'); + const inner = await multiple.result(); + shouldBe(typeof inner, 'object'); + } + + // Failure path: the resultPromise must reject with the load error rather + // than resolve with undefined. + { + let err = null; + try { + await import('./this-module-does-not-exist.js'); + } catch (e) { + err = e; + } + if (err === null) + throw new Error('missing module did not reject'); + } + + // Interleave: kick off a failing import and a successful import together; + // both resultPromises sit in their own payloadCell slot and must settle + // independently with the right outcome. + { + const ok = import('./import-tests/cocoa.js'); + const bad = import('./this-module-does-not-exist.js').then( + () => { throw new Error('bad import resolved'); }, + e => 'rejected' + ); + const [okValue, badValue] = await Promise.all([ok, bad]); + shouldBe(okValue.hello(), 42); + shouldBe(badValue, 'rejected'); + } +}()).then(() => {}, abort); diff --git a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp index dde7207c85cd..a9ff471a33a9 100644 --- a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp @@ -490,7 +490,7 @@ void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject) // 7. Let onRejected be CreateBuiltinFunction(rejectedClosure, 0, "", « »). // Also handled in JSMicrotask.cpp. // 8. Perform PerformPromiseThen(capability.[[Promise]], onFulfilled, onRejected). - promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::AsyncModuleExecutionDone, jsUndefined(), this); + promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this); // 9. Perform ! module.ExecuteModule(capability). execute(globalObject, promise); RETURN_IF_EXCEPTION(scope, void()); diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 5518bd7c4d19..362b48b3f78a 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -1441,7 +1441,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]] RELEASE_AND_RETURN(scope, promiseResolveThenableJobWithInternalMicrotaskFastSlow(globalObject, promise, task, context)); - promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, jsUndefined(), context); + promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, nullptr, context); return; } diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 8d1b501bd98e..cb9fe5973da9 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -720,7 +720,7 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Module resultPromise->markAsHandled(); promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::ModuleLoadLinkEvaluateSettled, resultPromise, context); - resultPromise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::ModuleLoadStoreError, jsUndefined(), context); + resultPromise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::ModuleLoadStoreError, nullptr, context); return resultPromise; } @@ -778,7 +778,7 @@ void JSModuleLoader::innerModuleLoading(JSGlobalObject* globalObject, ModuleGrap ASSERT(module->loadedModules().size() <= loadedModulesCountBefore + 1); ASSERT(needsErrorReaction != module->loadedModules().contains(ModuleMapKey { request.m_specifier.impl(), request.type() })); if (needsErrorReaction) - promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::ModuleGraphLoadingError, jsUndefined(), state); + promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::ModuleGraphLoadingError, nullptr, state); } // 2.d.iv. If state.[[IsLoading]] is false, return UNUSED. if (!state->isLoading()) diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index fd069ce5f7aa..55b3662861c6 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -237,7 +237,7 @@ JSPromise* JSPromise::rejectWithCaughtException(JSGlobalObject* globalObject, Th return this; } -void JSPromise::setInlineMicrotaskReaction(VM& vm, InternalMicrotask task, JSValue context) +void JSPromise::setInlineMicrotaskReaction(VM& vm, InternalMicrotask task, JSPromise* promise, JSValue context) { ASSERT(status() == Status::Pending); ASSERT(inlineReactionKind() == InlineReactionKind::None); @@ -249,7 +249,7 @@ void JSPromise::setInlineMicrotaskReaction(VM& vm, InternalMicrotask task, JSVal | (static_cast(InlineReactionKind::InternalMicrotask) << inlineReactionKindShift) | (static_cast(task) << inlineReactionMicrotaskShift); setSlot(vm, context); - setFlags(newFlags); + setPackedCell(vm, newFlags, promise); } void JSPromise::setInlineHandlerReaction(VM& vm, InlineReactionKind kind, JSPromise* resultPromise, JSValue handler) @@ -275,7 +275,8 @@ JSPromiseReaction* JSPromise::spillInlineReaction(VM& vm) case InlineReactionKind::InternalMicrotask: { InternalMicrotask task = inlineReactionMicrotask(); JSValue context = m_slot.get(); - reaction = JSSlimPromiseReaction::create(vm, jsUndefined(), task, context, nullptr); + auto* promise = uncheckedDowncast(payloadCell()); + reaction = JSSlimPromiseReaction::create(vm, promise ? JSValue(promise) : jsUndefined(), task, context, nullptr); break; } case InlineReactionKind::FulfillHandler: @@ -373,16 +374,17 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue } } -void JSPromise::performPromiseThenWithInternalMicrotask(VM& vm, JSGlobalObject* globalObject, InternalMicrotask task, JSValue promise, JSValue context) +void JSPromise::performPromiseThenWithInternalMicrotask(VM& vm, JSGlobalObject* globalObject, InternalMicrotask task, JSPromise* promise, JSValue context) { + JSValue promiseValue = promise ? JSValue(promise) : jsUndefined(); switch (status()) { case JSPromise::Status::Pending: { - if (promise.isUndefined() && inlineReactionKind() == InlineReactionKind::None && !payloadCell()) [[likely]] { - setInlineMicrotaskReaction(vm, task, context); + if (inlineReactionKind() == InlineReactionKind::None && !payloadCell()) [[likely]] { + setInlineMicrotaskReaction(vm, task, promise, context); break; } JSPromiseReaction* existing = reactionHead(vm); - auto* reaction = JSSlimPromiseReaction::create(vm, promise, task, context, existing); + auto* reaction = JSSlimPromiseReaction::create(vm, promiseValue, task, context, existing); setPackedCell(vm, flags() | isHandledFlag, reaction); break; } @@ -390,13 +392,13 @@ void JSPromise::performPromiseThenWithInternalMicrotask(VM& vm, JSGlobalObject* JSValue settled = settlementValue(); if (!isHandled()) globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, this, JSPromiseRejectionOperation::Handle); - globalObject->queueMicrotask(vm, task, static_cast(Status::Rejected), promise, settled, context); + globalObject->queueMicrotask(vm, task, static_cast(Status::Rejected), promiseValue, settled, context); markAsHandled(); break; } case JSPromise::Status::Fulfilled: { JSValue settled = settlementValue(); - globalObject->queueMicrotask(vm, task, static_cast(Status::Fulfilled), promise, settled, context); + globalObject->queueMicrotask(vm, task, static_cast(Status::Fulfilled), promiseValue, settled, context); break; } } @@ -431,10 +433,12 @@ ALWAYS_INLINE void JSPromise::settleInlineInternalMicrotask(VM& vm, JSGlobalObje ASSERT(flagsSnapshot & isHandledFlag); InternalMicrotask task = static_cast((flagsSnapshot & inlineReactionMicrotaskMask) >> inlineReactionMicrotaskShift); JSValue context = m_slot.get(); + auto* promise = uncheckedDowncast(payloadCell()); + JSValue promiseValue = promise ? JSValue(promise) : jsUndefined(); uint16_t settledFlags = (flagsSnapshot & ~(inlineReactionKindMask | inlineReactionMicrotaskMask)) | static_cast(newStatus); setSlot(vm, argument); setPackedCell(vm, settledFlags, nullptr); - globalObject->queueMicrotask(vm, task, static_cast(newStatus), jsUndefined(), argument, context); + globalObject->queueMicrotask(vm, task, static_cast(newStatus), promiseValue, argument, context); } ALWAYS_INLINE void JSPromise::settleInlineHandler(VM& vm, JSGlobalObject* globalObject, Status newStatus, JSValue argument, uint16_t flagsSnapshot) @@ -783,7 +787,7 @@ void JSPromise::resolveWithInternalMicrotaskForAsyncAwait(JSGlobalObject* global if (resolution.inherits()) { auto* promise = uncheckedDowncast(resolution); if (promiseSpeciesWatchpointIsValid(vm, promise)) [[likely]] - return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, jsUndefined(), context); + return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, nullptr, context); JSValue constructor; JSValue error; @@ -807,7 +811,7 @@ void JSPromise::resolveWithInternalMicrotaskForAsyncAwait(JSGlobalObject* global } if (constructor == globalObject->promiseConstructor()) - return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, jsUndefined(), context); + return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, nullptr, context); } resolveWithInternalMicrotask(globalObject, vm, resolution, task, context); diff --git a/Source/JavaScriptCore/runtime/JSPromise.h b/Source/JavaScriptCore/runtime/JSPromise.h index 6d37be64c5ff..60dcdac7c099 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.h +++ b/Source/JavaScriptCore/runtime/JSPromise.h @@ -155,7 +155,7 @@ class JSPromise : public JSNonFinalObject { static void rejectWithInternalMicrotask(VM&, JSGlobalObject*, JSValue argument, InternalMicrotask, JSValue context); static void fulfillWithInternalMicrotask(VM&, JSGlobalObject*, JSValue argument, InternalMicrotask, JSValue context); - void performPromiseThenWithInternalMicrotask(VM&, JSGlobalObject*, InternalMicrotask, JSValue promise, JSValue context); + void performPromiseThenWithInternalMicrotask(VM&, JSGlobalObject*, InternalMicrotask, JSPromise*, JSValue context); bool isThenFastAndNonObservable(); @@ -216,8 +216,8 @@ class JSPromise : public JSNonFinalObject { void setSlot(VM& vm, JSValue value) { m_slot.set(vm, this, value); } void clearSlot() { m_slot.clear(); } - void setInlineMicrotaskReaction(VM&, InternalMicrotask, JSValue context); - void setInlineHandlerReaction(VM&, InlineReactionKind, JSPromise* resultPromise, JSValue handler); + void setInlineMicrotaskReaction(VM&, InternalMicrotask, JSPromise*, JSValue context); + void setInlineHandlerReaction(VM&, InlineReactionKind, JSPromise*, JSValue handler); JSPromiseReaction* spillInlineReaction(VM&); JSPromiseReaction* reactionHead(VM&); void settleInlineInternalMicrotask(VM&, JSGlobalObject*, Status, JSValue argument, uint16_t flags); diff --git a/Source/JavaScriptCore/wasm/js/JSWebAssembly.cpp b/Source/JavaScriptCore/wasm/js/JSWebAssembly.cpp index 2b72221be0d4..0e9453e06fc0 100644 --- a/Source/JavaScriptCore/wasm/js/JSWebAssembly.cpp +++ b/Source/JavaScriptCore/wasm/js/JSWebAssembly.cpp @@ -453,7 +453,7 @@ JSC_DEFINE_HOST_FUNCTION(webAssemblyCompileStreamingFunc, (JSGlobalObject* globa JSPromise* sourcePromise = JSPromise::resolvedPromise(globalObject, callFrame->argument(0)); RETURN_IF_EXCEPTION(scope, JSValue::encode(promise->rejectWithCaughtException(globalObject, scope))); - sourcePromise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::WebAssemblyCompileStreaming, jsUndefined(), context); + sourcePromise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::WebAssemblyCompileStreaming, nullptr, context); return JSValue::encode(promise); } @@ -497,7 +497,7 @@ JSC_DEFINE_HOST_FUNCTION(webAssemblyInstantiateStreamingFunc, (JSGlobalObject* g JSPromise* sourcePromise = JSPromise::resolvedPromise(globalObject, callFrame->argument(0)); RETURN_IF_EXCEPTION(scope, JSValue::encode(promise->rejectWithCaughtException(globalObject, scope))); - sourcePromise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::WebAssemblyInstantiateStreaming, jsUndefined(), context); + sourcePromise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::WebAssemblyInstantiateStreaming, nullptr, context); return JSValue::encode(promise); } From 139c75b3f9c93c5a48be0da7d9e9e9e430bdaba4 Mon Sep 17 00:00:00 2001 From: Claudio Saavedra Date: Thu, 14 May 2026 01:45:37 -0700 Subject: [PATCH 005/424] [GLIB] Non-unified build fixes for May 14th, 2026 https://bugs.webkit.org/show_bug.cgi?id=314803 Unreviewed build fix. * Source/WebCore/rendering/RenderLayerSVGAdditions.cpp: * Source/WebKit/WebProcess/Inspector/FrameNetworkAgentProxy.cpp: Canonical link: https://commits.webkit.org/313231@main --- Source/WebCore/rendering/RenderLayerSVGAdditions.cpp | 1 + Source/WebKit/WebProcess/Inspector/FrameNetworkAgentProxy.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/WebCore/rendering/RenderLayerSVGAdditions.cpp b/Source/WebCore/rendering/RenderLayerSVGAdditions.cpp index 0002922911b7..7a417944f1a6 100644 --- a/Source/WebCore/rendering/RenderLayerSVGAdditions.cpp +++ b/Source/WebCore/rendering/RenderLayerSVGAdditions.cpp @@ -24,6 +24,7 @@ #include "HitTestResult.h" #include "ReferencedSVGResources.h" #include "RenderAncestorIterator.h" +#include "RenderBoxInlines.h" #include "RenderDescendantIterator.h" #include "RenderElementInlines.h" #include "RenderLayerBacking.h" diff --git a/Source/WebKit/WebProcess/Inspector/FrameNetworkAgentProxy.cpp b/Source/WebKit/WebProcess/Inspector/FrameNetworkAgentProxy.cpp index 59d39e740638..e6788ffec9c4 100644 --- a/Source/WebKit/WebProcess/Inspector/FrameNetworkAgentProxy.cpp +++ b/Source/WebKit/WebProcess/Inspector/FrameNetworkAgentProxy.cpp @@ -42,7 +42,7 @@ #include #include #include -#include +#include #include #include #include From e67bead77ea13a5d79908a5cc086a98ab1cef94a Mon Sep 17 00:00:00 2001 From: Nikolas Zimmermann Date: Thu, 14 May 2026 02:21:15 -0700 Subject: [PATCH 006/424] [GTK][WPE] Drop --pid host from wkdev-build container creation https://bugs.webkit.org/show_bug.cgi?id=314805 Unreviewed build fix The auto-enter helper was passing `--pid host` to `podman create`, which crun rejects on some host configurations with: Error: containers not creating Cgroups must create a private PID namespace: invalid argument ERROR: Failed to create WebKit Container SDK container named 'wkdev-build'. Remove --pid host, we don't need it for the wkdev-build container (unlike the full SDK container, for convenience). * Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py: (_build_podman_create_args): Canonical link: https://commits.webkit.org/313232@main --- Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py index a48a2c5c84bf..fb1d41d1958a 100644 --- a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py +++ b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py @@ -204,7 +204,11 @@ def _build_podman_create_args(pinned_version): '--ulimit', 'host', '--pids-limit', '-1', '--tmpfs', '/tmp', - '--pid', 'host', + # No --pid host: crun rejects sharing the host PID namespace when the + # container is not creating its own cgroup ("containers not creating + # Cgroups must create a private PID namespace"). The build container + # runs `sleep infinity` as its own PID 1 with no systemd inside, so a + # private PID namespace is fine. '--ipc', 'host', '--network', 'host', '--pull=newer', From c21ff09bed7d0d4af2604a49abf3570a772a70dc Mon Sep 17 00:00:00 2001 From: Abrar Rahman Protyasha Date: Thu, 14 May 2026 03:24:21 -0700 Subject: [PATCH 007/424] REGRESSION(313207@main): Broke WebCore module verification (double-quoted include CommandLineAPIHost.h and InstrumentingAgents.h) https://bugs.webkit.org/show_bug.cgi?id=314807 rdar://177055939 Unreviewed build fix. We simply change these private header includes to angle brackets, thus addressing the following build errors: ``` /Release-iphonesimulator/WebCore.framework/PrivateHeaders/CommandLineAPIHost.h:32:10: error: double-quoted include "InstrumentingAgents.h" in framework header, expected angle-bracketed instead [-Werror,-Wquoted-include-in-framework-header] Release-iphonesimulator/WebCore.framework/PrivateHeaders/WebInjectedScriptManager.h:28:10: error: double-quoted include "CommandLineAPIHost.h" in framework header, expected angle-bracketed instead [-Werror,-Wquoted-include-in-framework-header] ``` * Source/WebCore/inspector/CommandLineAPIHost.h: * Source/WebCore/inspector/WebInjectedScriptManager.h: Canonical link: https://commits.webkit.org/313233@main --- Source/WebCore/inspector/CommandLineAPIHost.h | 2 +- Source/WebCore/inspector/WebInjectedScriptManager.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/WebCore/inspector/CommandLineAPIHost.h b/Source/WebCore/inspector/CommandLineAPIHost.h index cf0c9ecaffc6..4929b0d43ac8 100644 --- a/Source/WebCore/inspector/CommandLineAPIHost.h +++ b/Source/WebCore/inspector/CommandLineAPIHost.h @@ -29,8 +29,8 @@ #pragma once -#include "InstrumentingAgents.h" #include +#include #include #include #include diff --git a/Source/WebCore/inspector/WebInjectedScriptManager.h b/Source/WebCore/inspector/WebInjectedScriptManager.h index e4d75e31d3a4..14919487344f 100644 --- a/Source/WebCore/inspector/WebInjectedScriptManager.h +++ b/Source/WebCore/inspector/WebInjectedScriptManager.h @@ -25,8 +25,8 @@ #pragma once -#include "CommandLineAPIHost.h" #include +#include #include #include #include From ff2f768ad68a9b40d3497b3c17ee29d6ecce4560 Mon Sep 17 00:00:00 2001 From: Antti Koivisto Date: Thu, 14 May 2026 04:00:21 -0700 Subject: [PATCH 008/424] Implement case sensitive modifier (s) in attribute selector https://bugs.webkit.org/show_bug.cgi?id=272573 rdar://126331481 Reviewed by Anne van Kesteren. https://drafts.csswg.org/selectors/#attribute-case An explicit 's' modifier forces case-sensitive matching regardless of HTML's legacy case-insensitive attribute list, complementing the existing 'i' modifier. Extend CSSSelector::AttributeMatchType from two values to three: Default (no flag, honors HTML quirk), CaseInsensitive (i), and CaseSensitive (s). The value is stored in a single 2-bit field exposed via a new attributeMatchType() accessor, replacing the earlier boolean predicate pair. Matching call sites switch on the enum; explicit 's' short-circuits the HTML legacy list. * LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/cssom-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/semantics-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/syntax-expected.txt: Rebaseline: 620 previously-failing 's' flag subtests now pass. * Source/WebCore/css/CSSSelector.h: (WebCore::CSSSelector::CSSSelector): (WebCore::CSSSelector::attributeMatchType const): (WebCore::CSSSelector::attributeValueMatchingIsCaseInsensitive const): Deleted. Replace the two bit fields with one 2-bit m_attributeMatchType and extend AttributeMatchType with Default / CaseSensitive. * Source/WebCore/css/CSSSelector.cpp: (WebCore::CSSSelector::selectorText const): Emit ' s]' for explicit case-sensitive so round-tripping preserves the flag. (WebCore::CSSSelector::setAttribute): (WebCore::CSSSelector::CSSSelector): (WebCore::CSSSelector::simpleSelectorEqual const): * Source/WebCore/css/parser/CSSSelectorParser.cpp: (WebCore::CSSSelectorParser::consumeAttribute): (WebCore::CSSSelectorParser::consumeAttributeFlags): Accept 's' and return CaseSensitive; use Default for the no-flag case. * Source/WebCore/css/SelectorChecker.cpp: (WebCore::SelectorChecker::attributeSelectorMatches): (WebCore::SelectorChecker::checkOne const): Switch on attributeMatchType(); Default falls through to the existing HTML legacy case-insensitive check. * Source/WebCore/cssjit/SelectorCompiler.cpp: (WebCore::SelectorCompiler::attributeSelectorCaseSensitivity): Return CaseSensitive for explicit 's' before consulting the HTML legacy list. (WebCore::SelectorCompiler::AttributeMatchingInfo::AttributeMatchingInfo): * Source/WebCore/dom/SelectorQuery.cpp: (WebCore::canBeUsedForIdFastPath): (WebCore::canOptimizeSingleAttributeExactMatch): Compare against CSSSelector::CaseInsensitive directly; explicit 's' and Default continue to take the existing fast paths. Canonical link: https://commits.webkit.org/313234@main --- .../attribute-case/cssom-expected.txt | 64 +- .../attribute-case/semantics-expected.txt | 888 +++++++++--------- .../attribute-case/syntax-expected.txt | 306 +++--- Source/WebCore/css/CSSSelector.cpp | 19 +- Source/WebCore/css/CSSSelector.h | 14 +- Source/WebCore/css/SelectorChecker.cpp | 34 +- .../WebCore/css/parser/CSSSelectorParser.cpp | 10 +- Source/WebCore/cssjit/SelectorCompiler.cpp | 10 +- Source/WebCore/dom/SelectorQuery.cpp | 4 +- 9 files changed, 686 insertions(+), 663 deletions(-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/cssom-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/cssom-expected.txt index 2bf1e6e890d3..321f81884072 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/cssom-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/cssom-expected.txt @@ -39,36 +39,36 @@ PASS [*|foo="bar" i] insertRule in @media PASS [*|foo="bar" i] getting CSSRule#cssText in @media PASS [*|foo="bar" i] getting CSSStyleRule#selectorText in @media PASS [*|foo="bar" i] setting CSSStyleRule#selectorText in @media -FAIL [foo="bar" s] insertRule The string did not match the expected pattern. -FAIL [foo="bar" s] getting CSSRule#cssText The string did not match the expected pattern. -FAIL [foo="bar" s] getting CSSStyleRule#selectorText The string did not match the expected pattern. -FAIL [foo="bar" s] setting CSSStyleRule#selectorText assert_equals: expected "[foo=\"bar\" s]" but got "foobar" -FAIL [foo="bar" s] insertRule in @media The string did not match the expected pattern. -FAIL [foo="bar" s] getting CSSRule#cssText in @media The string did not match the expected pattern. -FAIL [foo="bar" s] getting CSSStyleRule#selectorText in @media The string did not match the expected pattern. -FAIL [foo="bar" s] setting CSSStyleRule#selectorText in @media assert_equals: expected "[foo=\"bar\" s]" but got "foobar" -FAIL [foo="bar" /**/ s] insertRule The string did not match the expected pattern. -FAIL [foo="bar" /**/ s] getting CSSRule#cssText The string did not match the expected pattern. -FAIL [foo="bar" /**/ s] getting CSSStyleRule#selectorText The string did not match the expected pattern. -FAIL [foo="bar" /**/ s] setting CSSStyleRule#selectorText assert_equals: expected "[foo=\"bar\" s]" but got "foobar" -FAIL [foo="bar" /**/ s] insertRule in @media The string did not match the expected pattern. -FAIL [foo="bar" /**/ s] getting CSSRule#cssText in @media The string did not match the expected pattern. -FAIL [foo="bar" /**/ s] getting CSSStyleRule#selectorText in @media The string did not match the expected pattern. -FAIL [foo="bar" /**/ s] setting CSSStyleRule#selectorText in @media assert_equals: expected "[foo=\"bar\" s]" but got "foobar" -FAIL [foo="bar"/**/s] insertRule The string did not match the expected pattern. -FAIL [foo="bar"/**/s] getting CSSRule#cssText The string did not match the expected pattern. -FAIL [foo="bar"/**/s] getting CSSStyleRule#selectorText The string did not match the expected pattern. -FAIL [foo="bar"/**/s] setting CSSStyleRule#selectorText assert_equals: expected "[foo=\"bar\" s]" but got "foobar" -FAIL [foo="bar"/**/s] insertRule in @media The string did not match the expected pattern. -FAIL [foo="bar"/**/s] getting CSSRule#cssText in @media The string did not match the expected pattern. -FAIL [foo="bar"/**/s] getting CSSStyleRule#selectorText in @media The string did not match the expected pattern. -FAIL [foo="bar"/**/s] setting CSSStyleRule#selectorText in @media assert_equals: expected "[foo=\"bar\" s]" but got "foobar" -FAIL [*|foo="bar" s] insertRule The string did not match the expected pattern. -FAIL [*|foo="bar" s] getting CSSRule#cssText The string did not match the expected pattern. -FAIL [*|foo="bar" s] getting CSSStyleRule#selectorText The string did not match the expected pattern. -FAIL [*|foo="bar" s] setting CSSStyleRule#selectorText assert_equals: expected "[*|foo=\"bar\" s]" but got "foobar" -FAIL [*|foo="bar" s] insertRule in @media The string did not match the expected pattern. -FAIL [*|foo="bar" s] getting CSSRule#cssText in @media The string did not match the expected pattern. -FAIL [*|foo="bar" s] getting CSSStyleRule#selectorText in @media The string did not match the expected pattern. -FAIL [*|foo="bar" s] setting CSSStyleRule#selectorText in @media assert_equals: expected "[*|foo=\"bar\" s]" but got "foobar" +PASS [foo="bar" s] insertRule +PASS [foo="bar" s] getting CSSRule#cssText +PASS [foo="bar" s] getting CSSStyleRule#selectorText +PASS [foo="bar" s] setting CSSStyleRule#selectorText +PASS [foo="bar" s] insertRule in @media +PASS [foo="bar" s] getting CSSRule#cssText in @media +PASS [foo="bar" s] getting CSSStyleRule#selectorText in @media +PASS [foo="bar" s] setting CSSStyleRule#selectorText in @media +PASS [foo="bar" /**/ s] insertRule +PASS [foo="bar" /**/ s] getting CSSRule#cssText +PASS [foo="bar" /**/ s] getting CSSStyleRule#selectorText +PASS [foo="bar" /**/ s] setting CSSStyleRule#selectorText +PASS [foo="bar" /**/ s] insertRule in @media +PASS [foo="bar" /**/ s] getting CSSRule#cssText in @media +PASS [foo="bar" /**/ s] getting CSSStyleRule#selectorText in @media +PASS [foo="bar" /**/ s] setting CSSStyleRule#selectorText in @media +PASS [foo="bar"/**/s] insertRule +PASS [foo="bar"/**/s] getting CSSRule#cssText +PASS [foo="bar"/**/s] getting CSSStyleRule#selectorText +PASS [foo="bar"/**/s] setting CSSStyleRule#selectorText +PASS [foo="bar"/**/s] insertRule in @media +PASS [foo="bar"/**/s] getting CSSRule#cssText in @media +PASS [foo="bar"/**/s] getting CSSStyleRule#selectorText in @media +PASS [foo="bar"/**/s] setting CSSStyleRule#selectorText in @media +PASS [*|foo="bar" s] insertRule +PASS [*|foo="bar" s] getting CSSRule#cssText +PASS [*|foo="bar" s] getting CSSStyleRule#selectorText +PASS [*|foo="bar" s] setting CSSStyleRule#selectorText +PASS [*|foo="bar" s] insertRule in @media +PASS [*|foo="bar" s] getting CSSRule#cssText in @media +PASS [*|foo="bar" s] getting CSSStyleRule#selectorText in @media +PASS [*|foo="bar" s] setting CSSStyleRule#selectorText in @media diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/semantics-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/semantics-expected.txt index 5e3efb92bc36..da2f9e82d188 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/semantics-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/semantics-expected.txt @@ -57,39 +57,39 @@ PASS [foo='BAR'][foo='bar' i]
in standards mode PASS [foo='BAR'][foo='bar' i]
with querySelector in standards mode PASS [foo='bar' i][foo='BAR']
in standards mode PASS [foo='bar' i][foo='BAR']
with querySelector in standards mode -FAIL [foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in standards mode '[foo='bar' s]' is not a valid selector. -FAIL [foo='' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s]
with querySelector in standards mode '[foo='' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in both */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in both */
with querySelector in standards mode '[foo='ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [*|foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in standards mode '[*|foo='bar' s]' is not a valid selector. -FAIL [*|foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in standards mode '[*|foo='bar' s]' is not a valid selector. -FAIL [align='left' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='left' s]
with querySelector in standards mode '[align='left' s]' is not a valid selector. -FAIL [align='LEFT' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='LEFT' s]
with querySelector in standards mode '[align='LEFT' s]' is not a valid selector. -FAIL [class~='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='a' s]
with querySelector in standards mode '[class~='a' s]' is not a valid selector. -FAIL [class~='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='A' s]
with querySelector in standards mode '[class~='A' s]' is not a valid selector. -FAIL [id^='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id^='a' s]
with querySelector in standards mode '[id^='a' s]' is not a valid selector. -FAIL [id$='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id$='A' s]
with querySelector in standards mode '[id$='A' s]' is not a valid selector. -FAIL [lang|='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang|='a' s]
with querySelector in standards mode '[lang|='a' s]' is not a valid selector. -FAIL [lang*='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang*='A' s]
with querySelector in standards mode '[lang*='A' s]' is not a valid selector. -FAIL [*|lang='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='a' s]
with querySelector in standards mode '[*|lang='a' s]' is not a valid selector. -FAIL [*|lang='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='A' s]
with querySelector in standards mode '[*|lang='A' s]' is not a valid selector. -FAIL @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='BAR' s][foo='BAR' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR' s][foo='BAR' s]
with querySelector in standards mode '[foo='BAR' s][foo='BAR' s]' is not a valid selector. +PASS [foo='bar' s]
in standards mode +PASS [foo='bar' s]
with querySelector in standards mode +PASS [foo='' s]
in standards mode +PASS [foo='' s]
with querySelector in standards mode +PASS [foo='ä' s] /* COMBINING in both */
in standards mode +PASS [foo='ä' s] /* COMBINING in both */
with querySelector in standards mode +PASS [*|foo='bar' s]
in standards mode +PASS [*|foo='bar' s]
with querySelector in standards mode +PASS [*|foo='bar' s]
in standards mode +PASS [*|foo='bar' s]
with querySelector in standards mode +PASS [align='left' s]
in standards mode +PASS [align='left' s]
with querySelector in standards mode +PASS [align='LEFT' s]
in standards mode +PASS [align='LEFT' s]
with querySelector in standards mode +PASS [class~='a' s]
in standards mode +PASS [class~='a' s]
with querySelector in standards mode +PASS [class~='A' s]
in standards mode +PASS [class~='A' s]
with querySelector in standards mode +PASS [id^='a' s]
in standards mode +PASS [id^='a' s]
with querySelector in standards mode +PASS [id$='A' s]
in standards mode +PASS [id$='A' s]
with querySelector in standards mode +PASS [lang|='a' s]
in standards mode +PASS [lang|='a' s]
with querySelector in standards mode +PASS [lang*='A' s]
in standards mode +PASS [lang*='A' s]
with querySelector in standards mode +PASS [*|lang='a' s]
in standards mode +PASS [*|lang='a' s]
with querySelector in standards mode +PASS [*|lang='A' s]
in standards mode +PASS [*|lang='A' s]
with querySelector in standards mode +PASS @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in standards mode +PASS [foo='BAR' s][foo='BAR' s]
in standards mode +PASS [foo='BAR' s][foo='BAR' s]
with querySelector in standards mode PASS [align='left'] /* sanity check (match HTML) */
in standards mode PASS [align='left'] /* sanity check (match HTML) */
with querySelector in standards mode PASS [align='LEFT'] /* sanity check (match HTML) */
in standards mode @@ -205,121 +205,121 @@ PASS [foo*='É' i]
in standards mode PASS [foo*='É' i]
with querySelector in standards mode PASS [foo|='É' i]
in standards mode PASS [foo|='É' i]
with querySelector in standards mode -FAIL [foo='' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s]
with querySelector in standards mode '[foo='' s]' is not a valid selector. -FAIL [foo='\0' s] /* \0 in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='\0' s] /* \0 in selector */
with querySelector in standards mode '[foo='\0' s] /* \0 in selector */' is not a valid selector. -FAIL [foo='' s] /* \0 in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s] /* \0 in attribute */
with querySelector in standards mode '[foo='' s] /* \0 in attribute */' is not a valid selector. -FAIL [foo='ä' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s]
with querySelector in standards mode '[foo='ä' s]' is not a valid selector. -FAIL [foo='Ä' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s]
with querySelector in standards mode '[foo='Ä' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo~='ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo~='ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo~='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo^='Ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo^='Ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo^='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo$='Ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo$='Ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo$='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo*='ä' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo*='ä' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo*='ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo|='ä' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo|='ä' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo|='ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo='Ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo='Ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in selector */
with querySelector in standards mode '[foo='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='a' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='a' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo='a' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='A' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='A' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo='A' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='a' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='a' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo='a' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='A' s] /* COMBINING in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='A' s] /* COMBINING in attribute */
with querySelector in standards mode '[foo='A' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='i' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='i' s]
with querySelector in standards mode '[foo='i' s]' is not a valid selector. -FAIL [foo='i' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='i' s]
with querySelector in standards mode '[foo='i' s]' is not a valid selector. -FAIL [foo='I' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='I' s]
with querySelector in standards mode '[foo='I' s]' is not a valid selector. -FAIL [foo='I' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='I' s]
with querySelector in standards mode '[foo='I' s]' is not a valid selector. -FAIL [foo='İ' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='İ' s]
with querySelector in standards mode '[foo='İ' s]' is not a valid selector. -FAIL [foo='ı' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ı' s]
with querySelector in standards mode '[foo='ı' s]' is not a valid selector. -FAIL [foo='İ' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='İ' s]
with querySelector in standards mode '[foo='İ' s]' is not a valid selector. -FAIL [foo='ı' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ı' s]
with querySelector in standards mode '[foo='ı' s]' is not a valid selector. -FAIL [foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in standards mode '[foo='bar' s]' is not a valid selector. -FAIL [|foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [|foo='bar' s]
with querySelector in standards mode '[|foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in standards mode '[foo='bar' s]' is not a valid selector. -FAIL [foo=' ' s] /* tab in selector */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo=' ' s] /* tab in selector */
with querySelector in standards mode '[foo=' ' s] /* tab in selector */' is not a valid selector. -FAIL [foo=' ' s] /* tab in attribute */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo=' ' s] /* tab in attribute */
with querySelector in standards mode '[foo=' ' s] /* tab in attribute */' is not a valid selector. -FAIL @namespace x 'a'; [x|foo='' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL @namespace x 'A'; [x|foo='' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='bar' s][foo='bar']
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='bar']
with querySelector in standards mode '[foo='bar' s][foo='bar']' is not a valid selector. -FAIL [foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in standards mode '[foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in standards mode '[foo='bar' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in both */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in both */
with querySelector in standards mode '[foo='ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in both */
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in both */
with querySelector in standards mode '[foo='Ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [*|foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in standards mode '[*|foo='bar' s]' is not a valid selector. -FAIL [*|foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in standards mode '[*|foo='bar' s]' is not a valid selector. -FAIL [align='left' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='left' s]
with querySelector in standards mode '[align='left' s]' is not a valid selector. -FAIL [align='LEFT' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='LEFT' s]
with querySelector in standards mode '[align='LEFT' s]' is not a valid selector. -FAIL [class~='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='a' s]
with querySelector in standards mode '[class~='a' s]' is not a valid selector. -FAIL [class~='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='A' s]
with querySelector in standards mode '[class~='A' s]' is not a valid selector. -FAIL [id^='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id^='a' s]
with querySelector in standards mode '[id^='a' s]' is not a valid selector. -FAIL [id$='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id$='A' s]
with querySelector in standards mode '[id$='A' s]' is not a valid selector. -FAIL [lang|='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang|='a' s]
with querySelector in standards mode '[lang|='a' s]' is not a valid selector. -FAIL [lang*='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang*='A' s]
with querySelector in standards mode '[lang*='A' s]' is not a valid selector. -FAIL [*|lang='a' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='a' s]
with querySelector in standards mode '[*|lang='a' s]' is not a valid selector. -FAIL [*|lang='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='A' s]
with querySelector in standards mode '[*|lang='A' s]' is not a valid selector. -FAIL @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='bar' s][foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='bar' s]
with querySelector in standards mode '[foo='bar' s][foo='bar' s]' is not a valid selector. -FAIL [foo='BAR' s][foo='bar']
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR' s][foo='bar']
with querySelector in standards mode '[foo='BAR' s][foo='bar']' is not a valid selector. -FAIL [foo='bar'][foo='BAR' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar'][foo='BAR' s]
with querySelector in standards mode '[foo='bar'][foo='BAR' s]' is not a valid selector. -FAIL [foo='BAR'][foo='bar' s]
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR'][foo='bar' s]
with querySelector in standards mode '[foo='BAR'][foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s][foo='BAR']
in standards mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='BAR']
with querySelector in standards mode '[foo='bar' s][foo='BAR']' is not a valid selector. +PASS [foo='' s]
in standards mode +PASS [foo='' s]
with querySelector in standards mode +PASS [foo='\0' s] /* \0 in selector */
in standards mode +PASS [foo='\0' s] /* \0 in selector */
with querySelector in standards mode +PASS [foo='' s] /* \0 in attribute */
in standards mode +PASS [foo='' s] /* \0 in attribute */
with querySelector in standards mode +PASS [foo='ä' s]
in standards mode +PASS [foo='ä' s]
with querySelector in standards mode +PASS [foo='Ä' s]
in standards mode +PASS [foo='Ä' s]
with querySelector in standards mode +PASS [foo='ä' s] /* COMBINING in selector */
in standards mode +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo~='ä' s] /* COMBINING in selector */
in standards mode +PASS [foo~='ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo^='Ä' s] /* COMBINING in selector */
in standards mode +PASS [foo^='Ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo$='Ä' s] /* COMBINING in selector */
in standards mode +PASS [foo$='Ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo*='ä' s] /* COMBINING in attribute */
in standards mode +PASS [foo*='ä' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo|='ä' s] /* COMBINING in attribute */
in standards mode +PASS [foo|='ä' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo='Ä' s] /* COMBINING in attribute */
in standards mode +PASS [foo='Ä' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo='Ä' s] /* COMBINING in attribute */
in standards mode +PASS [foo='Ä' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo='ä' s] /* COMBINING in selector */
in standards mode +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo='ä' s] /* COMBINING in selector */
in standards mode +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo='Ä' s] /* COMBINING in selector */
in standards mode +PASS [foo='Ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo='Ä' s] /* COMBINING in selector */
in standards mode +PASS [foo='Ä' s] /* COMBINING in selector */
with querySelector in standards mode +PASS [foo='a' s] /* COMBINING in attribute */
in standards mode +PASS [foo='a' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo='A' s] /* COMBINING in attribute */
in standards mode +PASS [foo='A' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo='a' s] /* COMBINING in attribute */
in standards mode +PASS [foo='a' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo='A' s] /* COMBINING in attribute */
in standards mode +PASS [foo='A' s] /* COMBINING in attribute */
with querySelector in standards mode +PASS [foo='i' s]
in standards mode +PASS [foo='i' s]
with querySelector in standards mode +PASS [foo='i' s]
in standards mode +PASS [foo='i' s]
with querySelector in standards mode +PASS [foo='I' s]
in standards mode +PASS [foo='I' s]
with querySelector in standards mode +PASS [foo='I' s]
in standards mode +PASS [foo='I' s]
with querySelector in standards mode +PASS [foo='İ' s]
in standards mode +PASS [foo='İ' s]
with querySelector in standards mode +PASS [foo='ı' s]
in standards mode +PASS [foo='ı' s]
with querySelector in standards mode +PASS [foo='İ' s]
in standards mode +PASS [foo='İ' s]
with querySelector in standards mode +PASS [foo='ı' s]
in standards mode +PASS [foo='ı' s]
with querySelector in standards mode +PASS [foo='bar' s]
in standards mode +PASS [foo='bar' s]
with querySelector in standards mode +PASS [|foo='bar' s]
in standards mode +PASS [|foo='bar' s]
with querySelector in standards mode +PASS [foo='bar' s]
in standards mode +PASS [foo='bar' s]
with querySelector in standards mode +PASS [foo=' ' s] /* tab in selector */
in standards mode +PASS [foo=' ' s] /* tab in selector */
with querySelector in standards mode +PASS [foo=' ' s] /* tab in attribute */
in standards mode +PASS [foo=' ' s] /* tab in attribute */
with querySelector in standards mode +PASS @namespace x 'a'; [x|foo='' s]
in standards mode +PASS @namespace x 'A'; [x|foo='' s]
in standards mode +PASS [foo='bar' s][foo='bar']
in standards mode +PASS [foo='bar' s][foo='bar']
with querySelector in standards mode +PASS [foo='bar' s]
in standards mode +PASS [foo='bar' s]
with querySelector in standards mode +PASS [foo='bar' s]
in standards mode +PASS [foo='bar' s]
with querySelector in standards mode +PASS [foo='ä' s] /* COMBINING in both */
in standards mode +PASS [foo='ä' s] /* COMBINING in both */
with querySelector in standards mode +PASS [foo='Ä' s] /* COMBINING in both */
in standards mode +PASS [foo='Ä' s] /* COMBINING in both */
with querySelector in standards mode +PASS [*|foo='bar' s]
in standards mode +PASS [*|foo='bar' s]
with querySelector in standards mode +PASS [*|foo='bar' s]
in standards mode +PASS [*|foo='bar' s]
with querySelector in standards mode +PASS [align='left' s]
in standards mode +PASS [align='left' s]
with querySelector in standards mode +PASS [align='LEFT' s]
in standards mode +PASS [align='LEFT' s]
with querySelector in standards mode +PASS [class~='a' s]
in standards mode +PASS [class~='a' s]
with querySelector in standards mode +PASS [class~='A' s]
in standards mode +PASS [class~='A' s]
with querySelector in standards mode +PASS [id^='a' s]
in standards mode +PASS [id^='a' s]
with querySelector in standards mode +PASS [id$='A' s]
in standards mode +PASS [id$='A' s]
with querySelector in standards mode +PASS [lang|='a' s]
in standards mode +PASS [lang|='a' s]
with querySelector in standards mode +PASS [lang*='A' s]
in standards mode +PASS [lang*='A' s]
with querySelector in standards mode +PASS [*|lang='a' s]
in standards mode +PASS [*|lang='a' s]
with querySelector in standards mode +PASS [*|lang='A' s]
in standards mode +PASS [*|lang='A' s]
with querySelector in standards mode +PASS @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in standards mode +PASS [foo='bar' s][foo='bar' s]
in standards mode +PASS [foo='bar' s][foo='bar' s]
with querySelector in standards mode +PASS [foo='BAR' s][foo='bar']
in standards mode +PASS [foo='BAR' s][foo='bar']
with querySelector in standards mode +PASS [foo='bar'][foo='BAR' s]
in standards mode +PASS [foo='bar'][foo='BAR' s]
with querySelector in standards mode +PASS [foo='BAR'][foo='bar' s]
in standards mode +PASS [foo='BAR'][foo='bar' s]
with querySelector in standards mode +PASS [foo='bar' s][foo='BAR']
in standards mode +PASS [foo='bar' s][foo='BAR']
with querySelector in standards mode PASS [foo='BAR'] /* sanity check (match) */
in quirks mode PASS [foo='BAR'] /* sanity check (match) */
with querySelector in quirks mode PASS [foo='bar'] /* sanity check (match) */
in quirks mode @@ -378,39 +378,39 @@ PASS [foo='BAR'][foo='bar' i]
in quirks mode PASS [foo='BAR'][foo='bar' i]
with querySelector in quirks mode PASS [foo='bar' i][foo='BAR']
in quirks mode PASS [foo='bar' i][foo='BAR']
with querySelector in quirks mode -FAIL [foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in quirks mode '[foo='bar' s]' is not a valid selector. -FAIL [foo='' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s]
with querySelector in quirks mode '[foo='' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in both */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in both */
with querySelector in quirks mode '[foo='ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [*|foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in quirks mode '[*|foo='bar' s]' is not a valid selector. -FAIL [*|foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in quirks mode '[*|foo='bar' s]' is not a valid selector. -FAIL [align='left' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='left' s]
with querySelector in quirks mode '[align='left' s]' is not a valid selector. -FAIL [align='LEFT' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='LEFT' s]
with querySelector in quirks mode '[align='LEFT' s]' is not a valid selector. -FAIL [class~='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='a' s]
with querySelector in quirks mode '[class~='a' s]' is not a valid selector. -FAIL [class~='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='A' s]
with querySelector in quirks mode '[class~='A' s]' is not a valid selector. -FAIL [id^='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id^='a' s]
with querySelector in quirks mode '[id^='a' s]' is not a valid selector. -FAIL [id$='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id$='A' s]
with querySelector in quirks mode '[id$='A' s]' is not a valid selector. -FAIL [lang|='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang|='a' s]
with querySelector in quirks mode '[lang|='a' s]' is not a valid selector. -FAIL [lang*='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang*='A' s]
with querySelector in quirks mode '[lang*='A' s]' is not a valid selector. -FAIL [*|lang='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='a' s]
with querySelector in quirks mode '[*|lang='a' s]' is not a valid selector. -FAIL [*|lang='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='A' s]
with querySelector in quirks mode '[*|lang='A' s]' is not a valid selector. -FAIL @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='BAR' s][foo='BAR' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR' s][foo='BAR' s]
with querySelector in quirks mode '[foo='BAR' s][foo='BAR' s]' is not a valid selector. +PASS [foo='bar' s]
in quirks mode +PASS [foo='bar' s]
with querySelector in quirks mode +PASS [foo='' s]
in quirks mode +PASS [foo='' s]
with querySelector in quirks mode +PASS [foo='ä' s] /* COMBINING in both */
in quirks mode +PASS [foo='ä' s] /* COMBINING in both */
with querySelector in quirks mode +PASS [*|foo='bar' s]
in quirks mode +PASS [*|foo='bar' s]
with querySelector in quirks mode +PASS [*|foo='bar' s]
in quirks mode +PASS [*|foo='bar' s]
with querySelector in quirks mode +PASS [align='left' s]
in quirks mode +PASS [align='left' s]
with querySelector in quirks mode +PASS [align='LEFT' s]
in quirks mode +PASS [align='LEFT' s]
with querySelector in quirks mode +PASS [class~='a' s]
in quirks mode +PASS [class~='a' s]
with querySelector in quirks mode +PASS [class~='A' s]
in quirks mode +PASS [class~='A' s]
with querySelector in quirks mode +PASS [id^='a' s]
in quirks mode +PASS [id^='a' s]
with querySelector in quirks mode +PASS [id$='A' s]
in quirks mode +PASS [id$='A' s]
with querySelector in quirks mode +PASS [lang|='a' s]
in quirks mode +PASS [lang|='a' s]
with querySelector in quirks mode +PASS [lang*='A' s]
in quirks mode +PASS [lang*='A' s]
with querySelector in quirks mode +PASS [*|lang='a' s]
in quirks mode +PASS [*|lang='a' s]
with querySelector in quirks mode +PASS [*|lang='A' s]
in quirks mode +PASS [*|lang='A' s]
with querySelector in quirks mode +PASS @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in quirks mode +PASS [foo='BAR' s][foo='BAR' s]
in quirks mode +PASS [foo='BAR' s][foo='BAR' s]
with querySelector in quirks mode PASS [align='left'] /* sanity check (match HTML) */
in quirks mode PASS [align='left'] /* sanity check (match HTML) */
with querySelector in quirks mode PASS [align='LEFT'] /* sanity check (match HTML) */
in quirks mode @@ -526,121 +526,121 @@ PASS [foo*='É' i]
in quirks mode PASS [foo*='É' i]
with querySelector in quirks mode PASS [foo|='É' i]
in quirks mode PASS [foo|='É' i]
with querySelector in quirks mode -FAIL [foo='' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s]
with querySelector in quirks mode '[foo='' s]' is not a valid selector. -FAIL [foo='\0' s] /* \0 in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='\0' s] /* \0 in selector */
with querySelector in quirks mode '[foo='\0' s] /* \0 in selector */' is not a valid selector. -FAIL [foo='' s] /* \0 in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s] /* \0 in attribute */
with querySelector in quirks mode '[foo='' s] /* \0 in attribute */' is not a valid selector. -FAIL [foo='ä' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s]
with querySelector in quirks mode '[foo='ä' s]' is not a valid selector. -FAIL [foo='Ä' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s]
with querySelector in quirks mode '[foo='Ä' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo~='ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo~='ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo~='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo^='Ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo^='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo^='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo$='Ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo$='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo$='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo*='ä' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo*='ä' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo*='ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo|='ä' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo|='ä' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo|='ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo='Ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo='Ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode '[foo='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='a' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='a' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo='a' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='A' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='A' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo='A' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='a' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='a' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo='a' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='A' s] /* COMBINING in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='A' s] /* COMBINING in attribute */
with querySelector in quirks mode '[foo='A' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='i' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='i' s]
with querySelector in quirks mode '[foo='i' s]' is not a valid selector. -FAIL [foo='i' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='i' s]
with querySelector in quirks mode '[foo='i' s]' is not a valid selector. -FAIL [foo='I' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='I' s]
with querySelector in quirks mode '[foo='I' s]' is not a valid selector. -FAIL [foo='I' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='I' s]
with querySelector in quirks mode '[foo='I' s]' is not a valid selector. -FAIL [foo='İ' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='İ' s]
with querySelector in quirks mode '[foo='İ' s]' is not a valid selector. -FAIL [foo='ı' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ı' s]
with querySelector in quirks mode '[foo='ı' s]' is not a valid selector. -FAIL [foo='İ' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='İ' s]
with querySelector in quirks mode '[foo='İ' s]' is not a valid selector. -FAIL [foo='ı' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ı' s]
with querySelector in quirks mode '[foo='ı' s]' is not a valid selector. -FAIL [foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in quirks mode '[foo='bar' s]' is not a valid selector. -FAIL [|foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [|foo='bar' s]
with querySelector in quirks mode '[|foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in quirks mode '[foo='bar' s]' is not a valid selector. -FAIL [foo=' ' s] /* tab in selector */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo=' ' s] /* tab in selector */
with querySelector in quirks mode '[foo=' ' s] /* tab in selector */' is not a valid selector. -FAIL [foo=' ' s] /* tab in attribute */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo=' ' s] /* tab in attribute */
with querySelector in quirks mode '[foo=' ' s] /* tab in attribute */' is not a valid selector. -FAIL @namespace x 'a'; [x|foo='' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL @namespace x 'A'; [x|foo='' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='bar' s][foo='bar']
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='bar']
with querySelector in quirks mode '[foo='bar' s][foo='bar']' is not a valid selector. -FAIL [foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in quirks mode '[foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in quirks mode '[foo='bar' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in both */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in both */
with querySelector in quirks mode '[foo='ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in both */
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in both */
with querySelector in quirks mode '[foo='Ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [*|foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in quirks mode '[*|foo='bar' s]' is not a valid selector. -FAIL [*|foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in quirks mode '[*|foo='bar' s]' is not a valid selector. -FAIL [align='left' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='left' s]
with querySelector in quirks mode '[align='left' s]' is not a valid selector. -FAIL [align='LEFT' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='LEFT' s]
with querySelector in quirks mode '[align='LEFT' s]' is not a valid selector. -FAIL [class~='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='a' s]
with querySelector in quirks mode '[class~='a' s]' is not a valid selector. -FAIL [class~='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='A' s]
with querySelector in quirks mode '[class~='A' s]' is not a valid selector. -FAIL [id^='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id^='a' s]
with querySelector in quirks mode '[id^='a' s]' is not a valid selector. -FAIL [id$='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id$='A' s]
with querySelector in quirks mode '[id$='A' s]' is not a valid selector. -FAIL [lang|='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang|='a' s]
with querySelector in quirks mode '[lang|='a' s]' is not a valid selector. -FAIL [lang*='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang*='A' s]
with querySelector in quirks mode '[lang*='A' s]' is not a valid selector. -FAIL [*|lang='a' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='a' s]
with querySelector in quirks mode '[*|lang='a' s]' is not a valid selector. -FAIL [*|lang='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='A' s]
with querySelector in quirks mode '[*|lang='A' s]' is not a valid selector. -FAIL @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='bar' s][foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='bar' s]
with querySelector in quirks mode '[foo='bar' s][foo='bar' s]' is not a valid selector. -FAIL [foo='BAR' s][foo='bar']
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR' s][foo='bar']
with querySelector in quirks mode '[foo='BAR' s][foo='bar']' is not a valid selector. -FAIL [foo='bar'][foo='BAR' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar'][foo='BAR' s]
with querySelector in quirks mode '[foo='bar'][foo='BAR' s]' is not a valid selector. -FAIL [foo='BAR'][foo='bar' s]
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR'][foo='bar' s]
with querySelector in quirks mode '[foo='BAR'][foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s][foo='BAR']
in quirks mode assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='BAR']
with querySelector in quirks mode '[foo='bar' s][foo='BAR']' is not a valid selector. +PASS [foo='' s]
in quirks mode +PASS [foo='' s]
with querySelector in quirks mode +PASS [foo='\0' s] /* \0 in selector */
in quirks mode +PASS [foo='\0' s] /* \0 in selector */
with querySelector in quirks mode +PASS [foo='' s] /* \0 in attribute */
in quirks mode +PASS [foo='' s] /* \0 in attribute */
with querySelector in quirks mode +PASS [foo='ä' s]
in quirks mode +PASS [foo='ä' s]
with querySelector in quirks mode +PASS [foo='Ä' s]
in quirks mode +PASS [foo='Ä' s]
with querySelector in quirks mode +PASS [foo='ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo~='ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo~='ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo^='Ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo^='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo$='Ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo$='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo*='ä' s] /* COMBINING in attribute */
in quirks mode +PASS [foo*='ä' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo|='ä' s] /* COMBINING in attribute */
in quirks mode +PASS [foo|='ä' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo='Ä' s] /* COMBINING in attribute */
in quirks mode +PASS [foo='Ä' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo='Ä' s] /* COMBINING in attribute */
in quirks mode +PASS [foo='Ä' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo='ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo='ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo='Ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo='Ä' s] /* COMBINING in selector */
in quirks mode +PASS [foo='Ä' s] /* COMBINING in selector */
with querySelector in quirks mode +PASS [foo='a' s] /* COMBINING in attribute */
in quirks mode +PASS [foo='a' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo='A' s] /* COMBINING in attribute */
in quirks mode +PASS [foo='A' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo='a' s] /* COMBINING in attribute */
in quirks mode +PASS [foo='a' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo='A' s] /* COMBINING in attribute */
in quirks mode +PASS [foo='A' s] /* COMBINING in attribute */
with querySelector in quirks mode +PASS [foo='i' s]
in quirks mode +PASS [foo='i' s]
with querySelector in quirks mode +PASS [foo='i' s]
in quirks mode +PASS [foo='i' s]
with querySelector in quirks mode +PASS [foo='I' s]
in quirks mode +PASS [foo='I' s]
with querySelector in quirks mode +PASS [foo='I' s]
in quirks mode +PASS [foo='I' s]
with querySelector in quirks mode +PASS [foo='İ' s]
in quirks mode +PASS [foo='İ' s]
with querySelector in quirks mode +PASS [foo='ı' s]
in quirks mode +PASS [foo='ı' s]
with querySelector in quirks mode +PASS [foo='İ' s]
in quirks mode +PASS [foo='İ' s]
with querySelector in quirks mode +PASS [foo='ı' s]
in quirks mode +PASS [foo='ı' s]
with querySelector in quirks mode +PASS [foo='bar' s]
in quirks mode +PASS [foo='bar' s]
with querySelector in quirks mode +PASS [|foo='bar' s]
in quirks mode +PASS [|foo='bar' s]
with querySelector in quirks mode +PASS [foo='bar' s]
in quirks mode +PASS [foo='bar' s]
with querySelector in quirks mode +PASS [foo=' ' s] /* tab in selector */
in quirks mode +PASS [foo=' ' s] /* tab in selector */
with querySelector in quirks mode +PASS [foo=' ' s] /* tab in attribute */
in quirks mode +PASS [foo=' ' s] /* tab in attribute */
with querySelector in quirks mode +PASS @namespace x 'a'; [x|foo='' s]
in quirks mode +PASS @namespace x 'A'; [x|foo='' s]
in quirks mode +PASS [foo='bar' s][foo='bar']
in quirks mode +PASS [foo='bar' s][foo='bar']
with querySelector in quirks mode +PASS [foo='bar' s]
in quirks mode +PASS [foo='bar' s]
with querySelector in quirks mode +PASS [foo='bar' s]
in quirks mode +PASS [foo='bar' s]
with querySelector in quirks mode +PASS [foo='ä' s] /* COMBINING in both */
in quirks mode +PASS [foo='ä' s] /* COMBINING in both */
with querySelector in quirks mode +PASS [foo='Ä' s] /* COMBINING in both */
in quirks mode +PASS [foo='Ä' s] /* COMBINING in both */
with querySelector in quirks mode +PASS [*|foo='bar' s]
in quirks mode +PASS [*|foo='bar' s]
with querySelector in quirks mode +PASS [*|foo='bar' s]
in quirks mode +PASS [*|foo='bar' s]
with querySelector in quirks mode +PASS [align='left' s]
in quirks mode +PASS [align='left' s]
with querySelector in quirks mode +PASS [align='LEFT' s]
in quirks mode +PASS [align='LEFT' s]
with querySelector in quirks mode +PASS [class~='a' s]
in quirks mode +PASS [class~='a' s]
with querySelector in quirks mode +PASS [class~='A' s]
in quirks mode +PASS [class~='A' s]
with querySelector in quirks mode +PASS [id^='a' s]
in quirks mode +PASS [id^='a' s]
with querySelector in quirks mode +PASS [id$='A' s]
in quirks mode +PASS [id$='A' s]
with querySelector in quirks mode +PASS [lang|='a' s]
in quirks mode +PASS [lang|='a' s]
with querySelector in quirks mode +PASS [lang*='A' s]
in quirks mode +PASS [lang*='A' s]
with querySelector in quirks mode +PASS [*|lang='a' s]
in quirks mode +PASS [*|lang='a' s]
with querySelector in quirks mode +PASS [*|lang='A' s]
in quirks mode +PASS [*|lang='A' s]
with querySelector in quirks mode +PASS @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in quirks mode +PASS [foo='bar' s][foo='bar' s]
in quirks mode +PASS [foo='bar' s][foo='bar' s]
with querySelector in quirks mode +PASS [foo='BAR' s][foo='bar']
in quirks mode +PASS [foo='BAR' s][foo='bar']
with querySelector in quirks mode +PASS [foo='bar'][foo='BAR' s]
in quirks mode +PASS [foo='bar'][foo='BAR' s]
with querySelector in quirks mode +PASS [foo='BAR'][foo='bar' s]
in quirks mode +PASS [foo='BAR'][foo='bar' s]
with querySelector in quirks mode +PASS [foo='bar' s][foo='BAR']
in quirks mode +PASS [foo='bar' s][foo='BAR']
with querySelector in quirks mode PASS [foo='BAR'] /* sanity check (match) */
in XML PASS [foo='BAR'] /* sanity check (match) */
with querySelector in XML PASS [foo='bar'] /* sanity check (match) */
in XML @@ -699,39 +699,39 @@ PASS [foo='BAR'][foo='bar' i]
in XML PASS [foo='BAR'][foo='bar' i]
with querySelector in XML PASS [foo='bar' i][foo='BAR']
in XML PASS [foo='bar' i][foo='BAR']
with querySelector in XML -FAIL [foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in XML '[foo='bar' s]' is not a valid selector. -FAIL [foo='' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s]
with querySelector in XML '[foo='' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in both */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in both */
with querySelector in XML '[foo='ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [*|foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in XML '[*|foo='bar' s]' is not a valid selector. -FAIL [*|foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in XML '[*|foo='bar' s]' is not a valid selector. -FAIL [align='left' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='left' s]
with querySelector in XML '[align='left' s]' is not a valid selector. -FAIL [align='LEFT' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='LEFT' s]
with querySelector in XML '[align='LEFT' s]' is not a valid selector. -FAIL [class~='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='a' s]
with querySelector in XML '[class~='a' s]' is not a valid selector. -FAIL [class~='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='A' s]
with querySelector in XML '[class~='A' s]' is not a valid selector. -FAIL [id^='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id^='a' s]
with querySelector in XML '[id^='a' s]' is not a valid selector. -FAIL [id$='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id$='A' s]
with querySelector in XML '[id$='A' s]' is not a valid selector. -FAIL [lang|='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang|='a' s]
with querySelector in XML '[lang|='a' s]' is not a valid selector. -FAIL [lang*='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang*='A' s]
with querySelector in XML '[lang*='A' s]' is not a valid selector. -FAIL [*|lang='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='a' s]
with querySelector in XML '[*|lang='a' s]' is not a valid selector. -FAIL [*|lang='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='A' s]
with querySelector in XML '[*|lang='A' s]' is not a valid selector. -FAIL @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='BAR' s][foo='BAR' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR' s][foo='BAR' s]
with querySelector in XML '[foo='BAR' s][foo='BAR' s]' is not a valid selector. +PASS [foo='bar' s]
in XML +PASS [foo='bar' s]
with querySelector in XML +PASS [foo='' s]
in XML +PASS [foo='' s]
with querySelector in XML +PASS [foo='ä' s] /* COMBINING in both */
in XML +PASS [foo='ä' s] /* COMBINING in both */
with querySelector in XML +PASS [*|foo='bar' s]
in XML +PASS [*|foo='bar' s]
with querySelector in XML +PASS [*|foo='bar' s]
in XML +PASS [*|foo='bar' s]
with querySelector in XML +PASS [align='left' s]
in XML +PASS [align='left' s]
with querySelector in XML +PASS [align='LEFT' s]
in XML +PASS [align='LEFT' s]
with querySelector in XML +PASS [class~='a' s]
in XML +PASS [class~='a' s]
with querySelector in XML +PASS [class~='A' s]
in XML +PASS [class~='A' s]
with querySelector in XML +PASS [id^='a' s]
in XML +PASS [id^='a' s]
with querySelector in XML +PASS [id$='A' s]
in XML +PASS [id$='A' s]
with querySelector in XML +PASS [lang|='a' s]
in XML +PASS [lang|='a' s]
with querySelector in XML +PASS [lang*='A' s]
in XML +PASS [lang*='A' s]
with querySelector in XML +PASS [*|lang='a' s]
in XML +PASS [*|lang='a' s]
with querySelector in XML +PASS [*|lang='A' s]
in XML +PASS [*|lang='A' s]
with querySelector in XML +PASS @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in XML +PASS [foo='BAR' s][foo='BAR' s]
in XML +PASS [foo='BAR' s][foo='BAR' s]
with querySelector in XML PASS [missingattr] /* sanity check (no match) */
in XML PASS [missingattr] /* sanity check (no match) */
with querySelector in XML PASS [foo='bar'] /* sanity check (no match) */
in XML @@ -839,120 +839,120 @@ PASS [foo*='É' i]
in XML PASS [foo*='É' i]
with querySelector in XML PASS [foo|='É' i]
in XML PASS [foo|='É' i]
with querySelector in XML -FAIL [foo='' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s]
with querySelector in XML '[foo='' s]' is not a valid selector. -FAIL [foo='\0' s] /* \0 in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='\0' s] /* \0 in selector */
with querySelector in XML '[foo='\0' s] /* \0 in selector */' is not a valid selector. -FAIL [foo='' s] /* \0 in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='' s] /* \0 in attribute */
with querySelector in XML '[foo='' s] /* \0 in attribute */' is not a valid selector. -FAIL [foo='ä' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s]
with querySelector in XML '[foo='ä' s]' is not a valid selector. -FAIL [foo='Ä' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s]
with querySelector in XML '[foo='Ä' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in XML '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo~='ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo~='ä' s] /* COMBINING in selector */
with querySelector in XML '[foo~='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo^='Ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo^='Ä' s] /* COMBINING in selector */
with querySelector in XML '[foo^='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo$='Ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo$='Ä' s] /* COMBINING in selector */
with querySelector in XML '[foo$='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo*='ä' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo*='ä' s] /* COMBINING in attribute */
with querySelector in XML '[foo*='ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo|='ä' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo|='ä' s] /* COMBINING in attribute */
with querySelector in XML '[foo|='ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in attribute */
with querySelector in XML '[foo='Ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in attribute */
with querySelector in XML '[foo='Ä' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in XML '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in selector */
with querySelector in XML '[foo='ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in selector */
with querySelector in XML '[foo='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in selector */
with querySelector in XML '[foo='Ä' s] /* COMBINING in selector */' is not a valid selector. -FAIL [foo='a' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='a' s] /* COMBINING in attribute */
with querySelector in XML '[foo='a' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='A' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='A' s] /* COMBINING in attribute */
with querySelector in XML '[foo='A' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='a' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='a' s] /* COMBINING in attribute */
with querySelector in XML '[foo='a' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='A' s] /* COMBINING in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='A' s] /* COMBINING in attribute */
with querySelector in XML '[foo='A' s] /* COMBINING in attribute */' is not a valid selector. -FAIL [foo='i' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='i' s]
with querySelector in XML '[foo='i' s]' is not a valid selector. -FAIL [foo='i' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='i' s]
with querySelector in XML '[foo='i' s]' is not a valid selector. -FAIL [foo='I' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='I' s]
with querySelector in XML '[foo='I' s]' is not a valid selector. -FAIL [foo='I' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='I' s]
with querySelector in XML '[foo='I' s]' is not a valid selector. -FAIL [foo='İ' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='İ' s]
with querySelector in XML '[foo='İ' s]' is not a valid selector. -FAIL [foo='ı' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ı' s]
with querySelector in XML '[foo='ı' s]' is not a valid selector. -FAIL [foo='İ' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='İ' s]
with querySelector in XML '[foo='İ' s]' is not a valid selector. -FAIL [foo='ı' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ı' s]
with querySelector in XML '[foo='ı' s]' is not a valid selector. -FAIL [foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in XML '[foo='bar' s]' is not a valid selector. -FAIL [|foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [|foo='bar' s]
with querySelector in XML '[|foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in XML '[foo='bar' s]' is not a valid selector. -FAIL [foo=' ' s] /* tab in selector */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo=' ' s] /* tab in selector */
with querySelector in XML '[foo=' ' s] /* tab in selector */' is not a valid selector. -FAIL [foo=' ' s] /* tab in attribute */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo=' ' s] /* tab in attribute */
with querySelector in XML '[foo=' ' s] /* tab in attribute */' is not a valid selector. -FAIL @namespace x 'a'; [x|foo='' s]
in XML assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL @namespace x 'A'; [x|foo='' s]
in XML assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='bar' s][foo='bar']
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='bar']
with querySelector in XML '[foo='bar' s][foo='bar']' is not a valid selector. -FAIL [foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in XML '[foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s]
with querySelector in XML '[foo='bar' s]' is not a valid selector. -FAIL [foo='ä' s] /* COMBINING in both */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='ä' s] /* COMBINING in both */
with querySelector in XML '[foo='ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [foo='Ä' s] /* COMBINING in both */
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='Ä' s] /* COMBINING in both */
with querySelector in XML '[foo='Ä' s] /* COMBINING in both */' is not a valid selector. -FAIL [*|foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in XML '[*|foo='bar' s]' is not a valid selector. -FAIL [*|foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|foo='bar' s]
with querySelector in XML '[*|foo='bar' s]' is not a valid selector. -FAIL [align='left' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='left' s]
with querySelector in XML '[align='left' s]' is not a valid selector. -FAIL [align='LEFT' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [align='LEFT' s]
with querySelector in XML '[align='LEFT' s]' is not a valid selector. -FAIL [class~='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='a' s]
with querySelector in XML '[class~='a' s]' is not a valid selector. -FAIL [class~='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [class~='A' s]
with querySelector in XML '[class~='A' s]' is not a valid selector. -FAIL [id^='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id^='a' s]
with querySelector in XML '[id^='a' s]' is not a valid selector. -FAIL [id$='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [id$='A' s]
with querySelector in XML '[id$='A' s]' is not a valid selector. -FAIL [lang|='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang|='a' s]
with querySelector in XML '[lang|='a' s]' is not a valid selector. -FAIL [lang*='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [lang*='A' s]
with querySelector in XML '[lang*='A' s]' is not a valid selector. -FAIL [*|lang='a' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='a' s]
with querySelector in XML '[*|lang='a' s]' is not a valid selector. -FAIL [*|lang='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|lang='A' s]
with querySelector in XML '[*|lang='A' s]' is not a valid selector. -FAIL @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in XML assert_equals: rule didn't parse into CSSOM expected 2 but got 1 -FAIL [foo='bar' s][foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='bar' s]
with querySelector in XML '[foo='bar' s][foo='bar' s]' is not a valid selector. -FAIL [foo='BAR' s][foo='bar']
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR' s][foo='bar']
with querySelector in XML '[foo='BAR' s][foo='bar']' is not a valid selector. -FAIL [foo='bar'][foo='BAR' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar'][foo='BAR' s]
with querySelector in XML '[foo='bar'][foo='BAR' s]' is not a valid selector. -FAIL [foo='BAR'][foo='bar' s]
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='BAR'][foo='bar' s]
with querySelector in XML '[foo='BAR'][foo='bar' s]' is not a valid selector. -FAIL [foo='bar' s][foo='BAR']
in XML assert_equals: rule didn't parse into CSSOM expected 1 but got 0 -FAIL [foo='bar' s][foo='BAR']
with querySelector in XML '[foo='bar' s][foo='BAR']' is not a valid selector. +PASS [foo='' s]
in XML +PASS [foo='' s]
with querySelector in XML +PASS [foo='\0' s] /* \0 in selector */
in XML +PASS [foo='\0' s] /* \0 in selector */
with querySelector in XML +PASS [foo='' s] /* \0 in attribute */
in XML +PASS [foo='' s] /* \0 in attribute */
with querySelector in XML +PASS [foo='ä' s]
in XML +PASS [foo='ä' s]
with querySelector in XML +PASS [foo='Ä' s]
in XML +PASS [foo='Ä' s]
with querySelector in XML +PASS [foo='ä' s] /* COMBINING in selector */
in XML +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo~='ä' s] /* COMBINING in selector */
in XML +PASS [foo~='ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo^='Ä' s] /* COMBINING in selector */
in XML +PASS [foo^='Ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo$='Ä' s] /* COMBINING in selector */
in XML +PASS [foo$='Ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo*='ä' s] /* COMBINING in attribute */
in XML +PASS [foo*='ä' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo|='ä' s] /* COMBINING in attribute */
in XML +PASS [foo|='ä' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo='Ä' s] /* COMBINING in attribute */
in XML +PASS [foo='Ä' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo='Ä' s] /* COMBINING in attribute */
in XML +PASS [foo='Ä' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo='ä' s] /* COMBINING in selector */
in XML +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo='ä' s] /* COMBINING in selector */
in XML +PASS [foo='ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo='Ä' s] /* COMBINING in selector */
in XML +PASS [foo='Ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo='Ä' s] /* COMBINING in selector */
in XML +PASS [foo='Ä' s] /* COMBINING in selector */
with querySelector in XML +PASS [foo='a' s] /* COMBINING in attribute */
in XML +PASS [foo='a' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo='A' s] /* COMBINING in attribute */
in XML +PASS [foo='A' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo='a' s] /* COMBINING in attribute */
in XML +PASS [foo='a' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo='A' s] /* COMBINING in attribute */
in XML +PASS [foo='A' s] /* COMBINING in attribute */
with querySelector in XML +PASS [foo='i' s]
in XML +PASS [foo='i' s]
with querySelector in XML +PASS [foo='i' s]
in XML +PASS [foo='i' s]
with querySelector in XML +PASS [foo='I' s]
in XML +PASS [foo='I' s]
with querySelector in XML +PASS [foo='I' s]
in XML +PASS [foo='I' s]
with querySelector in XML +PASS [foo='İ' s]
in XML +PASS [foo='İ' s]
with querySelector in XML +PASS [foo='ı' s]
in XML +PASS [foo='ı' s]
with querySelector in XML +PASS [foo='İ' s]
in XML +PASS [foo='İ' s]
with querySelector in XML +PASS [foo='ı' s]
in XML +PASS [foo='ı' s]
with querySelector in XML +PASS [foo='bar' s]
in XML +PASS [foo='bar' s]
with querySelector in XML +PASS [|foo='bar' s]
in XML +PASS [|foo='bar' s]
with querySelector in XML +PASS [foo='bar' s]
in XML +PASS [foo='bar' s]
with querySelector in XML +PASS [foo=' ' s] /* tab in selector */
in XML +PASS [foo=' ' s] /* tab in selector */
with querySelector in XML +PASS [foo=' ' s] /* tab in attribute */
in XML +PASS [foo=' ' s] /* tab in attribute */
with querySelector in XML +PASS @namespace x 'a'; [x|foo='' s]
in XML +PASS @namespace x 'A'; [x|foo='' s]
in XML +PASS [foo='bar' s][foo='bar']
in XML +PASS [foo='bar' s][foo='bar']
with querySelector in XML +PASS [foo='bar' s]
in XML +PASS [foo='bar' s]
with querySelector in XML +PASS [foo='bar' s]
in XML +PASS [foo='bar' s]
with querySelector in XML +PASS [foo='ä' s] /* COMBINING in both */
in XML +PASS [foo='ä' s] /* COMBINING in both */
with querySelector in XML +PASS [foo='Ä' s] /* COMBINING in both */
in XML +PASS [foo='Ä' s] /* COMBINING in both */
with querySelector in XML +PASS [*|foo='bar' s]
in XML +PASS [*|foo='bar' s]
with querySelector in XML +PASS [*|foo='bar' s]
in XML +PASS [*|foo='bar' s]
with querySelector in XML +PASS [align='left' s]
in XML +PASS [align='left' s]
with querySelector in XML +PASS [align='LEFT' s]
in XML +PASS [align='LEFT' s]
with querySelector in XML +PASS [class~='a' s]
in XML +PASS [class~='a' s]
with querySelector in XML +PASS [class~='A' s]
in XML +PASS [class~='A' s]
with querySelector in XML +PASS [id^='a' s]
in XML +PASS [id^='a' s]
with querySelector in XML +PASS [id$='A' s]
in XML +PASS [id$='A' s]
with querySelector in XML +PASS [lang|='a' s]
in XML +PASS [lang|='a' s]
with querySelector in XML +PASS [lang*='A' s]
in XML +PASS [lang*='A' s]
with querySelector in XML +PASS [*|lang='a' s]
in XML +PASS [*|lang='a' s]
with querySelector in XML +PASS [*|lang='A' s]
in XML +PASS [*|lang='A' s]
with querySelector in XML +PASS @namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]
in XML +PASS [foo='bar' s][foo='bar' s]
in XML +PASS [foo='bar' s][foo='bar' s]
with querySelector in XML +PASS [foo='BAR' s][foo='bar']
in XML +PASS [foo='BAR' s][foo='bar']
with querySelector in XML +PASS [foo='bar'][foo='BAR' s]
in XML +PASS [foo='bar'][foo='BAR' s]
with querySelector in XML +PASS [foo='BAR'][foo='bar' s]
in XML +PASS [foo='BAR'][foo='bar' s]
with querySelector in XML +PASS [foo='bar' s][foo='BAR']
in XML +PASS [foo='bar' s][foo='BAR']
with querySelector in XML diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/syntax-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/syntax-expected.txt index a2cf941ef2be..1ca3ebe918b2 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/syntax-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/selectors/attribute-selectors/attribute-case/syntax-expected.txt @@ -55,60 +55,58 @@ PASS [|foo='bar' i] in standards mode PASS [|foo='bar' i] with querySelector in standards mode PASS [*|foo='bar' i] in standards mode PASS [*|foo='bar' i] with querySelector in standards mode -FAIL [baz='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s] with querySelector in standards mode '[baz='quux' s]' is not a valid selector. -FAIL [baz='quux' S] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' S] with querySelector in standards mode '[baz='quux' S]' is not a valid selector. -FAIL [baz=quux s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz=quux s] with querySelector in standards mode '[baz=quux s]' is not a valid selector. -FAIL [baz="quux" s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz="quux" s] with querySelector in standards mode '[baz="quux" s]' is not a valid selector. -FAIL [baz='quux's] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux's] with querySelector in standards mode '[baz='quux's]' is not a valid selector. -FAIL [baz='quux's ] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux's ] with querySelector in standards mode '[baz='quux's ]' is not a valid selector. -FAIL [baz='quux' s ] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s ] with querySelector in standards mode '[baz='quux' s ]' is not a valid selector. -FAIL [baz='quux' /**/ s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' /**/ s] with querySelector in standards mode '[baz='quux' /**/ s]' is not a valid selector. -FAIL [baz='quux' s /**/ ] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s /**/ ] with querySelector in standards mode '[baz='quux' s /**/ ]' is not a valid selector. -FAIL [baz='quux'/**/s/**/] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux'/**/s/**/] with querySelector in standards mode '[baz='quux'/**/s/**/]' is not a valid selector. -FAIL [baz=quux/**/s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz=quux/**/s] with querySelector in standards mode '[baz=quux/**/s]' is not a valid selector. -FAIL [baz='quux' s ] /* \t */ in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s ] /* \t */ with querySelector in standards mode '[baz='quux' s ] /* \t */' is not a valid selector. -FAIL [baz='quux' +PASS [baz='quux' s] in standards mode +PASS [baz='quux' s] with querySelector in standards mode +PASS [baz='quux' S] in standards mode +PASS [baz='quux' S] with querySelector in standards mode +PASS [baz=quux s] in standards mode +PASS [baz=quux s] with querySelector in standards mode +PASS [baz="quux" s] in standards mode +PASS [baz="quux" s] with querySelector in standards mode +PASS [baz='quux's] in standards mode +PASS [baz='quux's] with querySelector in standards mode +PASS [baz='quux's ] in standards mode +PASS [baz='quux's ] with querySelector in standards mode +PASS [baz='quux' s ] in standards mode +PASS [baz='quux' s ] with querySelector in standards mode +PASS [baz='quux' /**/ s] in standards mode +PASS [baz='quux' /**/ s] with querySelector in standards mode +PASS [baz='quux' s /**/ ] in standards mode +PASS [baz='quux' s /**/ ] with querySelector in standards mode +PASS [baz='quux'/**/s/**/] in standards mode +PASS [baz='quux'/**/s/**/] with querySelector in standards mode +PASS [baz=quux/**/s] in standards mode +PASS [baz=quux/**/s] with querySelector in standards mode +PASS [baz='quux' s ] /* \t */ in standards mode +PASS [baz='quux' s ] /* \t */ with querySelector in standards mode +PASS [baz='quux' s -] /* \n */ in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' -s -] /* \n */ with querySelector in standards mode '[baz='quux' +] /* \n */ in standards mode +PASS [baz='quux' s -] /* \n */' is not a valid selector. -FAIL [baz='quux'\rs\r] /* \r */ in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux'\rs\r] /* \r */ with querySelector in standards mode '[baz='quux'\rs\r] /* \r */' is not a valid selector. -FAIL [baz='quux' \s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \s] with querySelector in standards mode '[baz='quux' \s]' is not a valid selector. -FAIL [baz='quux' \73] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \73] with querySelector in standards mode '[baz='quux' \73]' is not a valid selector. -FAIL [baz='quux' \53] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \53] with querySelector in standards mode '[baz='quux' \53]' is not a valid selector. -FAIL [baz~='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz~='quux' s] with querySelector in standards mode '[baz~='quux' s]' is not a valid selector. -FAIL [baz^='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz^='quux' s] with querySelector in standards mode '[baz^='quux' s]' is not a valid selector. -FAIL [baz$='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz$='quux' s] with querySelector in standards mode '[baz$='quux' s]' is not a valid selector. -FAIL [baz*='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz*='quux' s] with querySelector in standards mode '[baz*='quux' s]' is not a valid selector. -FAIL [baz|='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz|='quux' s] with querySelector in standards mode '[baz|='quux' s]' is not a valid selector. -FAIL [|baz='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [|baz='quux' s] with querySelector in standards mode '[|baz='quux' s]' is not a valid selector. -FAIL [*|baz='quux' s] in standards mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|baz='quux' s] with querySelector in standards mode '[*|baz='quux' s]' is not a valid selector. +] /* \n */ with querySelector in standards mode +PASS [baz='quux'\rs\r] /* \r */ in standards mode +PASS [baz='quux'\rs\r] /* \r */ with querySelector in standards mode +PASS [baz='quux' \s] in standards mode +PASS [baz='quux' \s] with querySelector in standards mode +PASS [baz='quux' \73] in standards mode +PASS [baz='quux' \73] with querySelector in standards mode +PASS [baz='quux' \53] in standards mode +PASS [baz='quux' \53] with querySelector in standards mode +PASS [baz~='quux' s] in standards mode +PASS [baz~='quux' s] with querySelector in standards mode +PASS [baz^='quux' s] in standards mode +PASS [baz^='quux' s] with querySelector in standards mode +PASS [baz$='quux' s] in standards mode +PASS [baz$='quux' s] with querySelector in standards mode +PASS [baz*='quux' s] in standards mode +PASS [baz*='quux' s] with querySelector in standards mode +PASS [baz|='quux' s] in standards mode +PASS [baz|='quux' s] with querySelector in standards mode +PASS [|baz='quux' s] in standards mode +PASS [|baz='quux' s] with querySelector in standards mode +PASS [*|baz='quux' s] in standards mode +PASS [*|baz='quux' s] with querySelector in standards mode PASS [foo[ /* sanity check (invalid) */ in standards mode PASS [foo[ /* sanity check (invalid) */ with querySelector in standards mode PASS [foo='bar' i i] in standards mode @@ -241,60 +239,58 @@ PASS [|foo='bar' i] in quirks mode PASS [|foo='bar' i] with querySelector in quirks mode PASS [*|foo='bar' i] in quirks mode PASS [*|foo='bar' i] with querySelector in quirks mode -FAIL [baz='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s] with querySelector in quirks mode '[baz='quux' s]' is not a valid selector. -FAIL [baz='quux' S] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' S] with querySelector in quirks mode '[baz='quux' S]' is not a valid selector. -FAIL [baz=quux s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz=quux s] with querySelector in quirks mode '[baz=quux s]' is not a valid selector. -FAIL [baz="quux" s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz="quux" s] with querySelector in quirks mode '[baz="quux" s]' is not a valid selector. -FAIL [baz='quux's] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux's] with querySelector in quirks mode '[baz='quux's]' is not a valid selector. -FAIL [baz='quux's ] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux's ] with querySelector in quirks mode '[baz='quux's ]' is not a valid selector. -FAIL [baz='quux' s ] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s ] with querySelector in quirks mode '[baz='quux' s ]' is not a valid selector. -FAIL [baz='quux' /**/ s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' /**/ s] with querySelector in quirks mode '[baz='quux' /**/ s]' is not a valid selector. -FAIL [baz='quux' s /**/ ] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s /**/ ] with querySelector in quirks mode '[baz='quux' s /**/ ]' is not a valid selector. -FAIL [baz='quux'/**/s/**/] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux'/**/s/**/] with querySelector in quirks mode '[baz='quux'/**/s/**/]' is not a valid selector. -FAIL [baz=quux/**/s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz=quux/**/s] with querySelector in quirks mode '[baz=quux/**/s]' is not a valid selector. -FAIL [baz='quux' s ] /* \t */ in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s ] /* \t */ with querySelector in quirks mode '[baz='quux' s ] /* \t */' is not a valid selector. -FAIL [baz='quux' +PASS [baz='quux' s] in quirks mode +PASS [baz='quux' s] with querySelector in quirks mode +PASS [baz='quux' S] in quirks mode +PASS [baz='quux' S] with querySelector in quirks mode +PASS [baz=quux s] in quirks mode +PASS [baz=quux s] with querySelector in quirks mode +PASS [baz="quux" s] in quirks mode +PASS [baz="quux" s] with querySelector in quirks mode +PASS [baz='quux's] in quirks mode +PASS [baz='quux's] with querySelector in quirks mode +PASS [baz='quux's ] in quirks mode +PASS [baz='quux's ] with querySelector in quirks mode +PASS [baz='quux' s ] in quirks mode +PASS [baz='quux' s ] with querySelector in quirks mode +PASS [baz='quux' /**/ s] in quirks mode +PASS [baz='quux' /**/ s] with querySelector in quirks mode +PASS [baz='quux' s /**/ ] in quirks mode +PASS [baz='quux' s /**/ ] with querySelector in quirks mode +PASS [baz='quux'/**/s/**/] in quirks mode +PASS [baz='quux'/**/s/**/] with querySelector in quirks mode +PASS [baz=quux/**/s] in quirks mode +PASS [baz=quux/**/s] with querySelector in quirks mode +PASS [baz='quux' s ] /* \t */ in quirks mode +PASS [baz='quux' s ] /* \t */ with querySelector in quirks mode +PASS [baz='quux' s -] /* \n */ in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' -s -] /* \n */ with querySelector in quirks mode '[baz='quux' +] /* \n */ in quirks mode +PASS [baz='quux' s -] /* \n */' is not a valid selector. -FAIL [baz='quux'\rs\r] /* \r */ in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux'\rs\r] /* \r */ with querySelector in quirks mode '[baz='quux'\rs\r] /* \r */' is not a valid selector. -FAIL [baz='quux' \s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \s] with querySelector in quirks mode '[baz='quux' \s]' is not a valid selector. -FAIL [baz='quux' \73] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \73] with querySelector in quirks mode '[baz='quux' \73]' is not a valid selector. -FAIL [baz='quux' \53] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \53] with querySelector in quirks mode '[baz='quux' \53]' is not a valid selector. -FAIL [baz~='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz~='quux' s] with querySelector in quirks mode '[baz~='quux' s]' is not a valid selector. -FAIL [baz^='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz^='quux' s] with querySelector in quirks mode '[baz^='quux' s]' is not a valid selector. -FAIL [baz$='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz$='quux' s] with querySelector in quirks mode '[baz$='quux' s]' is not a valid selector. -FAIL [baz*='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz*='quux' s] with querySelector in quirks mode '[baz*='quux' s]' is not a valid selector. -FAIL [baz|='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz|='quux' s] with querySelector in quirks mode '[baz|='quux' s]' is not a valid selector. -FAIL [|baz='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [|baz='quux' s] with querySelector in quirks mode '[|baz='quux' s]' is not a valid selector. -FAIL [*|baz='quux' s] in quirks mode assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|baz='quux' s] with querySelector in quirks mode '[*|baz='quux' s]' is not a valid selector. +] /* \n */ with querySelector in quirks mode +PASS [baz='quux'\rs\r] /* \r */ in quirks mode +PASS [baz='quux'\rs\r] /* \r */ with querySelector in quirks mode +PASS [baz='quux' \s] in quirks mode +PASS [baz='quux' \s] with querySelector in quirks mode +PASS [baz='quux' \73] in quirks mode +PASS [baz='quux' \73] with querySelector in quirks mode +PASS [baz='quux' \53] in quirks mode +PASS [baz='quux' \53] with querySelector in quirks mode +PASS [baz~='quux' s] in quirks mode +PASS [baz~='quux' s] with querySelector in quirks mode +PASS [baz^='quux' s] in quirks mode +PASS [baz^='quux' s] with querySelector in quirks mode +PASS [baz$='quux' s] in quirks mode +PASS [baz$='quux' s] with querySelector in quirks mode +PASS [baz*='quux' s] in quirks mode +PASS [baz*='quux' s] with querySelector in quirks mode +PASS [baz|='quux' s] in quirks mode +PASS [baz|='quux' s] with querySelector in quirks mode +PASS [|baz='quux' s] in quirks mode +PASS [|baz='quux' s] with querySelector in quirks mode +PASS [*|baz='quux' s] in quirks mode +PASS [*|baz='quux' s] with querySelector in quirks mode PASS [foo[ /* sanity check (invalid) */ in quirks mode PASS [foo[ /* sanity check (invalid) */ with querySelector in quirks mode PASS [foo='bar' i i] in quirks mode @@ -427,60 +423,58 @@ PASS [|foo='bar' i] in XML PASS [|foo='bar' i] with querySelector in XML PASS [*|foo='bar' i] in XML PASS [*|foo='bar' i] with querySelector in XML -FAIL [baz='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s] with querySelector in XML '[baz='quux' s]' is not a valid selector. -FAIL [baz='quux' S] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' S] with querySelector in XML '[baz='quux' S]' is not a valid selector. -FAIL [baz=quux s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz=quux s] with querySelector in XML '[baz=quux s]' is not a valid selector. -FAIL [baz="quux" s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz="quux" s] with querySelector in XML '[baz="quux" s]' is not a valid selector. -FAIL [baz='quux's] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux's] with querySelector in XML '[baz='quux's]' is not a valid selector. -FAIL [baz='quux's ] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux's ] with querySelector in XML '[baz='quux's ]' is not a valid selector. -FAIL [baz='quux' s ] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s ] with querySelector in XML '[baz='quux' s ]' is not a valid selector. -FAIL [baz='quux' /**/ s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' /**/ s] with querySelector in XML '[baz='quux' /**/ s]' is not a valid selector. -FAIL [baz='quux' s /**/ ] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s /**/ ] with querySelector in XML '[baz='quux' s /**/ ]' is not a valid selector. -FAIL [baz='quux'/**/s/**/] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux'/**/s/**/] with querySelector in XML '[baz='quux'/**/s/**/]' is not a valid selector. -FAIL [baz=quux/**/s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz=quux/**/s] with querySelector in XML '[baz=quux/**/s]' is not a valid selector. -FAIL [baz='quux' s ] /* \t */ in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' s ] /* \t */ with querySelector in XML '[baz='quux' s ] /* \t */' is not a valid selector. -FAIL [baz='quux' -s -] /* \n */ in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' +PASS [baz='quux' s] in XML +PASS [baz='quux' s] with querySelector in XML +PASS [baz='quux' S] in XML +PASS [baz='quux' S] with querySelector in XML +PASS [baz=quux s] in XML +PASS [baz=quux s] with querySelector in XML +PASS [baz="quux" s] in XML +PASS [baz="quux" s] with querySelector in XML +PASS [baz='quux's] in XML +PASS [baz='quux's] with querySelector in XML +PASS [baz='quux's ] in XML +PASS [baz='quux's ] with querySelector in XML +PASS [baz='quux' s ] in XML +PASS [baz='quux' s ] with querySelector in XML +PASS [baz='quux' /**/ s] in XML +PASS [baz='quux' /**/ s] with querySelector in XML +PASS [baz='quux' s /**/ ] in XML +PASS [baz='quux' s /**/ ] with querySelector in XML +PASS [baz='quux'/**/s/**/] in XML +PASS [baz='quux'/**/s/**/] with querySelector in XML +PASS [baz=quux/**/s] in XML +PASS [baz=quux/**/s] with querySelector in XML +PASS [baz='quux' s ] /* \t */ in XML +PASS [baz='quux' s ] /* \t */ with querySelector in XML +PASS [baz='quux' s -] /* \n */ with querySelector in XML '[baz='quux' +] /* \n */ in XML +PASS [baz='quux' s -] /* \n */' is not a valid selector. -FAIL [baz='quux'\rs\r] /* \r */ in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux'\rs\r] /* \r */ with querySelector in XML '[baz='quux'\rs\r] /* \r */' is not a valid selector. -FAIL [baz='quux' \s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \s] with querySelector in XML '[baz='quux' \s]' is not a valid selector. -FAIL [baz='quux' \73] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \73] with querySelector in XML '[baz='quux' \73]' is not a valid selector. -FAIL [baz='quux' \53] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz='quux' \53] with querySelector in XML '[baz='quux' \53]' is not a valid selector. -FAIL [baz~='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz~='quux' s] with querySelector in XML '[baz~='quux' s]' is not a valid selector. -FAIL [baz^='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz^='quux' s] with querySelector in XML '[baz^='quux' s]' is not a valid selector. -FAIL [baz$='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz$='quux' s] with querySelector in XML '[baz$='quux' s]' is not a valid selector. -FAIL [baz*='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz*='quux' s] with querySelector in XML '[baz*='quux' s]' is not a valid selector. -FAIL [baz|='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [baz|='quux' s] with querySelector in XML '[baz|='quux' s]' is not a valid selector. -FAIL [|baz='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [|baz='quux' s] with querySelector in XML '[|baz='quux' s]' is not a valid selector. -FAIL [*|baz='quux' s] in XML assert_equals: valid rule didn't parse into CSSOM expected 1 but got 0 -FAIL [*|baz='quux' s] with querySelector in XML '[*|baz='quux' s]' is not a valid selector. +] /* \n */ with querySelector in XML +PASS [baz='quux'\rs\r] /* \r */ in XML +PASS [baz='quux'\rs\r] /* \r */ with querySelector in XML +PASS [baz='quux' \s] in XML +PASS [baz='quux' \s] with querySelector in XML +PASS [baz='quux' \73] in XML +PASS [baz='quux' \73] with querySelector in XML +PASS [baz='quux' \53] in XML +PASS [baz='quux' \53] with querySelector in XML +PASS [baz~='quux' s] in XML +PASS [baz~='quux' s] with querySelector in XML +PASS [baz^='quux' s] in XML +PASS [baz^='quux' s] with querySelector in XML +PASS [baz$='quux' s] in XML +PASS [baz$='quux' s] with querySelector in XML +PASS [baz*='quux' s] in XML +PASS [baz*='quux' s] with querySelector in XML +PASS [baz|='quux' s] in XML +PASS [baz|='quux' s] with querySelector in XML +PASS [|baz='quux' s] in XML +PASS [|baz='quux' s] with querySelector in XML +PASS [*|baz='quux' s] in XML +PASS [*|baz='quux' s] with querySelector in XML PASS [foo[ /* sanity check (invalid) */ in XML PASS [foo[ /* sanity check (invalid) */ with querySelector in XML PASS [foo='bar' i i] in XML diff --git a/Source/WebCore/css/CSSSelector.cpp b/Source/WebCore/css/CSSSelector.cpp index 9f5af04120d1..528bf7e49fe6 100644 --- a/Source/WebCore/css/CSSSelector.cpp +++ b/Source/WebCore/css/CSSSelector.cpp @@ -667,10 +667,17 @@ String CSSSelector::selectorText(StringView separator, StringView rightSide) con } if (selector->match() != Match::Set) { serializeString(builder, selector->serializingValue()); - if (selector->attributeValueMatchingIsCaseInsensitive()) - builder.append(" i]"_s); - else + switch (selector->attributeMatchType()) { + case AttributeMatchType::Default: builder.append(']'); + break; + case AttributeMatchType::CaseInsensitive: + builder.append(" i]"_s); + break; + case AttributeMatchType::CaseSensitive: + builder.append(" s]"_s); + break; + } } } else if (selector->match() == Match::PagePseudoClass) { switch (selector->pagePseudoClass()) { @@ -742,7 +749,7 @@ void CSSSelector::setAttribute(const QualifiedName& value, AttributeMatchType ma { createRareData(); m_data.rareData->attribute = value; - m_caseInsensitiveAttributeValueMatching = matchType == CaseInsensitive; + m_attributeMatchType = std::to_underlying(matchType); } void CSSSelector::setArgument(const AtomString& value) @@ -865,7 +872,7 @@ CSSSelector::CSSSelector(const CSSSelector& other) , m_hasRareData(other.m_hasRareData) , m_isForPage(other.m_isForPage) , m_tagIsForNamespaceRule(other.m_tagIsForNamespaceRule) - , m_caseInsensitiveAttributeValueMatching(other.m_caseInsensitiveAttributeValueMatching) + , m_attributeMatchType(other.m_attributeMatchType) , m_isImplicit(other.m_isImplicit) { // Manually ref count the m_data union because they are stored as raw ptr, not as Ref. @@ -1018,7 +1025,7 @@ bool CSSSelector::simpleSelectorEqual(const CSSSelector& other) const && m_pseudoType == other.m_pseudoType && m_hasRareData == other.m_hasRareData && m_tagIsForNamespaceRule == other.m_tagIsForNamespaceRule - && m_caseInsensitiveAttributeValueMatching == other.m_caseInsensitiveAttributeValueMatching + && m_attributeMatchType == other.m_attributeMatchType && m_isImplicit == other.m_isImplicit && valuesEqual(); } diff --git a/Source/WebCore/css/CSSSelector.h b/Source/WebCore/css/CSSSelector.h index 65b8c804ff9b..1c2b176474f7 100644 --- a/Source/WebCore/css/CSSSelector.h +++ b/Source/WebCore/css/CSSSelector.h @@ -122,7 +122,7 @@ class CSSSelector { Right, }; - enum AttributeMatchType { CaseSensitive, CaseInsensitive }; + enum class AttributeMatchType : uint8_t { Default, CaseInsensitive, CaseSensitive }; // Maps from the selector pseudo-element type to the style type. Only pseudo-elements that are not element-backed have a type in style. static std::optional NODELETE stylePseudoElementTypeFor(PseudoElement); @@ -154,7 +154,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END const AtomString& serializingValue() const LIFETIME_BOUND; const QualifiedName& attribute() const LIFETIME_BOUND; const AtomString& argument() const LIFETIME_BOUND { return m_hasRareData ? m_data.rareData->argument : nullAtom(); } - bool attributeValueMatchingIsCaseInsensitive() const; + AttributeMatchType attributeMatchType() const; const FixedVector* integerList() const LIFETIME_BOUND { return m_hasRareData ? std::get_if>(&m_data.rareData->argumentList) : nullptr; } const FixedVector* stringList() const LIFETIME_BOUND { return m_hasRareData ? std::get_if>(&m_data.rareData->argumentList) : nullptr; } const FixedVector* langList() const LIFETIME_BOUND { return m_hasRareData ? std::get_if>(&m_data.rareData->argumentList) : nullptr; } @@ -231,9 +231,9 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END unsigned m_hasRareData : 1 { false }; unsigned m_isForPage : 1 { false }; unsigned m_tagIsForNamespaceRule : 1 { false }; - unsigned m_caseInsensitiveAttributeValueMatching : 1 { false }; + unsigned m_attributeMatchType : 2 { std::to_underlying(AttributeMatchType::Default) }; unsigned m_isImplicit : 1 { false }; - // 25 bits + // 26 bits #if !ASSERT_WITH_SECURITY_IMPLICATION_DISABLED unsigned m_destructorHasBeenCalled : 1 { false }; #endif @@ -383,7 +383,7 @@ inline CSSSelector::CSSSelector(CSSSelector&& other) , m_hasRareData(other.m_hasRareData) , m_isForPage(other.m_isForPage) , m_tagIsForNamespaceRule(other.m_tagIsForNamespaceRule) - , m_caseInsensitiveAttributeValueMatching(other.m_caseInsensitiveAttributeValueMatching) + , m_attributeMatchType(other.m_attributeMatchType) , m_isImplicit(other.m_isImplicit) , m_data(WTF::move(other.m_data)) { @@ -445,9 +445,9 @@ inline const AtomString& CSSSelector::serializingValue() const return *reinterpret_cast(&m_data.value); } -inline bool CSSSelector::attributeValueMatchingIsCaseInsensitive() const +inline auto CSSSelector::attributeMatchType() const -> AttributeMatchType { - return m_caseInsensitiveAttributeValueMatching; + return static_cast(m_attributeMatchType); } inline auto CSSSelector::pseudoClass() const -> PseudoClass diff --git a/Source/WebCore/css/SelectorChecker.cpp b/Source/WebCore/css/SelectorChecker.cpp index fe79857a2cb4..7268afaaa969 100644 --- a/Source/WebCore/css/SelectorChecker.cpp +++ b/Source/WebCore/css/SelectorChecker.cpp @@ -695,11 +695,18 @@ bool SelectorChecker::attributeSelectorMatches(const Element& element, const Qua auto& selectorName = isHTML ? selectorAttribute.localNameLowercase() : selectorAttribute.localName(); if (!Attribute::nameMatchesFilter(attributeName, selectorAttribute.prefix(), selectorName, selectorAttribute.namespaceURI())) return false; - bool caseSensitive = true; - if (selector.attributeValueMatchingIsCaseInsensitive()) - caseSensitive = false; - else if (element.document().isHTMLDocument() && element.isHTMLElement() && !HTMLDocument::isCaseSensitiveAttribute(selector.attribute())) - caseSensitive = false; + bool caseSensitive = [&] { + switch (selector.attributeMatchType()) { + case CSSSelector::AttributeMatchType::CaseInsensitive: + return false; + case CSSSelector::AttributeMatchType::CaseSensitive: + return true; + case CSSSelector::AttributeMatchType::Default: + return !(element.document().isHTMLDocument() && element.isHTMLElement() && !HTMLDocument::isCaseSensitiveAttribute(selector.attribute())); + } + ASSERT_NOT_REACHED(); + return true; + }(); return attributeValueMatches(Attribute(attributeName, attributeValue), selector.match(), selector.value(), caseSensitive); } @@ -810,11 +817,18 @@ bool SelectorChecker::checkOne(CheckingContext& checkingContext, LocalContext& c return false; const QualifiedName& attr = selector.attribute(); - bool caseSensitive = true; - if (selector.attributeValueMatchingIsCaseInsensitive()) - caseSensitive = false; - else if (m_documentIsHTML && element->isHTMLElement() && !HTMLDocument::isCaseSensitiveAttribute(attr)) - caseSensitive = false; + bool caseSensitive = [&] { + switch (selector.attributeMatchType()) { + case CSSSelector::AttributeMatchType::CaseInsensitive: + return false; + case CSSSelector::AttributeMatchType::CaseSensitive: + return true; + case CSSSelector::AttributeMatchType::Default: + return !(m_documentIsHTML && element->isHTMLElement() && !HTMLDocument::isCaseSensitiveAttribute(attr)); + } + ASSERT_NOT_REACHED(); + return true; + }(); return anyAttributeMatches(element, selector, attr, caseSensitive); } diff --git a/Source/WebCore/css/parser/CSSSelectorParser.cpp b/Source/WebCore/css/parser/CSSSelectorParser.cpp index 12335b4d89e6..c01e1ea81326 100644 --- a/Source/WebCore/css/parser/CSSSelectorParser.cpp +++ b/Source/WebCore/css/parser/CSSSelectorParser.cpp @@ -790,7 +790,7 @@ std::unique_ptr CSSSelectorParser::consumeAttribute(CSSParse auto selector = makeUnique(); if (block.atEnd()) { - selector->setAttribute(qualifiedName, CSSSelector::CaseSensitive); + selector->setAttribute(qualifiedName, CSSSelector::AttributeMatchType::Default); selector->setMatch(CSSSelector::Match::Set); return selector; } @@ -1140,12 +1140,14 @@ CSSSelector::Match CSSSelectorParser::consumeAttributeMatch(CSSParserTokenRange& CSSSelector::AttributeMatchType CSSSelectorParser::consumeAttributeFlags(CSSParserTokenRange& range) { if (range.peek().type() != IdentToken) - return CSSSelector::CaseSensitive; + return CSSSelector::AttributeMatchType::Default; const CSSParserToken& flag = range.consumeIncludingWhitespace(); if (equalLettersIgnoringASCIICase(flag.value(), "i"_s)) - return CSSSelector::CaseInsensitive; + return CSSSelector::AttributeMatchType::CaseInsensitive; + if (equalLettersIgnoringASCIICase(flag.value(), "s"_s)) + return CSSSelector::AttributeMatchType::CaseSensitive; m_failedParsing = true; - return CSSSelector::CaseSensitive; + return CSSSelector::AttributeMatchType::Default; } // token sequences have special serialization rules: https://www.w3.org/TR/css-syntax-3/#serializing-anb diff --git a/Source/WebCore/cssjit/SelectorCompiler.cpp b/Source/WebCore/cssjit/SelectorCompiler.cpp index 62140f02a1f4..ec3d717c8271 100644 --- a/Source/WebCore/cssjit/SelectorCompiler.cpp +++ b/Source/WebCore/cssjit/SelectorCompiler.cpp @@ -387,8 +387,14 @@ static AttributeCaseSensitivity attributeSelectorCaseSensitivity(const CSSSelect if (selector.match() == CSSSelector::Match::Set) return AttributeCaseSensitivity::CaseSensitive; - if (selector.attributeValueMatchingIsCaseInsensitive()) + switch (selector.attributeMatchType()) { + case CSSSelector::AttributeMatchType::CaseInsensitive: return AttributeCaseSensitivity::CaseInsensitive; + case CSSSelector::AttributeMatchType::CaseSensitive: + return AttributeCaseSensitivity::CaseSensitive; + case CSSSelector::AttributeMatchType::Default: + break; + } if (HTMLDocument::isCaseSensitiveAttribute(selector.attribute())) return AttributeCaseSensitivity::CaseSensitive; return AttributeCaseSensitivity::HTMLLegacyCaseInsensitive; @@ -400,7 +406,7 @@ class AttributeMatchingInfo { : m_selector(&selector) , m_attributeCaseSensitivity(attributeSelectorCaseSensitivity(selector)) { - ASSERT(!(m_attributeCaseSensitivity == AttributeCaseSensitivity::CaseInsensitive && !selector.attributeValueMatchingIsCaseInsensitive())); + ASSERT(!(m_attributeCaseSensitivity == AttributeCaseSensitivity::CaseInsensitive && selector.attributeMatchType() != CSSSelector::AttributeMatchType::CaseInsensitive)); ASSERT(!(selector.match() == CSSSelector::Match::Set && m_attributeCaseSensitivity != AttributeCaseSensitivity::CaseSensitive)); } diff --git a/Source/WebCore/dom/SelectorQuery.cpp b/Source/WebCore/dom/SelectorQuery.cpp index 8eb87e174205..dcf492fd8717 100644 --- a/Source/WebCore/dom/SelectorQuery.cpp +++ b/Source/WebCore/dom/SelectorQuery.cpp @@ -84,7 +84,7 @@ template static ALWAYS_INLINE void appendOutputForElement(Outpu static bool NODELETE canBeUsedForIdFastPath(const CSSSelector& selector) { return selector.match() == CSSSelector::Match::Id - || (selector.match() == CSSSelector::Match::Exact && selector.attribute() == HTMLNames::idAttr && !selector.attributeValueMatchingIsCaseInsensitive()); + || (selector.match() == CSSSelector::Match::Exact && selector.attribute() == HTMLNames::idAttr && selector.attributeMatchType() != CSSSelector::AttributeMatchType::CaseInsensitive); } static IdMatchingType NODELETE findIdMatchingType(const CSSSelector& firstSelector) @@ -105,7 +105,7 @@ static IdMatchingType NODELETE findIdMatchingType(const CSSSelector& firstSelect static bool canOptimizeSingleAttributeExactMatch(const CSSSelector& selector) { // Bailout if attribute name needs to be definitely case-insensitive. - if (selector.attributeValueMatchingIsCaseInsensitive()) + if (selector.attributeMatchType() == CSSSelector::AttributeMatchType::CaseInsensitive) return false; const auto& attribute = selector.attribute(); From 3ece54788730b769f69815d72ecc42f0b8688223 Mon Sep 17 00:00:00 2001 From: Nikolas Zimmermann Date: Thu, 14 May 2026 04:10:18 -0700 Subject: [PATCH 009/424] [GTK][WPE] Cap wkdev-build container hostname to HOST_NAME_MAX https://bugs.webkit.org/show_bug.cgi?id=314810 Unreviewed build fix. In a Kubernetes pod the host hostname is the pod name (up to 63 chars), so blindly appending it to the container name overflows the 64-byte HOST_NAME_MAX. crun then fails to start the container with: Error: OCI runtime error: ... crun: sethostname: Invalid argument ERROR: Failed to start container 'wkdev-build'. Route the hostname through a small helper that truncates to 63 chars. * Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py: (_container_hostname): (_build_podman_create_args): Canonical link: https://commits.webkit.org/313235@main --- .../webkitpy/port/linux_container_sdk_utils.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py index fb1d41d1958a..36454e337b3f 100644 --- a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py +++ b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py @@ -179,6 +179,17 @@ def _bind_mount(src, dst, options='rslave'): return ['--mount', 'type=bind,source={},destination={},{}'.format(src, dst, options)] +def _container_hostname(): + # Linux caps hostnames at HOST_NAME_MAX (64) bytes; sethostname(2) returns + # EINVAL above that, which surfaces from crun as "sethostname: Invalid + # argument" and aborts container startup. In a Kubernetes pod the host + # hostname is already up to 63 chars, so blindly appending it overflows. + hostname = '{}.{}'.format(WKDEV_CONTAINER_NAME, socket.gethostname()) + if len(hostname) > 63: + hostname = hostname[:63] + return hostname + + def _build_podman_create_args(pinned_version): image_ref = '{}:{}'.format(WKDEV_SDK_IMAGE_REPOSITORY, pinned_version) container_home = _container_home_path() @@ -188,7 +199,7 @@ def _build_podman_create_args(pinned_version): args = [ '--name', WKDEV_CONTAINER_NAME, - '--hostname', '{}.{}'.format(WKDEV_CONTAINER_NAME, socket.gethostname()), + '--hostname', _container_hostname(), '--workdir', '/home/{}'.format(user), '--userns', 'keep-id', '--user', 'root:root', From 21accbddc7448de44ff42643b7a2c0bf8aac968b Mon Sep 17 00:00:00 2001 From: Dan Glastonbury Date: Thu, 14 May 2026 04:23:28 -0700 Subject: [PATCH 010/424] Cache CGColorTransform per thread https://bugs.webkit.org/show_bug.cgi?id=314802 rdar://177049985 Reviewed by Gerald Squelart. In 313211@main, platformConvertColorComponents() was changed to hold a cached CGColorTransform that is never released. This commit changes the cached CGColorTransform into a ThreadSpecific, so that the transform is released when the thread is destroyed. * Source/WebCore/platform/graphics/cg/ColorCG.cpp: (WebCore::platformConvertColorComponents): Canonical link: https://commits.webkit.org/313236@main --- Source/WebCore/platform/graphics/cg/ColorCG.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/WebCore/platform/graphics/cg/ColorCG.cpp b/Source/WebCore/platform/graphics/cg/ColorCG.cpp index cd9ff9d1dcc4..72cdd93d3cba 100644 --- a/Source/WebCore/platform/graphics/cg/ColorCG.cpp +++ b/Source/WebCore/platform/graphics/cg/ColorCG.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include namespace WebCore { @@ -252,8 +253,8 @@ ColorComponents platformConvertColorComponents(ColorSpace inputColorSp std::array sourceComponents { c1, c2, c3, c4 }; std::array destinationComponents { }; - static NeverDestroyed, RetainPtr>> cachedTransform; - auto& [cachedSpace, transform] = cachedTransform.get(); + static NeverDestroyed, RetainPtr>>> cachedTransform; + auto& [cachedSpace, transform] = *cachedTransform.get(); RetainPtr outputColorSpacePlatform = outputColorSpace.platformColorSpace(); if (!transform || !CGColorSpaceEqualToColorSpace(cachedSpace.get(), outputColorSpacePlatform.get())) { cachedSpace = WTF::move(outputColorSpacePlatform); From 6a1ee35b7d0b7825cdf12211e6659c7399da39b9 Mon Sep 17 00:00:00 2001 From: Vitaly Dyachkov Date: Thu, 14 May 2026 04:25:37 -0700 Subject: [PATCH 011/424] [WPE][GTK] `fast/repaint/table-row-move-after-content-change.html` is a flaky image failure https://bugs.webkit.org/show_bug.cgi?id=314809 Unreviewed test gardening. * LayoutTests/platform/glib/TestExpectations: Canonical link: https://commits.webkit.org/313237@main --- LayoutTests/platform/glib/TestExpectations | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LayoutTests/platform/glib/TestExpectations b/LayoutTests/platform/glib/TestExpectations index bdd8225d12ef..792d553e182a 100644 --- a/LayoutTests/platform/glib/TestExpectations +++ b/LayoutTests/platform/glib/TestExpectations @@ -5564,6 +5564,8 @@ webkit.org/b/314373 media/media-source/media-source-real-webm-remove.html [ Fail # test timeout, skip it for now webkit.org/b/314643 media/media-source/media-source-duration-truncation-ended.html [ Skip ] +webkit.org/b/314809 fast/repaint/table-row-move-after-content-change.html [ Pass ImageOnlyFailure ] + # End: Common failures between GTK and WPE. #//////////////////////////////////////////////////////////////////////////////////////// From 84ce5566711f858e4caf80b6aeeb7d01984c52a8 Mon Sep 17 00:00:00 2001 From: Vitaly Dyachkov Date: Thu, 14 May 2026 04:40:43 -0700 Subject: [PATCH 012/424] [WPE] `imported/w3c/web-platform-tests/css/mediaqueries/test_media_queries.html` is a constant failure https://bugs.webkit.org/show_bug.cgi?id=314811 Unreviewed test gardening. * LayoutTests/platform/wpe/TestExpectations: Canonical link: https://commits.webkit.org/313238@main --- LayoutTests/platform/wpe/TestExpectations | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LayoutTests/platform/wpe/TestExpectations b/LayoutTests/platform/wpe/TestExpectations index 74fbe08711d2..0b21a3b8e600 100644 --- a/LayoutTests/platform/wpe/TestExpectations +++ b/LayoutTests/platform/wpe/TestExpectations @@ -1328,3 +1328,5 @@ webkit.org/b/313112 [ arm64 ] webrtc/video-mute.html [ Pass Failure Timeout ] imported/w3c/web-platform-tests/css/css-scroll-snap/scroll-target-margin-005.html [ Failure ] webkit.org/b/314218 imported/w3c/web-platform-tests/html/semantics/embedded-content/media-elements/track/track-element/track-cues-enter-seeking.html [ Failure Pass ] + +webkit.org/b/314811 imported/w3c/web-platform-tests/css/mediaqueries/test_media_queries.html [ Failure ] From 6053a426c6d4dae46ef4eb5989d040084f0f4a47 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 14 May 2026 06:48:28 -0700 Subject: [PATCH 013/424] Nesting requestClose() fires multiple cancel events https://bugs.webkit.org/show_bug.cgi?id=311741 Reviewed by Anne van Kesteren. Adds a flag to dialog elements that is set when calling requestClose(), this causes an early return if already set so that authors can't accidentally create a RangeError due to looping. * LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html: Added. * Source/WebCore/html/HTMLDialogElement.cpp: (WebCore::HTMLDialogElement::requestClose): * Source/WebCore/html/HTMLDialogElement.h: Canonical link: https://commits.webkit.org/313239@main --- .../dialog-requestclose-recurse-expected.txt | 3 +++ .../dialog-requestclose-recurse.html | 24 +++++++++++++++++++ Source/WebCore/html/HTMLDialogElement.cpp | 9 +++++++ Source/WebCore/html/HTMLDialogElement.h | 1 + 4 files changed, 37 insertions(+) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse-expected.txt create mode 100644 LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse-expected.txt new file mode 100644 index 000000000000..f6a43935aa26 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse-expected.txt @@ -0,0 +1,3 @@ + +PASS Calling requestClose inside of cancel event handler doesn't trigger another cancel event + diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html new file mode 100644 index 000000000000..672b00a0fafe --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html @@ -0,0 +1,24 @@ + + + +dialog: requestClose while cancelling + + + + +Dialog + + diff --git a/Source/WebCore/html/HTMLDialogElement.cpp b/Source/WebCore/html/HTMLDialogElement.cpp index 3056979e6c1d..2692720db996 100644 --- a/Source/WebCore/html/HTMLDialogElement.cpp +++ b/Source/WebCore/html/HTMLDialogElement.cpp @@ -246,10 +246,19 @@ void HTMLDialogElement::requestClose(const String& returnValue, Element* source) if (!isOpen()) return; + // FIXME(311746): Add missing prelimanary checks that should prevent this function running. + + if (m_isRequestingToClose) + return; + + m_isRequestingToClose = true; + Ref cancelEvent = Event::create(eventNames().cancelEvent, Event::CanBubble::No, Event::IsCancelable::Yes); dispatchEvent(cancelEvent); if (!cancelEvent->defaultPrevented()) close(returnValue, source); + + m_isRequestingToClose = false; } bool HTMLDialogElement::isValidCommandType(const CommandType command) diff --git a/Source/WebCore/html/HTMLDialogElement.h b/Source/WebCore/html/HTMLDialogElement.h index 2e5a6ba67b2a..59b2290b03c0 100644 --- a/Source/WebCore/html/HTMLDialogElement.h +++ b/Source/WebCore/html/HTMLDialogElement.h @@ -86,6 +86,7 @@ class HTMLDialogElement final : public HTMLElement { String m_returnValue; bool m_isModal { false }; bool m_isOpen { false }; + bool m_isRequestingToClose { false }; WeakPtr m_previouslyFocusedElement; RefPtr m_toggleEventTask; From 0a1c4dbcb3479e936b324d106225fe92f6ba4ffe Mon Sep 17 00:00:00 2001 From: Vitaly Dyachkov Date: Thu, 14 May 2026 06:52:39 -0700 Subject: [PATCH 014/424] REGRESSION(312965@main) [GTK] `imported/w3c/web-platform-tests/svg/geometry/reftests/circle-004.svg` is an image failure https://bugs.webkit.org/show_bug.cgi?id=314817 Unreviewed test gardening. * LayoutTests/platform/gtk/TestExpectations: Canonical link: https://commits.webkit.org/313240@main --- LayoutTests/platform/gtk/TestExpectations | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LayoutTests/platform/gtk/TestExpectations b/LayoutTests/platform/gtk/TestExpectations index b58d3f5825e4..9831d845b822 100644 --- a/LayoutTests/platform/gtk/TestExpectations +++ b/LayoutTests/platform/gtk/TestExpectations @@ -1456,4 +1456,6 @@ webkit.org/b/313649 editing/pasteboard/reveal-selection-after-pasting-images.htm webkit.org/b/313985 fullscreen/fullscreen-clears-hover-on-overlay.html [ Failure Pass ] -webkit.org/b/303859 imported/w3c/web-platform-tests/html/canvas/element/manual/text/canvas.2d.disconnected.html [ Pass ImageOnlyFailure ] \ No newline at end of file +webkit.org/b/303859 imported/w3c/web-platform-tests/html/canvas/element/manual/text/canvas.2d.disconnected.html [ Pass ImageOnlyFailure ] + +webkit.org/b/314817 imported/w3c/web-platform-tests/svg/geometry/reftests/circle-004.svg [ ImageOnlyFailure ] From e30fb3e304f3ec7eefa43a791d52f3ee2bc91676 Mon Sep 17 00:00:00 2001 From: Tyler Wilcock Date: Thu, 14 May 2026 07:06:32 -0700 Subject: [PATCH 015/424] AX: nodeChangeForObject overwrites childrenIDs in the nodeMap, sometimes causing content to be missing in the isolated tree https://bugs.webkit.org/show_bug.cgi?id=314773 rdar://176891627 Reviewed by Dominic Mazzoni. With this sequence, content can become missing from the isolated tree: 1. An attribute change (e.g. role) on parent X is posted as a notification into AXObjectCache's m_notificationsToPost, which schedules notificationPostTimer. 2. X's children are dynamically replaced (innerHTML, etc.), so live children differ from what's stored in m_nodeMap[X].childrenIDs. The children-changed events sit in AXObjectCache's m_deferredChildrenChangedList waiting for performDeferredCacheUpdate to drain them into AXIsolatedTree's m_needsUpdateChildren. 3. performCacheUpdate fires before notificationPostTimer, but bails because layout is dirty. m_deferredChildrenChangedList stays untouched, so AXIsolatedTree's m_needsUpdateChildren stays empty for X. 4. notificationPostTimer fires. It (a) queues a full node update for X into m_needsUpdateNode via updateIsolatedTree, and (b) calls processQueuedIsolatedNodeUpdates synchronously inside postPlatformNotification (so the AT sees an up-to-date tree when it responds to the notification). 5. processQueuedNodeUpdates runs with m_needsUpdateNode={X} and m_needsUpdateChildren={} (still empty from step 3). It calls resolveAppends -> nodeChangeForObject(X), which sees live children = NEW and stored children = OLD, and overwrites m_nodeMap[X].childrenIDs with the NEW IDs without registering any of them in m_nodeMap. 6. Later, performCacheUpdate runs successfully and queues updateChildren(X) into m_needsUpdateChildren. The snapshot timer fires processQueuedNodeUpdates, which calls updateChildren(X). It reads oldChildrenIDs from the polluted m_nodeMap (= NEW IDs) and compares against newChildrenIDs from live (= the same NEW IDs). old == new, so it never calls collectNodeChangesForSubtree on the new children, and thus they never get isolated objects created for them. 7. AXIsolatedObject::children resolves the stored childrenIDs against tree().objectForID(...). The unregistered NEW IDs return null and get silently dropped via WTF::compactMap, leaving them invisible to AT clients. Fix this in the same way we fixed a similar problem for collectNodeChangesForSubtree in https://commits.webkit.org/285352@main. If the object is already in the nodemap, we leave the existing childrenIDs alone. Arguably we should do this even if the object isn't in the nodemap, but rethinking this is saved for a later commit since this change alone fixes the bug on the webpage. * LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children-expected.txt: Added. * LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children.html: Added. * Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp: (WebCore::AXIsolatedTree::nodeChangeForObject): Canonical link: https://commits.webkit.org/313241@main --- ...nt-with-stale-parent-children-expected.txt | 13 ++++ ...eplacement-with-stale-parent-children.html | 66 +++++++++++++++++++ .../isolatedtree/AXIsolatedTree.cpp | 11 +++- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children-expected.txt create mode 100644 LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children.html diff --git a/LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children-expected.txt b/LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children-expected.txt new file mode 100644 index 000000000000..064a1103d0d3 --- /dev/null +++ b/LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children-expected.txt @@ -0,0 +1,13 @@ +Tests that all newly-added children of a container are reachable in the AX tree after the contents are dynamically replaced. + +Initial state: +PASS: container.childrenCount === 2 + +After swap: +PASS: container.childrenCount === 13 +PASS: reachableButtons === buttonCount + +PASS successfullyParsed is true + +TEST COMPLETE +Q1Q2Q3Q4Q5Q6Q7Q8Q9Q10Q11Q12Next diff --git a/LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children.html b/LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children.html new file mode 100644 index 000000000000..398aa0f7a100 --- /dev/null +++ b/LayoutTests/accessibility/dynamic-subtree-replacement-with-stale-parent-children.html @@ -0,0 +1,66 @@ + + + + + + + + +
+ + +
+ + + + diff --git a/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp b/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp index c8ec97ee9829..656b707f501b 100644 --- a/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp +++ b/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp @@ -343,7 +343,16 @@ std::optional AXIsolatedTree::nodeChangeForObject(Re AX_ASSERT(axObject->wrapper()); auto data = createIsolatedObjectData(axObject, *this); Markable parentID = data.parentID; - m_nodeMap.set(axObject->objectID(), ParentChildrenIDs { parentID, data.childrenIDs }); + auto iterator = m_nodeMap.find(axObject->objectID()); + if (iterator == m_nodeMap.end()) + m_nodeMap.set(axObject->objectID(), ParentChildrenIDs { parentID, data.childrenIDs }); + else { + // We don't want to update the childrenIDs for an existing object, as |updateChildren| relies on + // knowing what children have changed (which are new, which are old) to decide what objects to create + // and destroy. If we were to update childrenIDs here, |updateChildren| would either fail to create + // objects for new children, and / or leak old ones. |collectNodeChangesForSubtree| has the same behavior. + iterator->value.parentID = parentID; + } NodeChange nodeChange { WTF::move(data), axObject->wrapper() }; if (axObject->isRoot()) From e9ddfb6d1b2b8461132171014ae14c83bcfc3e14 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Lopez Perez Date: Thu, 14 May 2026 07:49:08 -0700 Subject: [PATCH 016/424] [GTK][WPE][Tools] layout tests are failing when using the auto-enter WebKit Container SDK environment https://bugs.webkit.org/show_bug.cgi?id=314819 Reviewed by Nikolas Zimmermann. The http server used on the layout test is unable to start with the new auto-enter WebKit Container SDK environment because the tooling is failing to resolve the username. This is because the tooling is trying to resolve the username from the environment, and the USER or USERNAME env variables are not forwarded into it. Fix this by ensuring this variables are forwarded, can be useful for other things, and also fix apache_http_server.py to call getuser() when those are not set. * Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py: (LayoutTestApacheHttpd.__init__): * Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py: Canonical link: https://commits.webkit.org/313242@main --- .../webkitpy/layout_tests/servers/apache_http_server.py | 3 ++- Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py b/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py index f33aeb0c8a0d..62748ce4d527 100644 --- a/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py +++ b/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py @@ -35,6 +35,7 @@ import re import socket +from getpass import getuser from webkitpy.common.iteration_compatibility import iteritems from webkitpy.layout_tests.servers import http_server_base @@ -108,7 +109,7 @@ def __init__(self, port_obj, output_dir, additional_dirs=None, port=None): start_cmd.extend(['-c', 'Alias %s "%s"' % (alias, path)]) if not port_obj.host.platform.is_win(): - start_cmd.extend(['-C', 'User "%s"' % os.environ.get("USERNAME", os.environ.get("USER", ""))]) + start_cmd.extend(['-C', 'User "%s"' % os.environ.get("USERNAME", os.environ.get("USER", getuser()))]) enable_ipv6 = self._port_obj.http_server_supports_ipv6() # Perform part of the checks Apache's APR does when trying to listen to diff --git a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py index 36454e337b3f..95604b59f568 100644 --- a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py +++ b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py @@ -65,6 +65,8 @@ 'RESULTS_SERVER_API_KEY', 'SSLKEYLOGFILE', 'TERM', + 'USER', + 'USERNAME', 'XDG_SESSION_TYPE', 'XR_RUNTIME_JSON', )) From 26970a5ac7ec026b06e9da87ff75bc72ce997fcd Mon Sep 17 00:00:00 2001 From: Wenson Hsieh Date: Thu, 14 May 2026 07:50:59 -0700 Subject: [PATCH 017/424] Web process sometimes crashes with excessive stack depth in TextExtraction::extractRecursive https://bugs.webkit.org/show_bug.cgi?id=314772 rdar://177009329 Reviewed by Abrar Rahman Protyasha. Add a couple of mitigations to prevent crashes due to excessive stack depth: - Avoid ever re-traversing the same element (which is theoretically possible if layout is triggered in the middle of recursive extraction). - Enforce a maximum traversal depth and bail early if the stack grows too deep. Test: fast/text-extraction/max-recursion-depth-cap.html * LayoutTests/fast/text-extraction/max-recursion-depth-cap-expected.html: Added. * LayoutTests/fast/text-extraction/max-recursion-depth-cap.html: Added. * Source/WebCore/page/text-extraction/TextExtraction.cpp: (WebCore::TextExtraction::extractRecursive): Canonical link: https://commits.webkit.org/313243@main --- .../max-recursion-depth-cap-expected.html | 28 +++++++++++++++++++ .../max-recursion-depth-cap.html | 28 +++++++++++++++++++ .../page/text-extraction/TextExtraction.cpp | 19 +++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 LayoutTests/fast/text-extraction/max-recursion-depth-cap-expected.html create mode 100644 LayoutTests/fast/text-extraction/max-recursion-depth-cap.html diff --git a/LayoutTests/fast/text-extraction/max-recursion-depth-cap-expected.html b/LayoutTests/fast/text-extraction/max-recursion-depth-cap-expected.html new file mode 100644 index 000000000000..6e79f371cf8f --- /dev/null +++ b/LayoutTests/fast/text-extraction/max-recursion-depth-cap-expected.html @@ -0,0 +1,28 @@ + + + + + + + + +
+ + + diff --git a/LayoutTests/fast/text-extraction/max-recursion-depth-cap.html b/LayoutTests/fast/text-extraction/max-recursion-depth-cap.html new file mode 100644 index 000000000000..cb589afb1598 --- /dev/null +++ b/LayoutTests/fast/text-extraction/max-recursion-depth-cap.html @@ -0,0 +1,28 @@ + + + + + + + + +
+ + + diff --git a/Source/WebCore/page/text-extraction/TextExtraction.cpp b/Source/WebCore/page/text-extraction/TextExtraction.cpp index b2a361fead98..90a2e8b11736 100644 --- a/Source/WebCore/page/text-extraction/TextExtraction.cpp +++ b/Source/WebCore/page/text-extraction/TextExtraction.cpp @@ -270,6 +270,8 @@ static void addBoxShadowIfNeeded(Node& node, const String& colorAsString) using ClientNodeAttributesMap = WeakHashMap, WeakPtrImplWithEventTargetData>; +static constexpr unsigned maxExtractionRecursionDepth = 255; + struct TraversalContext { const Request originalRequest; const ClientNodeAttributesMap clientNodeAttributes; @@ -280,7 +282,9 @@ struct TraversalContext { Vector> enclosingBlocks; WeakHashMap enclosingBlockNumberMap; WeakHashSet additionalContainersToCollect; + WeakHashSet visitedContainers; unsigned inAdditionalContainerToCollectCount { 0 }; + unsigned depth { 0 }; Vector hasOverflowItemsStack; Vector visualBlockContainerStack { 0 }; unsigned nextVisualBlockContainerNumber { 1 }; @@ -859,9 +863,22 @@ static bool isVisuallyDistinctContainer(const RenderStyle& style, const FloatRec static inline void extractRecursive(Node& node, Item& parentItem, TraversalContext& context) { + if (context.depth >= maxExtractionRecursionDepth) + return; + if (context.nodesToSkip.contains(node)) return; + if (RefPtr container = dynamicDowncast(node)) { + if (!context.visitedContainers.add(*container).isNewEntry) + return; + } + + ++context.depth; + auto depthScope = makeScopeExit([&] { + --context.depth; + }); + bool isBlock = WebCore::isBlock(node); if (isBlock) context.pushEnclosingBlock(node); @@ -1382,7 +1399,9 @@ Result extractItem(Request&& request, LocalFrame& frame) .enclosingBlocks = { }, .enclosingBlockNumberMap = { }, .additionalContainersToCollect = WTF::move(additionalContainersToCollect), + .visitedContainers = { }, .inAdditionalContainerToCollectCount = 0, + .depth = 0, .hasOverflowItemsStack = { false }, .onlyCollectTextAndLinksCount = 0, .mergeParagraphs = request.mergeParagraphs, From ccc4b106b593192c92c3432bb1acdfae7cd3a7b2 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 14 May 2026 08:06:12 -0700 Subject: [PATCH 018/424] Re-import close watcher WPTs https://bugs.webkit.org/show_bug.cgi?id=314735 Reviewed by Anne van Kesteren. Upstream commit: https://github.com/web-platform-tests/wpt/commit/ddfb74862dc00e3211cb90efd34819349153694b * LayoutTests/TestExpectations: * LayoutTests/imported/w3c/web-platform-tests/close-watcher/META.yml: * LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html: Added. * LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html: Added. * LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html: Added. * LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/dialog-prevents-close.html: Added. * LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/w3c-import.log: Added. * LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/w3c-import.log: Added. * LayoutTests/tests-options.json: Canonical link: https://commits.webkit.org/313244@main --- LayoutTests/TestExpectations | 3 + .../web-platform-tests/close-watcher/META.yml | 3 +- .../iframes/dialog-same-origin-nn.html | 48 ++++++++++++++ .../iframes/dialog-same-origin-ynn.html | 55 ++++++++++++++++ .../iframes/dialog-same-origin-ynyn.html | 62 +++++++++++++++++++ .../resources/dialog-prevents-close.html | 21 +++++++ .../iframes/resources/w3c-import.log | 17 +++++ .../close-watcher/iframes/w3c-import.log | 19 ++++++ LayoutTests/tests-options.json | 9 +++ 9 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/dialog-prevents-close.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/w3c-import.log create mode 100644 LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/w3c-import.log diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations index b83e87b343b4..50f77f862efa 100644 --- a/LayoutTests/TestExpectations +++ b/LayoutTests/TestExpectations @@ -5981,6 +5981,9 @@ webkit.org/b/263305 imported/w3c/web-platform-tests/close-watcher/user-activatio webkit.org/b/263305 imported/w3c/web-platform-tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html [ Skip ] webkit.org/b/263305 imported/w3c/web-platform-tests/close-watcher/user-activation/yyy-popovers.html [ Skip ] webkit.org/b/263305 imported/w3c/web-platform-tests/close-watcher/user-activation/yyy.html?dialog [ Skip ] +webkit.org/b/263305 imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html [ Skip ] +webkit.org/b/263305 imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html [ Skip ] +webkit.org/b/263305 imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html [ Skip ] # Needs moveBefore support webkit.org/b/281223 imported/w3c/web-platform-tests/dom/nodes/moveBefore/focus-preserve-render.html [ Skip ] diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/META.yml b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/META.yml index 4534ab8abe24..f08fb7d1559e 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/META.yml +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/META.yml @@ -1,4 +1,3 @@ -spec: https://wicg.github.io/close-watcher/ +spec: https://html.spec.whatwg.org/multipage/#close-requests-and-close-watchers suggested_reviewers: - - domenic - natechapin diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html new file mode 100644 index 000000000000..b2da2d415a83 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html new file mode 100644 index 000000000000..3ab5b758684d --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html new file mode 100644 index 000000000000..e486fcb36828 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/dialog-prevents-close.html b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/dialog-prevents-close.html new file mode 100644 index 000000000000..580e5220dae2 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/dialog-prevents-close.html @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/w3c-import.log b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/w3c-import.log new file mode 100644 index 000000000000..15f8dca2d507 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/w3c-import.log @@ -0,0 +1,17 @@ +The tests in this directory were imported from the W3C repository. +Do NOT modify these tests directly in WebKit. +Instead, create a pull request on the WPT github: + https://github.com/web-platform-tests/wpt + +Then run the Tools/Scripts/import-w3c-tests in WebKit to reimport + +Do NOT modify or remove this file. + +------------------------------------------------------------------------ +Properties requiring vendor prefixes: +None +Property values requiring vendor prefixes: +None +------------------------------------------------------------------------ +List of files: +/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/resources/dialog-prevents-close.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/w3c-import.log b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/w3c-import.log new file mode 100644 index 000000000000..d80bdad9efa4 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/w3c-import.log @@ -0,0 +1,19 @@ +The tests in this directory were imported from the W3C repository. +Do NOT modify these tests directly in WebKit. +Instead, create a pull request on the WPT github: + https://github.com/web-platform-tests/wpt + +Then run the Tools/Scripts/import-w3c-tests in WebKit to reimport + +Do NOT modify or remove this file. + +------------------------------------------------------------------------ +Properties requiring vendor prefixes: +None +Property values requiring vendor prefixes: +None +------------------------------------------------------------------------ +List of files: +/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html +/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html +/LayoutTests/imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html diff --git a/LayoutTests/tests-options.json b/LayoutTests/tests-options.json index 304add0b511c..dedce7a77e6d 100644 --- a/LayoutTests/tests-options.json +++ b/LayoutTests/tests-options.json @@ -845,6 +845,15 @@ "imported/w3c/web-platform-tests/clipboard-apis/async-idlharness.https.html": [ "slow" ], + "imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-nn.html": [ + "slow" + ], + "imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynn.html": [ + "slow" + ], + "imported/w3c/web-platform-tests/close-watcher/iframes/dialog-same-origin-ynyn.html": [ + "slow" + ], "imported/w3c/web-platform-tests/content-security-policy/embedded-enforcement/required_csp-header-crlf.html": [ "slow" ], From 85bcefbc014cc44c0bd51343edf0cb805260989e Mon Sep 17 00:00:00 2001 From: Claudio Saavedra Date: Thu, 14 May 2026 08:19:59 -0700 Subject: [PATCH 019/424] [JSC] Fix a few more missing includes https://bugs.webkit.org/show_bug.cgi?id=314821 Unreviewed. * Source/JavaScriptCore/heap/ConservativeRoots.cpp: * Source/WebCore/bindings/js/JSDOMConvertVariadic.h: Canonical link: https://commits.webkit.org/313245@main --- Source/JavaScriptCore/heap/ConservativeRoots.cpp | 1 + Source/WebCore/bindings/js/JSDOMConvertVariadic.h | 1 + 2 files changed, 2 insertions(+) diff --git a/Source/JavaScriptCore/heap/ConservativeRoots.cpp b/Source/JavaScriptCore/heap/ConservativeRoots.cpp index 5599f50321d8..f27eeb3a0a8f 100644 --- a/Source/JavaScriptCore/heap/ConservativeRoots.cpp +++ b/Source/JavaScriptCore/heap/ConservativeRoots.cpp @@ -31,6 +31,7 @@ #include "CodeBlockSetInlines.h" #include "JITStubRoutineSet.h" #include "JSCast.h" +#include "MarkedBlockInlines.h" #include "WasmCallee.h" #include diff --git a/Source/WebCore/bindings/js/JSDOMConvertVariadic.h b/Source/WebCore/bindings/js/JSDOMConvertVariadic.h index c33adb686208..837ed992c9c4 100644 --- a/Source/WebCore/bindings/js/JSDOMConvertVariadic.h +++ b/Source/WebCore/bindings/js/JSDOMConvertVariadic.h @@ -27,6 +27,7 @@ #include "IDLTypes.h" #include "JSDOMConvertBase.h" +#include #include namespace WebCore { From e84a0a95fef2b18f8f776653e9288fd67fdde423 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 14 May 2026 08:22:04 -0700 Subject: [PATCH 020/424] CloseWatcher requestClose() requires history action activation https://bugs.webkit.org/show_bug.cgi?id=287873 Reviewed by Anne van Kesteren. The specification no longer requires history action activation for close watcher cancel events to be cancelable, when triggered via JS. This patch removes that restriction for the requestClose() JS function. * LayoutTests/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/close-watcher/inside-event-listeners-expected.txt: * LayoutTests/platform/ios/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt: * Source/WebCore/html/closewatcher/CloseWatcher.cpp: (WebCore::CloseWatcher::requestClose): (WebCore::CloseWatcher::requestToClose): * Source/WebCore/html/closewatcher/CloseWatcher.h: * Source/WebCore/html/closewatcher/CloseWatcherManager.cpp: (WebCore::CloseWatcherManager::escapeKeyHandler): Canonical link: https://commits.webkit.org/313246@main --- .../w3c/web-platform-tests/close-watcher/basic-expected.txt | 4 ++-- .../close-watcher/inside-event-listeners-expected.txt | 6 +++--- .../w3c/web-platform-tests/close-watcher/basic-expected.txt | 4 ++-- Source/WebCore/html/closewatcher/CloseWatcher.cpp | 6 +++--- Source/WebCore/html/closewatcher/CloseWatcher.h | 4 +++- Source/WebCore/html/closewatcher/CloseWatcherManager.cpp | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt index 39527e678c20..9250c57cb776 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt @@ -1,8 +1,8 @@ -FAIL requestClose() with no user activation assert_array_equals: expected property 0 to be "cancel[cancelable=true]" but got "cancel[cancelable=false]" (expected array ["cancel[cancelable=true]", "close"] got ["cancel[cancelable=false]", "close"]) +PASS requestClose() with no user activation PASS destroy() then requestClose() PASS close() then requestClose() -FAIL requestClose() then destroy() assert_array_equals: expected property 0 to be "cancel[cancelable=true]" but got "cancel[cancelable=false]" (expected array ["cancel[cancelable=true]", "close"] got ["cancel[cancelable=false]", "close"]) +PASS requestClose() then destroy() PASS close() then destroy() PASS destroy() then close request PASS Close request then destroy() diff --git a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/inside-event-listeners-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/inside-event-listeners-expected.txt index 6c7d449c3629..a52199c44971 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/close-watcher/inside-event-listeners-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/close-watcher/inside-event-listeners-expected.txt @@ -1,8 +1,8 @@ PASS destroy() inside oncancel -FAIL destroy() inside onclose assert_array_equals: expected property 0 to be "cancel[cancelable=true]" but got "cancel[cancelable=false]" (expected array ["cancel[cancelable=true]", "close"] got ["cancel[cancelable=false]", "close"]) +PASS destroy() inside onclose PASS close() inside oncancel -FAIL close() inside onclose assert_array_equals: expected property 0 to be "cancel[cancelable=true]" but got "cancel[cancelable=false]" (expected array ["cancel[cancelable=true]", "close"] got ["cancel[cancelable=false]", "close"]) +PASS close() inside onclose PASS requestClose() inside oncancel -FAIL requestClose() inside onclose assert_array_equals: expected property 0 to be "cancel[cancelable=true]" but got "cancel[cancelable=false]" (expected array ["cancel[cancelable=true]", "close"] got ["cancel[cancelable=false]", "close"]) +PASS requestClose() inside onclose diff --git a/LayoutTests/platform/ios/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt b/LayoutTests/platform/ios/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt index 93ea032f86fb..423ac6078cd4 100644 --- a/LayoutTests/platform/ios/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt +++ b/LayoutTests/platform/ios/imported/w3c/web-platform-tests/close-watcher/basic-expected.txt @@ -1,8 +1,8 @@ -FAIL requestClose() with no user activation assert_array_equals: expected property 0 to be "cancel[cancelable=true]" but got "cancel[cancelable=false]" (expected array ["cancel[cancelable=true]", "close"] got ["cancel[cancelable=false]", "close"]) +PASS requestClose() with no user activation PASS destroy() then requestClose() PASS close() then requestClose() -FAIL requestClose() then destroy() assert_array_equals: expected property 0 to be "cancel[cancelable=true]" but got "cancel[cancelable=false]" (expected array ["cancel[cancelable=true]", "close"] got ["cancel[cancelable=false]", "close"]) +PASS requestClose() then destroy() PASS close() then destroy() PASS destroy() then close request FAIL Close request then destroy() assert_array_equals: lengths differ, expected array ["cancel[cancelable=false]", "close"] length 2, got [] length 0 diff --git a/Source/WebCore/html/closewatcher/CloseWatcher.cpp b/Source/WebCore/html/closewatcher/CloseWatcher.cpp index fddaa092c67f..c9b919bbb007 100644 --- a/Source/WebCore/html/closewatcher/CloseWatcher.cpp +++ b/Source/WebCore/html/closewatcher/CloseWatcher.cpp @@ -93,17 +93,17 @@ ScriptExecutionContext* CloseWatcher::scriptExecutionContext() const void CloseWatcher::requestClose() { - requestToClose(); + requestToClose(RequireHistoryActionActivation::No); } -bool CloseWatcher::requestToClose() +bool CloseWatcher::requestToClose(RequireHistoryActionActivation requireHistoryActionActivation) { if (!canBeClosed()) return true; RefPtr document = dynamicDowncast(scriptExecutionContext()); Ref manager = protect(document->window())->closeWatcherManager(); - bool canPreventClose = manager->canPreventClose() && document->window()->hasHistoryActionActivation(); + bool canPreventClose = requireHistoryActionActivation == RequireHistoryActionActivation::No || (manager->canPreventClose() && document->window()->hasHistoryActionActivation()); Ref cancelEvent = Event::create(eventNames().cancelEvent, Event::CanBubble::No, canPreventClose ? Event::IsCancelable::Yes : Event::IsCancelable::No); m_isRunningCancelAction = true; dispatchEvent(cancelEvent); diff --git a/Source/WebCore/html/closewatcher/CloseWatcher.h b/Source/WebCore/html/closewatcher/CloseWatcher.h index 7046fc66560f..dd349a036b78 100644 --- a/Source/WebCore/html/closewatcher/CloseWatcher.h +++ b/Source/WebCore/html/closewatcher/CloseWatcher.h @@ -37,6 +37,8 @@ namespace WebCore { class KeyboardEvent; +enum class RequireHistoryActionActivation : bool { No, Yes }; + class CloseWatcher final : public RefCounted, public EventTarget, public ActiveDOMObject { WTF_MAKE_TZONE_ALLOCATED(CloseWatcher); public: @@ -51,7 +53,7 @@ class CloseWatcher final : public RefCounted, public EventTarget, bool isActive() const { return m_active; } void requestClose(); - bool requestToClose(); + bool requestToClose(RequireHistoryActionActivation); void close(); void destroy(); diff --git a/Source/WebCore/html/closewatcher/CloseWatcherManager.cpp b/Source/WebCore/html/closewatcher/CloseWatcherManager.cpp index f49962c3a403..a86cafe09239 100644 --- a/Source/WebCore/html/closewatcher/CloseWatcherManager.cpp +++ b/Source/WebCore/html/closewatcher/CloseWatcherManager.cpp @@ -80,7 +80,7 @@ void CloseWatcherManager::escapeKeyHandler(KeyboardEvent& event) auto& group = m_groups.last(); Vector> groupCopy(group); for (Ref watcher : groupCopy | std::views::reverse) { - if (!watcher->requestToClose()) + if (!watcher->requestToClose(RequireHistoryActionActivation::Yes)) break; } } From 85015722cb650d16b85f0010b9e8ccb910c02cd4 Mon Sep 17 00:00:00 2001 From: Geoffrey Garen Date: Thu, 14 May 2026 08:34:18 -0700 Subject: [PATCH 021/424] Fixed a stray -Wunsafe-buffer-usage pragma that accidentally disabled the check https://bugs.webkit.org/show_bug.cgi?id=314771 rdar://177022310 Reviewed by Chris Dumez. * Source/JavaScriptCore/llint/LLIntData.h: Don't WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN because we're already in a WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN, and since they're a stack, being unbalanced will skip all warnings in the code that follows. * Source/JavaScriptCore/runtime/JSLock.cpp: Skip enforcement for now because this code never conformed before. * Source/JavaScriptCore/runtime/VM.cpp: Adopt dataLogLn to avoid -Wunsafe-buffer-usage-in-format-attr-call now that the warning fires here. * Source/JavaScriptCore/wasm/js/WebAssemblySuspending.cpp: (JSC::runWebAssemblySuspendingFunction): Adopt unsafeMakeSpan because NUMBER_OF_CALLEE_SAVES_REGISTERS matches the spirit of a "safe" unsafe construction. Canonical link: https://commits.webkit.org/313247@main --- Source/JavaScriptCore/llint/LLIntData.h | 2 -- Source/JavaScriptCore/runtime/JSLock.cpp | 2 ++ Source/JavaScriptCore/runtime/VM.cpp | 12 ++++++------ .../tools/CharacterPropertyDataGenerator.cpp | 6 +++--- Source/JavaScriptCore/tools/FunctionOverrides.cpp | 2 +- .../JavaScriptCore/wasm/js/WebAssemblySuspending.cpp | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Source/JavaScriptCore/llint/LLIntData.h b/Source/JavaScriptCore/llint/LLIntData.h index f0e209879fb0..0c609a0625d6 100644 --- a/Source/JavaScriptCore/llint/LLIntData.h +++ b/Source/JavaScriptCore/llint/LLIntData.h @@ -77,8 +77,6 @@ extern "C" uint8_t os_script_config_storage_stub[] __asm__("_os_script_config_st extern "C" uint8_t os_script_config_storage[]; #endif -WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN - inline OpcodeConfig* addressOfOpcodeConfig() { return std::bit_cast(&os_script_config_storage); } #define g_opcodeConfig (*JSC::LLInt::addressOfOpcodeConfig()) diff --git a/Source/JavaScriptCore/runtime/JSLock.cpp b/Source/JavaScriptCore/runtime/JSLock.cpp index 5d549793b50b..7b84c8a57d21 100644 --- a/Source/JavaScriptCore/runtime/JSLock.cpp +++ b/Source/JavaScriptCore/runtime/JSLock.cpp @@ -163,6 +163,7 @@ void JSLock::unlock() } #if PLATFORM(COCOA) && CPU(ADDRESS64) && CPU(ARM64) +WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN // FIXME: rdar://168614004 NO_RETURN_DUE_TO_CRASH NEVER_INLINE void JSLock::dumpInfoAndCrashForLockNotOwned() // __attribute__((optnone)) { @@ -266,6 +267,7 @@ NO_RETURN_DUE_TO_CRASH NEVER_INLINE void JSLock::dumpInfoAndCrashForLockNotOwned #undef updateDumpState } +WTF_ALLOW_UNSAFE_BUFFER_USAGE_END #endif // Use WTF_IGNORES_THREAD_SAFETY_ANALYSIS because this function conditionally unlocks m_lock, which diff --git a/Source/JavaScriptCore/runtime/VM.cpp b/Source/JavaScriptCore/runtime/VM.cpp index e3aad5bd92c1..90a39ed0a28f 100644 --- a/Source/JavaScriptCore/runtime/VM.cpp +++ b/Source/JavaScriptCore/runtime/VM.cpp @@ -424,17 +424,17 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END std::call_once(registerFlag, [this]() { int pid = getpid(); const char* key = "com.apple.WebKit.bytecode.profiler"; - dataLogF("<%d> Registering callback for dumping profiles, dumping to %s.\n", pid, pathOutString->data()); - dataLogF("<%d> Use `notifyutil -v -p %s` to dump statistics.\n", pid, key); + dataLogLn("<", pid, "> Registering callback for dumping profiles, dumping to ", pathOutString.get(), "."); + dataLogLn("<", pid, "> Use `notifyutil -v -p ", key, "` to dump statistics."); int token; notify_register_dispatch(key, &token, mainDispatchQueueSingleton(), ^(int) { - dataLogF("<%d> Dumping\n", pid); + dataLogLn("<", pid, "> Dumping"); if (!m_perBytecodeProfiler->save(pathOutString->data())) - dataLogF("<%d> Failed to dump to %s. Do you need to add a sandbox extension? ((allow file-write* (subpath \"/private/tmp/\")) in WebProcess.sb.in\n", pid, pathOutString->data()); + dataLogLn("<", pid, "> Failed to dump to ", pathOutString.get(), ". Do you need to add a sandbox extension? ((allow file-write* (subpath \"/private/tmp/\")) in WebProcess.sb.in"); else - dataLogF("<%d> Dumped to %s\n", pid, pathOutString->data()); - dataLogF("<%d> Dumping finished\n", pid); + dataLogLn("<", pid, "> Dumped to ", pathOutString.get()); + dataLogLn("<", pid, "> Dumping finished"); }); }); #endif diff --git a/Source/JavaScriptCore/tools/CharacterPropertyDataGenerator.cpp b/Source/JavaScriptCore/tools/CharacterPropertyDataGenerator.cpp index 4d874309040e..d27b08b2c58a 100644 --- a/Source/JavaScriptCore/tools/CharacterPropertyDataGenerator.cpp +++ b/Source/JavaScriptCore/tools/CharacterPropertyDataGenerator.cpp @@ -185,10 +185,10 @@ class LineBreakData { for (unsigned y = 0; y < numChars; ++y) { const char16_t ch = y + minChar; dataLogF("/* %02X %c */ {B(", ch, ch < 0x7F ? ch : ' '); - const char* prefix = ""; + ASCIILiteral prefix = ""_s; for (unsigned x = 0; x < numCharsRoundUp8; ++x) { - dataLogF("%s%u", prefix, static_cast(m_pair[y].get(x))); - prefix = (x % 8 == 7) ? "),B(" : ","; + dataLog(prefix, static_cast(m_pair[y].get(x))); + prefix = (x % 8 == 7) ? "),B("_s : ","_s; } dataLogLn(")},"); } diff --git a/Source/JavaScriptCore/tools/FunctionOverrides.cpp b/Source/JavaScriptCore/tools/FunctionOverrides.cpp index 4c4ee4a884f3..918874a622f8 100644 --- a/Source/JavaScriptCore/tools/FunctionOverrides.cpp +++ b/Source/JavaScriptCore/tools/FunctionOverrides.cpp @@ -291,7 +291,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END int result = fclose(file); if (result) - dataLogF("Failed to close file %s: %s\n", fileName, safeStrerror(errno).data()); + dataLogLn("Failed to close file ", fileName, ": ", safeStrerror(errno).data()); } } // namespace JSC diff --git a/Source/JavaScriptCore/wasm/js/WebAssemblySuspending.cpp b/Source/JavaScriptCore/wasm/js/WebAssemblySuspending.cpp index 7596920405bf..dbefeaddccec 100644 --- a/Source/JavaScriptCore/wasm/js/WebAssemblySuspending.cpp +++ b/Source/JavaScriptCore/wasm/js/WebAssemblySuspending.cpp @@ -111,7 +111,7 @@ void* runWebAssemblySuspendingFunction(JSGlobalObject* globalObject, CallFrame* } CPURegister* vmEntryFrameCalleeSaves = vmEntryRecord(vm.topEntryFrame)->calleeSaveRegistersBuffer; - memcpySpan(std::span(vmEntryFrameCalleeSaves, NUMBER_OF_CALLEE_SAVES_REGISTERS), std::span(originalCalleeSaves, NUMBER_OF_CALLEE_SAVES_REGISTERS)); + memcpySpan(unsafeMakeSpan(vmEntryFrameCalleeSaves, NUMBER_OF_CALLEE_SAVES_REGISTERS), unsafeMakeSpan(originalCalleeSaves, NUMBER_OF_CALLEE_SAVES_REGISTERS)); JSObject* callee = callFrame->jsCallee(); JSFunctionWithFields* self = uncheckedDowncast(callee); From 86575fd6845c25501950dfad812cc35f1b761868 Mon Sep 17 00:00:00 2001 From: Mike Wyrzykowski Date: Thu, 14 May 2026 08:36:32 -0700 Subject: [PATCH 022/424] Enable GPU process model only for arm64 https://bugs.webkit.org/show_bug.cgi?id=314670 rdar://176025487 Reviewed by Dan Glastonbury. We can't disable all of ENABLE_GPU_PROCESS_MODEL because IPC messages must be consistent between arm64 and x86 slices. What we can do on x86 is avoid referencing the frameworks so we don't end up linking to them there. * Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift: * Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift: * Source/WebKit/GPUProcess/graphics/Model/USDModel.swift: Canonical link: https://commits.webkit.org/313248@main --- Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift | 6 +++--- .../WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift | 2 +- .../WebKit/GPUProcess/graphics/Model/ModelParameters.swift | 2 +- Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift | 2 +- Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift | 2 +- .../GPUProcess/graphics/Model/USDModel+Deformation.swift | 2 +- Source/WebKit/GPUProcess/graphics/Model/USDModel.swift | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift index 22809542c545..f2c3429e1b2a 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift @@ -24,7 +24,7 @@ import Metal import WebKit -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit @_spi(RealityCoreRendererAPI) import RealityKit import USDKit @@ -301,7 +301,7 @@ extension WKBridgeUpdateMesh { } } -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) func decodeValues(from data: Data) -> [T] where T: BitwiseCopyable { let stride = MemoryLayout.stride guard !data.isEmpty, data.count % stride == 0 else { return [] } @@ -619,7 +619,7 @@ extension WKBridgeMaterialGraph { } } -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) func toData(_ input: [T]) -> Data { // rdar://164559261 - this is needed because there is no way to represnt an NSArray of diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift index 8a7c9857a5ca..3e2f752b8579 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import Metal import USDKit diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift index 247254adcb11..984b3f1f4d85 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import USDKit @_spi(UsdLoaderAPI) import _USDKit_RealityKit diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift index b472cb943b31..d91c80b45a64 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import QuartzCore import USDKit diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift index 526e133e3e49..74304b79458b 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import DirectResource import Metal diff --git a/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift b/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift index d57a04bc5dc0..17f02b2f4852 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift @@ -26,7 +26,7 @@ import OSLog import WebKit import simd -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit @_spi(RealityCoreRendererAPI) import RealityKit @_spi(RealityCoreTextureProcessingAPI) import RealityCoreTextureProcessing diff --git a/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift b/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift index 97261a40ab92..69f84eed7ef4 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift @@ -26,7 +26,7 @@ import OSLog import WebKit import simd -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit @_spi(RealityCoreRendererAPI) import RealityKit @_spi(RealityCoreTextureProcessingAPI) import RealityCoreTextureProcessing From 5a1e30384382df1fbfedb3c0429b94d6c9cbf456 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Thu, 14 May 2026 08:39:02 -0700 Subject: [PATCH 023/424] AudioVideoRenderer TimeProgressEstimator incorrectly set m_effectiveRate https://bugs.webkit.org/show_bug.cgi?id=314795 rdar://177046564 Reviewed by Jer Noble and Eric Carlson. 311867@main ("Use a TimeProgressEstimator to interpolate AudioVideoRendererRemote::currentTime() between GPU updates") introduced two issues. 1. Every Play / Pause / SetRate IPC from WebContent caused the GPU-side RemoteAudioVideoRendererProxyManager handler to synchronously build a RemoteAudioVideoRendererState via stateFor() and push a StateUpdate IPC back to the WebContent. stateFor() reads videoPlaybackQualityMetrics, which on the protected-content AVSampleBufferDisplayLayer path (for example Netflix) fans into four separate [renderer videoPerformanceMetrics] synchronous XPCs into mediaserverd. The only consumer of that reply in the WebContent is the client-side TimeProgressEstimator, which only needs the MediaTimeUpdateData fields (currentTime, effectiveRate, wallTime) to unfreeze and re-anchor after the local setRate / pause already froze it. The full state payload and its metrics fetch were wasted work on every transition. 2. AudioVideoRendererRemote::TimeProgressEstimator::setRate() gated the assignment m_effectiveRate = rate on the same "if (currentRate)" guard that controls the rebase of m_cachedTime. The guard is only meaningful for the rebase (nothing to fold in when the old rate was already zero); the new-rate assignment shouldn't ride on it. As a result, when setRate() was called on an estimator whose current rate was 0 (for example immediately after a stall()), m_effectiveRate stayed at 0 locally until the next setTime() arrived. timeIsProgressing() and effectiveRate() would keep reporting 0 in that round-trip window. Fix 1: change Play / Pause / SetRate to asynchronous replies carrying only WebCore::MediaTimeUpdateData. The GPU handlers now call renderer->play / pause / setRate and invoke the completion with a fresh MediaTimeUpdateData built by a new static helper timeUpdateDataFor(renderer). stateFor() is refactored to reuse the same helper for the fields it shares. The new payload contains no videoPlaybackQualityMetrics, so these transitions no longer touch mediaserverd. The WebContent callers use sendWithAsyncReplyOnDispatcher on queueSingleton(); the reply handler applies m_timeEstimator.setTime(timeUpdateData) which is exactly the unfreeze that the old StateUpdate reply performed via updateCacheState. Fix 2: hoist m_effectiveRate = rate out of the rebase guard in TimeProgressEstimator::setRate, and drop the now-redundant m_effectiveRate = 0 inside the if (!rate) branch. All four (currentRate, rate) cases now leave m_effectiveRate == rate on exit. In a follow-up change we will extract the AudioVideoRendererRemote's TimeProgressEstimator to its own class and combine with the one used in MediaPlayerPrivateRemote this will allow for API tests and simplify the change. It will also allows for using a SharedMemory timeBase removing the need for sending IPC messages. * Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.messages.in: Declare MediaTimeUpdateData async replies on Play, Pause, SetRate. * Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h: (WebKit::RemoteAudioVideoRendererProxyManager::play): (WebKit::RemoteAudioVideoRendererProxyManager::pause): (WebKit::RemoteAudioVideoRendererProxyManager::setRate): Take a CompletionHandler&&. * Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp: (WebKit::timeUpdateDataFor): New file-local helper that returns a MediaTimeUpdateData without fetching videoPlaybackQualityMetrics. Stamps effectiveRate = timeIsProgressing() ? effectiveRate() : 0 and wallTime = MonotonicTime::now(), matching what stateFor() used to do inline. (WebKit::RemoteAudioVideoRendererProxyManager::play): (WebKit::RemoteAudioVideoRendererProxyManager::pause): (WebKit::RemoteAudioVideoRendererProxyManager::setRate): Invoke the completion with timeUpdateDataFor(*renderer) instead of sending a StateUpdate(stateFor(...)). Null-renderer path still invokes the completion with a default MediaTimeUpdateData so the IPC reply never hangs. (WebKit::RemoteAudioVideoRendererProxyManager::stateFor): Reuse timeUpdateDataFor for the timeUpdateData sub-struct. * Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.cpp: (WebKit::AudioVideoRendererRemote::TimeProgressEstimator::setRate): Always apply m_effectiveRate = rate; only the m_cachedTime / m_wallTime rebase remains conditional on the prior rate being non-zero. Drop the redundant m_effectiveRate = 0 inside the if (!rate) branch. (WebKit::AudioVideoRendererRemote::play): (WebKit::AudioVideoRendererRemote::pause): (WebKit::AudioVideoRendererRemote::setRate): Use sendWithAsyncReplyOnDispatcher on queueSingleton(); the reply handler calls m_timeEstimator.setTime(timeUpdateData) so the estimator still gets the same anchor it used to receive from the full StateUpdate reply. Canonical link: https://commits.webkit.org/313249@main --- .../RemoteAudioVideoRendererProxyManager.cpp | 33 ++++++++++++------- .../RemoteAudioVideoRendererProxyManager.h | 6 ++-- ...AudioVideoRendererProxyManager.messages.in | 6 ++-- .../GPU/media/AudioVideoRendererRemote.cpp | 27 +++++++++------ 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp b/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp index 48dacb38871f..7aeb9430e8b7 100644 --- a/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp +++ b/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp @@ -67,6 +67,15 @@ using namespace WebCore; WTF_MAKE_TZONE_ALLOCATED_IMPL(RemoteAudioVideoRendererProxyManager); +static WebCore::MediaTimeUpdateData timeUpdateDataFor(WebCore::AudioVideoRenderer& renderer) +{ + return { + .currentTime = renderer.currentTime(), + .effectiveRate = renderer.timeIsProgressing() ? renderer.effectiveRate() : 0.0, + .wallTime = MonotonicTime::now(), + }; +} + RefPtr RemoteAudioVideoRendererProxyManager::createRenderer() { #if USE(AVFOUNDATION) @@ -351,28 +360,34 @@ void RemoteAudioVideoRendererProxyManager::notifyWhenErrorOccurs(RemoteAudioVide } // SynchronizerInterface -void RemoteAudioVideoRendererProxyManager::play(RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime) +void RemoteAudioVideoRendererProxyManager::play(RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime, CompletionHandler&& completionHandler) { if (RefPtr renderer = rendererFor(identifier)) { renderer->play(hostTime); - m_gpuConnectionToWebProcess.get()->connection().send(Messages::AudioVideoRendererRemoteMessageReceiver::StateUpdate(stateFor(identifier)), identifier); + completionHandler(timeUpdateDataFor(*renderer)); + return; } + completionHandler({ }); } -void RemoteAudioVideoRendererProxyManager::pause(RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime) +void RemoteAudioVideoRendererProxyManager::pause(RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime, CompletionHandler&& completionHandler) { if (RefPtr renderer = rendererFor(identifier)) { renderer->pause(hostTime); - m_gpuConnectionToWebProcess.get()->connection().send(Messages::AudioVideoRendererRemoteMessageReceiver::StateUpdate(stateFor(identifier)), identifier); + completionHandler(timeUpdateDataFor(*renderer)); + return; } + completionHandler({ }); } -void RemoteAudioVideoRendererProxyManager::setRate(RemoteAudioVideoRendererIdentifier identifier, double rate) +void RemoteAudioVideoRendererProxyManager::setRate(RemoteAudioVideoRendererIdentifier identifier, double rate, CompletionHandler&& completionHandler) { if (RefPtr renderer = rendererFor(identifier)) { renderer->setRate(rate); - m_gpuConnectionToWebProcess.get()->connection().send(Messages::AudioVideoRendererRemoteMessageReceiver::StateUpdate(stateFor(identifier)), identifier); + completionHandler(timeUpdateDataFor(*renderer)); + return; } + completionHandler({ }); } void RemoteAudioVideoRendererProxyManager::stall(RemoteAudioVideoRendererIdentifier identifier) @@ -579,11 +594,7 @@ RemoteAudioVideoRendererState RemoteAudioVideoRendererProxyManager::stateFor(Rem if (!renderer) return { }; return { - .timeUpdateData = { - .currentTime = renderer->currentTime(), - .effectiveRate = renderer->timeIsProgressing() ? renderer->effectiveRate() : 0.0, - .wallTime = MonotonicTime::now(), - }, + .timeUpdateData = timeUpdateDataFor(*renderer), .paused = renderer->paused(), .videoPlaybackQualityMetrics = renderer->videoPlaybackQualityMetrics() }; diff --git a/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h b/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h index 535c5503216c..3ff9d50b6afe 100644 --- a/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h +++ b/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h @@ -113,9 +113,9 @@ class RemoteAudioVideoRendererProxyManager final : public IPC::MessageReceiver { void notifyWhenErrorOccurs(RemoteAudioVideoRendererIdentifier, CompletionHandler&&); // SynchronizerInterface - void play(RemoteAudioVideoRendererIdentifier, std::optional); - void pause(RemoteAudioVideoRendererIdentifier, std::optional); - void setRate(RemoteAudioVideoRendererIdentifier, double); + void play(RemoteAudioVideoRendererIdentifier, std::optional, CompletionHandler&&); + void pause(RemoteAudioVideoRendererIdentifier, std::optional, CompletionHandler&&); + void setRate(RemoteAudioVideoRendererIdentifier, double, CompletionHandler&&); void stall(RemoteAudioVideoRendererIdentifier); void prepareToSeek(RemoteAudioVideoRendererIdentifier, const MediaTime&, CompletionHandler&&); void finishSeek(RemoteAudioVideoRendererIdentifier, const MediaTime&, CompletionHandler&&); diff --git a/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.messages.in b/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.messages.in index 8a3cdbbc5145..5a27282c03b1 100644 --- a/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.messages.in +++ b/Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.messages.in @@ -54,9 +54,9 @@ messages -> RemoteAudioVideoRendererProxyManager { ApplicationWillResignActive(WebKit::RemoteAudioVideoRendererIdentifier identifier) SetSpatialTrackingInfo(WebKit::RemoteAudioVideoRendererIdentifier identifier, bool prefersSpatialAudioExperience, enum:uint8_t WebCore::MediaPlayerSoundStageSize soundStageSize, String sceneIdentifier, String defaultLabel, String label) - Play(WebKit::RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime) - Pause(WebKit::RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime) - SetRate(WebKit::RemoteAudioVideoRendererIdentifier identifier, double rate) + Play(WebKit::RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime) -> (struct WebCore::MediaTimeUpdateData timeUpdateData) + Pause(WebKit::RemoteAudioVideoRendererIdentifier identifier, std::optional hostTime) -> (struct WebCore::MediaTimeUpdateData timeUpdateData) + SetRate(WebKit::RemoteAudioVideoRendererIdentifier identifier, double rate) -> (struct WebCore::MediaTimeUpdateData timeUpdateData) Stall(WebKit::RemoteAudioVideoRendererIdentifier identifier) PrepareToSeek(WebKit::RemoteAudioVideoRendererIdentifier identifier, MediaTime seekTime) -> (Expected result) FinishSeek(WebKit::RemoteAudioVideoRendererIdentifier identifier, MediaTime seekTime) -> (GenericPromise::Result result) diff --git a/Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.cpp b/Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.cpp index c31d9bd7e9e6..007d085d3138 100644 --- a/Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.cpp +++ b/Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.cpp @@ -124,12 +124,10 @@ void AudioVideoRendererRemote::TimeProgressEstimator::setRate(double rate) auto elapsed = std::min(now - m_wallTime, kUpdateInterval); m_cachedTime += MediaTime::createWithDouble(currentRate * elapsed.seconds()); m_wallTime = now; - m_effectiveRate = rate; } - if (!rate) { - m_effectiveRate = 0; + m_effectiveRate = rate; + if (!rate) m_lastReturnedTime.reset(); - } m_forceUseCachedTime = true; } @@ -476,9 +474,12 @@ void AudioVideoRendererRemote::play(std::optional hostTime) Locker locker { m_lock }; m_cachedState.paused = false; } - // The GPU will reply with a StateUpdate carrying the new effective rate so the estimator can resume extrapolation without waiting for the 250ms periodic tick. + // The GPU will reply with the current MediaTimeUpdateData so the estimator can resume extrapolation without waiting for the 250ms periodic tick. ensureOnDispatcherWithConnection([hostTime](auto& renderer, auto& connection) { - connection.send(Messages::RemoteAudioVideoRendererProxyManager::Play(renderer.m_identifier, hostTime), 0); + connection.sendWithAsyncReplyOnDispatcher(Messages::RemoteAudioVideoRendererProxyManager::Play(renderer.m_identifier, hostTime), queueSingleton(), [weakThis = ThreadSafeWeakPtr { renderer }](WebCore::MediaTimeUpdateData&& timeUpdateData) { + if (RefPtr protectedThis = weakThis.get()) + protectedThis->m_timeEstimator.setTime(timeUpdateData); + }); }); } @@ -489,9 +490,12 @@ void AudioVideoRendererRemote::pause(std::optional hostTime) m_cachedState.paused = true; } m_timeEstimator.pause(); - // The GPU will reply with a StateUpdate so cached fields (videoPlaybackQualityMetrics, etc.) match its view immediately. + // The GPU will reply with the current MediaTimeUpdateData so the estimator re-anchors against the GPU's view. ensureOnDispatcherWithConnection([hostTime](auto& renderer, auto& connection) { - connection.send(Messages::RemoteAudioVideoRendererProxyManager::Pause(renderer.m_identifier, hostTime), 0); + connection.sendWithAsyncReplyOnDispatcher(Messages::RemoteAudioVideoRendererProxyManager::Pause(renderer.m_identifier, hostTime), queueSingleton(), [weakThis = ThreadSafeWeakPtr { renderer }](WebCore::MediaTimeUpdateData&& timeUpdateData) { + if (RefPtr protectedThis = weakThis.get()) + protectedThis->m_timeEstimator.setTime(timeUpdateData); + }); }); } @@ -504,9 +508,12 @@ bool AudioVideoRendererRemote::paused() const void AudioVideoRendererRemote::setRate(double rate) { m_timeEstimator.setRate(rate); - // The GPU will reply with a StateUpdate that unfreezes the estimator (setTime clears m_forceUseCachedTime) with the real effective rate. + // The GPU will reply with the current MediaTimeUpdateData that unfreezes the estimator (setTime clears m_forceUseCachedTime) with the real effective rate. ensureOnDispatcherWithConnection([rate](auto& renderer, auto& connection) { - connection.send(Messages::RemoteAudioVideoRendererProxyManager::SetRate(renderer.m_identifier, rate), 0); + connection.sendWithAsyncReplyOnDispatcher(Messages::RemoteAudioVideoRendererProxyManager::SetRate(renderer.m_identifier, rate), queueSingleton(), [weakThis = ThreadSafeWeakPtr { renderer }](WebCore::MediaTimeUpdateData&& timeUpdateData) { + if (RefPtr protectedThis = weakThis.get()) + protectedThis->m_timeEstimator.setTime(timeUpdateData); + }); }); } From 368c0bec7652c0cee5797d1827c5aa4163019a7a Mon Sep 17 00:00:00 2001 From: Fady Farag Date: Thu, 14 May 2026 08:47:14 -0700 Subject: [PATCH 024/424] Introduce `WTF::toArray()` to work around `std::to_array()` not being detected as `NODELETE` by static analysis https://bugs.webkit.org/show_bug.cgi?id=313933 rdar://176135547 Reviewed by Chris Dumez. Follow-up to 311966@main. * Source/WTF/wtf/StdLibExtras.h: (WTF::noexcept): * Source/WebCore/editing/EditingStyle.cpp: (WebCore::htmlElementEquivalents): (WebCore::htmlAttributeEquivalents): Canonical link: https://commits.webkit.org/313250@main --- Source/WTF/wtf/StdLibExtras.h | 14 +++++++++++ Source/WebCore/editing/EditingStyle.cpp | 8 +++---- Tools/Scripts/webkitpy/style/checker.py | 7 ++++-- Tools/Scripts/webkitpy/style/checkers/cpp.py | 24 +++++++++++++++++++ .../webkitpy/style/checkers/cpp_unittest.py | 18 ++++++++++++++ 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Source/WTF/wtf/StdLibExtras.h b/Source/WTF/wtf/StdLibExtras.h index ebdcb2707223..34c2300f1554 100644 --- a/Source/WTF/wtf/StdLibExtras.h +++ b/Source/WTF/wtf/StdLibExtras.h @@ -871,6 +871,20 @@ template return std::move(std::forward(value)); } +template +[[nodiscard]] SUPPRESS_NODELETE constexpr std::array, N> NODELETE toArray(T (&array)[N]) + noexcept(std::is_nothrow_constructible_v) +{ + return std::to_array(array); // NOLINT(runtime/wtf_to_array) +} + +template +[[nodiscard]] SUPPRESS_NODELETE constexpr std::array, N> NODELETE toArray(T (&&array)[N]) + noexcept(std::is_nothrow_move_constructible_v) +{ + return std::to_array(WTF::move(array)); // NOLINT(runtime/wtf_to_array) +} + template [[nodiscard]] ALWAYS_INLINE decltype(auto) makeUnique(Args&&... args) { diff --git a/Source/WebCore/editing/EditingStyle.cpp b/Source/WebCore/editing/EditingStyle.cpp index 45ac8b033309..4be8542c36c7 100644 --- a/Source/WebCore/editing/EditingStyle.cpp +++ b/Source/WebCore/editing/EditingStyle.cpp @@ -1050,9 +1050,9 @@ bool EditingStyle::conflictsWithInlineStyleOfElement(StyledElement& element, Ref return conflicts; } -SUPPRESS_NODELETE static std::span NODELETE htmlElementEquivalents() +static std::span NODELETE htmlElementEquivalents() { - static const auto equivalents = std::to_array({ + static const auto equivalents = WTF::toArray({ new HTMLFontWeightEquivalent(HTMLNames::bTag), new HTMLFontWeightEquivalent(HTMLNames::strongTag), @@ -1084,9 +1084,9 @@ bool EditingStyle::conflictsWithImplicitStyleOfElement(HTMLElement& element, Edi return false; } -SUPPRESS_NODELETE static std::span NODELETE htmlAttributeEquivalents() +static std::span NODELETE htmlAttributeEquivalents() { - static const auto equivalents = std::to_array({ + static const auto equivalents = WTF::toArray({ // elementIsStyledSpanOrHTMLEquivalent depends on the fact each HTMLAttriuteEquivalent matches exactly one attribute // of exactly one element except dirAttr. new HTMLAttributeEquivalent(CSSPropertyColor, HTMLNames::fontTag, HTMLNames::colorAttr), diff --git a/Tools/Scripts/webkitpy/style/checker.py b/Tools/Scripts/webkitpy/style/checker.py index dd6bbb73adc7..5e52a83aad0a 100644 --- a/Tools/Scripts/webkitpy/style/checker.py +++ b/Tools/Scripts/webkitpy/style/checker.py @@ -239,7 +239,8 @@ "-security/printf", "-runtime/lock_guard", "-runtime/wtf_make_unique", - "-runtime/wtf_move"]), + "-runtime/wtf_move", + "-runtime/wtf_to_array"]), ([ # To use GStreamer GL without conflicts of GL symbols, @@ -438,7 +439,8 @@ ([ # MiniBrowser doesn't use WTF, but only public WebKit API. os.path.join('Tools', 'MiniBrowser')], ["-runtime/wtf_make_unique", - "-runtime/wtf_move"]), + "-runtime/wtf_move", + "-runtime/wtf_to_array"]), ([ # Ignore formatting and whitespace issues in gmock. os.path.join('Source', 'ThirdParty', 'gmock')], @@ -449,6 +451,7 @@ "-readability", "-runtime/unsigned", "-runtime/wtf_move", + "-runtime/wtf_to_array", "-whitespace"]), ([ # Ignore whitespace issues in third party library esprima.js diff --git a/Tools/Scripts/webkitpy/style/checkers/cpp.py b/Tools/Scripts/webkitpy/style/checkers/cpp.py index 469674567141..498d3f4807a2 100644 --- a/Tools/Scripts/webkitpy/style/checkers/cpp.py +++ b/Tools/Scripts/webkitpy/style/checkers/cpp.py @@ -2882,6 +2882,28 @@ def check_wtf_move(clean_lines, line_number, file_state, error): error(line_number, 'runtime/wtf_move', 4, "Use 'WTF::move()' instead of 'WTFMove()'.") +def check_wtf_to_array(clean_lines, line_number, file_state, error): + """Looks for use of 'std::to_array' which should be replaced with 'WTF::toArray()'. + + Args: + clean_lines: A CleansedLines instance containing the file. + line_number: The number of the line to check. + file_state: A _FileState instance which maintains information about + the state of things in the file. + error: The function to call with any errors found. + """ + + # This check doesn't apply to C or Objective-C implementation files. + if file_state.is_c_or_objective_c(): + return + + line = clean_lines.elided[line_number] # Get rid of comments and strings. + + using_std_to_array = search(r'\bstd::to_array\s*[<(]', line) + if using_std_to_array: + error(line_number, 'runtime/wtf_to_array', 4, "Use 'WTF::toArray()' instead of 'std::to_array()'.") + + def check_unsafe_get(clean_lines, line_number, file_state, error): """Looks for use of 'unsafeGet()' or 'unsafePtr()' which should be avoided. @@ -3968,6 +3990,7 @@ def check_style(clean_lines, line_number, file_extension, class_state, file_stat check_max_min_macros(clean_lines, line_number, file_state, error) check_wtf_checked_size(clean_lines, line_number, file_state, error) check_wtf_move(clean_lines, line_number, file_state, error) + check_wtf_to_array(clean_lines, line_number, file_state, error) check_unsafe_get(clean_lines, line_number, file_state, error) check_wtf_make_unique(clean_lines, line_number, file_state, error) check_wtf_never_destroyed(clean_lines, line_number, file_state, error) @@ -5274,6 +5297,7 @@ class CppChecker(object): 'runtime/wtf_checked_size', 'runtime/wtf_make_unique', 'runtime/wtf_move', + 'runtime/wtf_to_array', 'runtime/wtf_never_destroyed', 'runtime/wtf_os_object_ptr', 'runtime/wtf_xpc_object_ptr', diff --git a/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py b/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py index 1d23d3d8c046..95de9734f9f6 100644 --- a/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py +++ b/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py @@ -6230,6 +6230,24 @@ def test_wtf_move(self): " [runtime/wtf_move] [4]", 'foo.mm') + def test_wtf_to_array(self): + self.assert_lint( + 'auto a = WTF::toArray({ 1, 2, 3 });', + '', + 'foo.cpp') + + self.assert_lint( + 'auto a = std::to_array({ 1, 2, 3 });', + "Use 'WTF::toArray()' instead of 'std::to_array()'." + " [runtime/wtf_to_array] [4]", + 'foo.cpp') + + self.assert_lint( + 'auto a = std::to_array(src);', + "Use 'WTF::toArray()' instead of 'std::to_array()'." + " [runtime/wtf_to_array] [4]", + 'foo.cpp') + def test_protected_getter(self): # Regular getter is fine. self.assert_lint( From c7342e9519e4d91f495156d713493c480779a1e1 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 14 May 2026 09:12:12 -0700 Subject: [PATCH 025/424] dialog.requestClose() is missing prelimanary checks https://bugs.webkit.org/show_bug.cgi?id=311746 Reviewed by Anne van Kesteren. This adds a check for the dialog being connected to the document and being in a fully active document. This matches more recent spec changes. Tests: imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2.html imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3.html * LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2.html: Added. * LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3.html: Added. * Source/WebCore/html/HTMLDialogElement.cpp: (WebCore::HTMLDialogElement::requestClose): Canonical link: https://commits.webkit.org/313251@main --- .../dialog-requestclose-2-expected.txt | 3 +++ .../dialog-requestclose-2.html | 18 +++++++++++++++++ .../dialog-requestclose-3-expected.txt | 3 +++ .../dialog-requestclose-3.html | 20 +++++++++++++++++++ Source/WebCore/html/HTMLDialogElement.cpp | 6 +++++- 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2-expected.txt create mode 100644 LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3-expected.txt create mode 100644 LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2-expected.txt new file mode 100644 index 000000000000..ddebe90016c3 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2-expected.txt @@ -0,0 +1,3 @@ + +PASS requestClose() should not close the dialog when disconnected + diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2.html b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2.html new file mode 100644 index 000000000000..2a99b45f050b --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-2.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3-expected.txt new file mode 100644 index 000000000000..afeeb5b80891 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3-expected.txt @@ -0,0 +1,3 @@ + +PASS requestClose() should not close the dialog when is in inactive document + diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3.html b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3.html new file mode 100644 index 000000000000..e05e3db9d0c2 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-3.html @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/Source/WebCore/html/HTMLDialogElement.cpp b/Source/WebCore/html/HTMLDialogElement.cpp index 2692720db996..4d6dd9c14231 100644 --- a/Source/WebCore/html/HTMLDialogElement.cpp +++ b/Source/WebCore/html/HTMLDialogElement.cpp @@ -246,7 +246,11 @@ void HTMLDialogElement::requestClose(const String& returnValue, Element* source) if (!isOpen()) return; - // FIXME(311746): Add missing prelimanary checks that should prevent this function running. + if (!isConnected()) + return; + + if (!protect(this->document())->isFullyActive()) + return; if (m_isRequestingToClose) return; From 20ca333f3a4ded5c4a2b4ff2b520e941906bff28 Mon Sep 17 00:00:00 2001 From: Yijia Huang Date: Thu, 14 May 2026 09:22:51 -0700 Subject: [PATCH 026/424] [JSC][Temporal] Add duration rounding, calendar field resolution, and date-time difference algorithms https://bugs.webkit.org/show_bug.cgi?id=314765 rdar://177014604 Reviewed by Yusuke Suzuki. Adds the algorithm core layer for Temporal. DurationArithmetic implements duration rounding and differencing (nudge, bubble, roundRelativeDuration, differenceZonedDateTime). CalendarFields implements CalendarDateFromFields, CalendarYearMonthFromFields, and CalendarMonthDayFromFields for ISO and non-ISO calendars via ICU. PlainDateTimeCore and ZonedDateTimeCore implement DifferenceTemporalPlainDateTime and DifferenceZonedDateTimeWithRounding. Tests: Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp Canonical link: https://commits.webkit.org/313252@main --- .../API/tests/TemporalCoreTest.cpp | 1800 ++++++++++++++++- Source/JavaScriptCore/CMakeLists.txt | 4 + .../JavaScriptCore.xcodeproj/project.pbxproj | 28 + .../Scripts/generate-unified-sources.sh | 2 +- Source/JavaScriptCore/Sources.txt | 4 + .../UnifiedSources-output.xcfilelist | 1 + Source/JavaScriptCore/runtime/ISO8601.h | 4 +- Source/JavaScriptCore/runtime/IntlObject.h | 4 +- .../runtime/TemporalDuration.cpp | 20 +- .../JavaScriptCore/runtime/TemporalDuration.h | 20 +- .../runtime/TemporalPlainDateTime.cpp | 49 +- .../runtime/temporal/core/CalendarFields.cpp | 734 +++++++ .../runtime/temporal/core/CalendarFields.h | 83 + .../temporal/core/CalendarICUBridge.cpp | 18 +- .../runtime/temporal/core/CalendarICUBridge.h | 2 +- .../temporal/core/DurationArithmetic.cpp | 1030 ++++++++++ .../temporal/core/DurationArithmetic.h | 146 ++ .../runtime/temporal/core/ISOArithmetic.cpp | 77 + .../runtime/temporal/core/ISOArithmetic.h | 6 + .../temporal/core/PlainDateTimeCore.cpp | 144 ++ .../runtime/temporal/core/PlainDateTimeCore.h | 52 + .../runtime/temporal/core/TemporalEnums.h | 150 +- .../temporal/core/TimeZoneICUBridge.cpp | 7 +- .../runtime/temporal/core/TimeZoneICUBridge.h | 2 +- .../temporal/core/ZonedDateTimeCore.cpp | 295 +++ .../runtime/temporal/core/ZonedDateTimeCore.h | 65 + 26 files changed, 4470 insertions(+), 277 deletions(-) create mode 100644 Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp create mode 100644 Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h create mode 100644 Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.cpp create mode 100644 Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.h create mode 100644 Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.cpp create mode 100644 Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.h create mode 100644 Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.cpp create mode 100644 Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.h diff --git a/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp b/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp index f86aed318a2d..f10f1a160760 100644 --- a/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp +++ b/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp @@ -27,15 +27,19 @@ #include "TemporalCoreTest.h" #include "CalendarArithmetic.h" +#include "CalendarFields.h" #include "CalendarICUBridge.h" +#include "DurationArithmetic.h" #include "ISO8601.h" #include "ISOArithmetic.h" #include "InstantCore.h" #include "JSCTimeZone.h" +#include "PlainDateTimeCore.h" #include "Rounding.h" #include "TemporalCoreTypes.h" #include "TemporalEnums.h" #include "TimeZoneICUBridge.h" +#include "ZonedDateTimeCore.h" #include #include @@ -277,6 +281,105 @@ static void testMaximumRoundingIncrement() TCHECK_EQ(maximumRoundingIncrement(TemporalUnit::Nanosecond).value_or(0), 1000u, "maxIncrement: Nanosecond = 1000"); } +// --------------------------------------------------------------------------- +// DurationArithmetic tests — mirrors temporal_rs src/builtins/core/duration.rs +// --------------------------------------------------------------------------- + +static void testTimeDurationFromComponents() +{ + // temporal_rs: TimeDuration::from_components + // 1h = 3600000000000 ns + Int128 h1 = timeDurationFromComponents(1, 0, 0, 0, 0, 0); + TCHECK_EQ(h1, Int128(3600000000000LL), "timeDuration: 1h"); + + // 1m = 60000000000 ns + Int128 m1 = timeDurationFromComponents(0, 1, 0, 0, 0, 0); + TCHECK_EQ(m1, Int128(60000000000LL), "timeDuration: 1m"); + + // 1s = 1000000000 ns + Int128 s1 = timeDurationFromComponents(0, 0, 1, 0, 0, 0); + TCHECK_EQ(s1, Int128(1000000000LL), "timeDuration: 1s"); + + // 1ms = 1000000 ns + Int128 ms1 = timeDurationFromComponents(0, 0, 0, 1, 0, 0); + TCHECK_EQ(ms1, Int128(1000000LL), "timeDuration: 1ms"); + + // 1µs = 1000 ns + Int128 us1 = timeDurationFromComponents(0, 0, 0, 0, 1, 0); + TCHECK_EQ(us1, Int128(1000LL), "timeDuration: 1µs"); + + // 1ns + Int128 ns1 = timeDurationFromComponents(0, 0, 0, 0, 0, 1); + TCHECK_EQ(ns1, Int128(1LL), "timeDuration: 1ns"); + + // Combined: 1h2m3s = 3723000000000 ns + Int128 combined = timeDurationFromComponents(1, 2, 3, 0, 0, 0); + TCHECK_EQ(combined, Int128(3723000000000LL), "timeDuration: 1h2m3s"); + + // 25h = 90000000000000 ns + Int128 h25 = timeDurationFromComponents(25, 0, 0, 0, 0, 0); + TCHECK_EQ(h25, Int128(90000000000000LL), "timeDuration: 25h"); +} + +static void testDurationSign() +{ + // temporal_rs: Duration::sign + ISO8601::Duration pos(1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + TCHECK_EQ(durationSign(pos), 1, "durationSign: positive"); + + ISO8601::Duration neg(-1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + TCHECK_EQ(durationSign(neg), -1, "durationSign: negative"); + + ISO8601::Duration zero; + TCHECK_EQ(durationSign(zero), 0, "durationSign: zero"); + + // Mixed field signs -> should not occur in valid durations, but sign uses first nonzero + ISO8601::Duration negHours(0, 0, 0, 0, -5, 0, 0, 0, 0, 0); + TCHECK_EQ(durationSign(negHours), -1, "durationSign: negative hours"); +} + +static void testLargestSubduration() +{ + // temporal_rs: Duration::default_largest_unit + ISO8601::Duration d1(1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + TCHECK_EQ(largestSubduration(d1), TemporalUnit::Year, "largestSub: years"); + + ISO8601::Duration d2(0, 2, 0, 0, 0, 0, 0, 0, 0, 0); + TCHECK_EQ(largestSubduration(d2), TemporalUnit::Month, "largestSub: months"); + + ISO8601::Duration d3(0, 0, 0, 0, 3, 0, 0, 0, 0, 0); + TCHECK_EQ(largestSubduration(d3), TemporalUnit::Hour, "largestSub: hours"); + + ISO8601::Duration d4(0, 0, 0, 0, 0, 0, 0, 0, 0, 5); + TCHECK_EQ(largestSubduration(d4), TemporalUnit::Nanosecond, "largestSub: nanoseconds"); + + // Zero duration -> Nanosecond (first nonzero not found, returns last) + ISO8601::Duration d5; + TCHECK_EQ(largestSubduration(d5), TemporalUnit::Nanosecond, "largestSub: zero"); +} + +static void testAdjustDateDurationRecord() +{ + // temporal_rs: AdjustDateDurationRecord + ISO8601::Duration base(2, 3, 1, 5, 0, 0, 0, 0, 0, 0); + + // Override days only + auto r1 = adjustDateDurationRecord(base, 10.0, std::nullopt, std::nullopt); + TCHECK_TRUE(r1.has_value(), "adjustDateDur: days override ok"); + TCHECK_EQ(static_cast(r1->years()), 2LL, "adjustDateDur: years preserved"); + TCHECK_EQ(static_cast(r1->months()), 3LL, "adjustDateDur: months preserved"); + TCHECK_EQ(static_cast(r1->days()), 10LL, "adjustDateDur: days overridden"); + + // Override weeks + auto r2 = adjustDateDurationRecord(base, 5.0, 2.0, std::nullopt); + TCHECK_TRUE(r2.has_value(), "adjustDateDur: weeks override ok"); + TCHECK_EQ(static_cast(r2->weeks()), 2LL, "adjustDateDur: weeks overridden"); + + // Mixed signs -> invalid, should error + auto r3 = adjustDateDurationRecord(base, -10.0, std::nullopt, std::nullopt); + TCHECK_TRUE(!r3.has_value(), "adjustDateDur: mixed sign rejected"); +} + // --------------------------------------------------------------------------- // CalendarArithmetic tests — mirrors temporal_rs src/builtins/core/calendar.rs // --------------------------------------------------------------------------- @@ -470,9 +573,8 @@ static void testNegativeRounding() static void testRoundAsIfPositive() { - // roundNumberToIncrementAsIfPositive always uses getUnsignedRoundingMode(mode, false). - // For negative x=-107, increment=10: C++ quotient=-10, r1=-11, r2=-10. - // Trunc->Zero->r1*inc = -110; Expand->Infinity->r2*inc = -100. + // roundNumberToIncrementAsIfPositive treats negative x as positive for rounding direction. + // x=-107 inc=10: Trunc→-110 (toward -∞ when treated positive), Expand→-100. TCHECK_EQ(roundNumberToIncrementAsIfPositive(Int128(-107), Int128(10), RoundingMode::Trunc), Int128(-110), "asIfPos: -107 Trunc=-110"); TCHECK_EQ(roundNumberToIncrementAsIfPositive(Int128(-107), Int128(10), RoundingMode::Expand), Int128(-100), "asIfPos: -107 Expand=-100"); TCHECK_EQ(roundNumberToIncrementAsIfPositive(Int128(-107), Int128(10), RoundingMode::Ceil), Int128(-100), "asIfPos: -107 Ceil=-100"); @@ -505,6 +607,30 @@ static void testNegateRoundingMode() TCHECK_EQ(static_cast(negateTemporalRoundingMode(RoundingMode::HalfEven)), static_cast(RoundingMode::HalfEven), "negate: HalfEven unchanged"); } +// --------------------------------------------------------------------------- +// Duration balancing — mirrors temporal_rs balance tests +// --------------------------------------------------------------------------- + +static void testDurationBalancing() +{ + // temporal_rs: balance_days_up_to_both_years_and_months + // 2020-01-01 + 11M = 2020-12-01, then + 396D = 2022-01-01 + auto r = isoDateAdd({ 2020, 1, 1 }, ISO8601::Duration(0, 11, 0, 396, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value(), "balance: 11m+396d from 2020-01-01 ok"); + TCHECK_EQ(r->year(), 2022, "balance: 11m+396d year=2022"); + TCHECK_EQ(r->month(), 1u, "balance: 11m+396d month=1"); + + // temporal_rs: negative balance + // -60h = -3d (using timeDurationFromComponents) + Int128 neg60h = timeDurationFromComponents(-60, 0, 0, 0, 0, 0); + TCHECK_EQ(neg60h, Int128(-216000000000000LL), "balance: -60h in ns"); + + // Subsecond balancing: -999ms + -999999µs + -999999999ns = -2.998998999s + Int128 negMs = timeDurationFromComponents(0, 0, 0, -999, -999999, -999999999); + // Total = -999*1e6 - 999999*1e3 - 999999999 = -999000000 - 999999000 - 999999999 = -2998998999 ns + TCHECK_EQ(negMs, Int128(-2998998999LL), "balance: -999ms-999999µs-999999999ns"); +} + // --------------------------------------------------------------------------- // isoDateAdd boundary/error cases // --------------------------------------------------------------------------- @@ -533,7 +659,8 @@ static void testISODateAddBoundaries() } // --------------------------------------------------------------------------- -// Negative number rounding — mirrors temporal_rs src/rounding.rs tests +// New tests — ISOArithmetic, DurationArithmetic, Rounding, Instant, Calendar, +// TimeZone, PlainDateTime // --------------------------------------------------------------------------- static void testBalanceISOYearMonth() @@ -587,6 +714,144 @@ static void testApplyUnsignedRoundingMode() TCHECK_EQ(applyUnsignedRoundingMode(3.5, 3.0, 4.0, UnsignedRoundingMode::HalfEven), 4.0, "applyURM: 3.5 HalfEven->4"); } +static void testNegateDuration() +{ + ISO8601::Duration d(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + auto neg = negateDuration(d); + TCHECK_EQ(static_cast(neg.years()), -1LL, "negate: years"); + TCHECK_EQ(static_cast(neg.months()), -2LL, "negate: months"); + TCHECK_EQ(static_cast(neg.days()), -4LL, "negate: days"); + TCHECK_EQ(static_cast(neg.hours()), -5LL, "negate: hours"); + // Double negation = identity + auto back = negateDuration(neg); + TCHECK_EQ(static_cast(back.years()), 1LL, "negate: double neg years"); + // Zero unchanged + ISO8601::Duration zero; + auto negZero = negateDuration(zero); + TCHECK_EQ(durationSign(negZero), 0, "negate: zero"); +} + +static void testAbsDuration() +{ + // Positive unchanged + ISO8601::Duration pos(1, 2, 0, 4, 0, 0, 0, 0, 0, 0); + auto absPos = absDuration(pos); + TCHECK_EQ(static_cast(absPos.years()), 1LL, "abs: pos years"); + TCHECK_EQ(static_cast(absPos.days()), 4LL, "abs: pos days"); + // Negative -> positive + ISO8601::Duration neg(-1, -2, 0, -4, 0, 0, 0, 0, 0, 0); + auto absNeg = absDuration(neg); + TCHECK_EQ(static_cast(absNeg.years()), 1LL, "abs: neg years"); + TCHECK_EQ(static_cast(absNeg.months()), 2LL, "abs: neg months"); + TCHECK_EQ(static_cast(absNeg.days()), 4LL, "abs: neg days"); + TCHECK_EQ(durationSign(absNeg), 1, "abs: sign positive"); + // Zero unchanged + ISO8601::Duration zero; + auto absZero = absDuration(zero); + TCHECK_EQ(durationSign(absZero), 0, "abs: zero"); +} + +static void testGetUTCEpochNanoseconds() +{ + // Unix epoch = 0 + auto r0 = getUTCEpochNanoseconds({ 1970, 1, 1 }, { 0, 0, 0, 0, 0, 0 }); + TCHECK_EQ(r0, Int128(0LL), "utcEpoch: 1970-01-01 00:00:00 = 0"); + // 1 day = 86400000000000 ns + auto r1 = getUTCEpochNanoseconds({ 1970, 1, 2 }, { 0, 0, 0, 0, 0, 0 }); + TCHECK_EQ(r1, Int128(86400000000000LL), "utcEpoch: 1970-01-02 = 1 day"); + // 1 second + auto r2 = getUTCEpochNanoseconds({ 1970, 1, 1 }, { 0, 0, 1, 0, 0, 0 }); + TCHECK_EQ(r2, Int128(1000000000LL), "utcEpoch: 1970-01-01 00:00:01 = 1s"); + // 2001-09-09T01:46:40Z = 1000000000 seconds = 1e18 ns + auto r3 = getUTCEpochNanoseconds({ 2001, 9, 9 }, { 1, 46, 40, 0, 0, 0 }); + TCHECK_EQ(r3, Int128(1000000000LL) * Int128(1000000000LL), "utcEpoch: unix billion"); + // Negative: 1969-12-31 = -1 day + auto r4 = getUTCEpochNanoseconds({ 1969, 12, 31 }, { 0, 0, 0, 0, 0, 0 }); + TCHECK_EQ(r4, Int128(-86400000000000LL), "utcEpoch: 1969-12-31 = -1day"); +} + +static void testSplitTimeDuration() +{ + // 25 hours = 90000000000000 ns -> 1 overflow day, 1h remainder + auto [days1, rem1] = splitTimeDuration(Int128(90000000000000LL)); + TCHECK_EQ(days1, 1LL, "split: 25h = 1 overflow day"); + TCHECK_EQ(rem1, Int128(3600000000000LL), "split: 25h remainder = 1h"); + // Exact 1 day + auto [days2, rem2] = splitTimeDuration(Int128(86400000000000LL)); + TCHECK_EQ(days2, 1LL, "split: 1day overflow"); + TCHECK_EQ(rem2, Int128(0LL), "split: 1day remainder=0"); + // Less than 1 day — no overflow + auto [days3, rem3] = splitTimeDuration(Int128(3600000000000LL)); + TCHECK_EQ(days3, 0LL, "split: 1h no overflow"); + TCHECK_EQ(rem3, Int128(3600000000000LL), "split: 1h remainder"); + // Negative: -25h -> floor(-25/24) = -2, remainder = 23h = 82800000000000 ns + auto [days4, rem4] = splitTimeDuration(Int128(-90000000000000LL)); + TCHECK_EQ(days4, -2LL, "split: -25h overflow=-2 (floor)"); + TCHECK_EQ(rem4, Int128(82800000000000LL), "split: -25h remainder=23h"); +} + +static void testPlainTimeFromSubdayNs() +{ + // 0 -> midnight + auto t0 = plainTimeFromSubdayNs(Int128(0)); + TCHECK_EQ(t0.hour(), 0u, "ptFromNs: midnight hour"); + TCHECK_EQ(t0.nanosecond(), 0u, "ptFromNs: midnight ns"); + // 1 hour = 3600000000000 ns -> 01:00:00 + auto t1 = plainTimeFromSubdayNs(Int128(3600000000000LL)); + TCHECK_EQ(t1.hour(), 1u, "ptFromNs: 1h hour"); + TCHECK_EQ(t1.minute(), 0u, "ptFromNs: 1h minute"); + // 1 ns -> 00:00:00.000000001 + auto t2 = plainTimeFromSubdayNs(Int128(1)); + TCHECK_EQ(t2.nanosecond(), 1u, "ptFromNs: 1ns"); + // 13:00:00 = 46800000000000 ns + auto t3 = plainTimeFromSubdayNs(Int128(46800000000000LL)); + TCHECK_EQ(t3.hour(), 13u, "ptFromNs: 13h hour"); +} + +static void testAdd24HourDaysToTimeDuration() +{ + // Add 1 day (86400000000000 ns) to 1h (3600000000000 ns) = 90000000000000 ns + auto r1 = add24HourDaysToTimeDuration(Int128(3600000000000LL), 1.0); + TCHECK_TRUE(r1.has_value(), "add24h: 1h+1d ok"); + TCHECK_EQ(*r1, Int128(90000000000000LL), "add24h: 1h+1d = 25h"); + // Add 0 days -> unchanged + auto r2 = add24HourDaysToTimeDuration(Int128(3600000000000LL), 0.0); + TCHECK_TRUE(r2.has_value(), "add24h: +0d ok"); + TCHECK_EQ(*r2, Int128(3600000000000LL), "add24h: +0d unchanged"); + // Negative days: 25h - 1d = 1h + auto r3 = add24HourDaysToTimeDuration(Int128(90000000000000LL), -1.0); + TCHECK_TRUE(r3.has_value(), "add24h: -1d ok"); + TCHECK_EQ(*r3, Int128(3600000000000LL), "add24h: 25h-1d = 1h"); +} + +static void testTemporalDurationFromInternal() +{ + // 4 days as time nanoseconds -> largestUnit=Day yields 4 days, 0 hours + Int128 fourDays = Int128(4LL) * Int128(86400000000000LL); + auto internal = ISO8601::InternalDuration::combineDateAndTimeDuration(ISO8601::Duration(), fourDays); + auto result = temporalDurationFromInternal(internal, TemporalUnit::Day); + TCHECK_EQ(static_cast(result.days()), 4LL, "fromInternal: 4d days"); + TCHECK_EQ(static_cast(result.hours()), 0LL, "fromInternal: 4d hours=0"); + // largestUnit=Hour: 4 days = 96 hours, 0 days + auto result2 = temporalDurationFromInternal(internal, TemporalUnit::Hour); + TCHECK_EQ(static_cast(result2.hours()), 96LL, "fromInternal: 96h"); + TCHECK_EQ(static_cast(result2.days()), 0LL, "fromInternal: 96h days=0"); +} + +static void testCompareISODateTime() +{ + ISO8601::PlainDate d1 { 2019, 1, 8 }, d2 { 2021, 9, 7 }; + ISO8601::PlainTime t1 { 8, 22, 36, 0, 0, 0 }, t2 { 12, 39, 40, 0, 0, 0 }; + // Equal + TCHECK_EQ(compareISODateTime(d1, t1, d1, t1), 0, "compareIDT: equal"); + // Different date — earlier vs later + TCHECK_EQ(compareISODateTime(d1, t1, d2, t2), -1, "compareIDT: earlier"); + TCHECK_EQ(compareISODateTime(d2, t2, d1, t1), 1, "compareIDT: later"); + // Same date, different time + ISO8601::PlainTime earlyT { 8, 22, 35, 0, 0, 0 }; + TCHECK_EQ(compareISODateTime(d1, earlyT, d1, t1), -1, "compareIDT: earlier time"); +} + static void testMaximumInstantIncrement() { TCHECK_EQ(maximumInstantIncrement(TemporalUnit::Hour), 24.0, "maxInstInc: Hour=24"); @@ -596,6 +861,146 @@ static void testMaximumInstantIncrement() TCHECK_EQ(maximumInstantIncrement(TemporalUnit::Microsecond), 8.64e10, "maxInstInc: µs"); } +static void testToDateDurationRecordWithoutTime() +{ + // Strip time fields, keep date fields + ISO8601::Duration d(1, 2, 0, 4, 5, 6, 7, 8, 9, 10); + auto r = toDateDurationRecordWithoutTime(d); + TCHECK_TRUE(r.has_value(), "stripTime: ok"); + TCHECK_EQ(static_cast(r->years()), 1LL, "stripTime: years"); + TCHECK_EQ(static_cast(r->months()), 2LL, "stripTime: months"); + TCHECK_EQ(static_cast(r->days()), 4LL, "stripTime: days"); + TCHECK_EQ(static_cast(r->hours()), 0LL, "stripTime: hours=0"); + TCHECK_EQ(static_cast(r->minutes()), 0LL, "stripTime: minutes=0"); +} + +// --------------------------------------------------------------------------- +// totalSeconds / totalSubseconds — internal balance helpers +// --------------------------------------------------------------------------- + +static void testTotalSecondsAndSubseconds() +{ + // temporal_rs: internal balance helpers + // 1h30m = 5400s + ISO8601::Duration d1(0, 0, 0, 0, 1, 30, 0, 0, 0, 0); + TCHECK_EQ(totalSeconds(d1), 5400LL, "totalSec: 1h30m=5400s"); + + // 1d2h = 26*3600 = 93600s + ISO8601::Duration d2(0, 0, 0, 1, 2, 0, 0, 0, 0, 0); + TCHECK_EQ(totalSeconds(d2), 93600LL, "totalSec: 1d2h=93600s"); + + // 0 duration -> 0s + ISO8601::Duration z; + TCHECK_EQ(totalSeconds(z), 0LL, "totalSec: zero"); + + // 999ms + 999999µs + 999999999ns = 999*1e6 + 999999*1e3 + 999999999 = 2998998999 ns + ISO8601::Duration d3(0, 0, 0, 0, 0, 0, 0, 999, 999999, 999999999); + Int128 expected = Int128(2998998999LL); + TCHECK_EQ(totalSubseconds(d3), expected, "totalSub: max subseconds"); + + // 1s = 0 subseconds (only ms/µs/ns contribute) + ISO8601::Duration d4(0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + TCHECK_EQ(totalSubseconds(d4), Int128(0LL), "totalSub: 1s=0 subseconds"); +} + +// --------------------------------------------------------------------------- +// totalTimeDuration — fractional unit conversion +// --------------------------------------------------------------------------- + +static void testTotalTimeDuration() +{ + // temporal_rs: internal nanosecond-to-unit conversion + // 3600000000000 ns = 1 hour + TCHECK_EQ(totalTimeDuration(Int128(3600000000000LL), TemporalUnit::Hour), 1.0, "totalTD: 1h"); + // 86400000000000 ns = 1 day + TCHECK_EQ(totalTimeDuration(Int128(86400000000000LL), TemporalUnit::Day), 1.0, "totalTD: 1day"); + // 1000000000 ns = 1 second + TCHECK_EQ(totalTimeDuration(Int128(1000000000LL), TemporalUnit::Second), 1.0, "totalTD: 1s"); + // 90000000000000 ns (25h) in hours = 25.0 + TCHECK_EQ(totalTimeDuration(Int128(90000000000000LL), TemporalUnit::Hour), 25.0, "totalTD: 25h"); + // 1000000 ns = 1 ms + TCHECK_EQ(totalTimeDuration(Int128(1000000LL), TemporalUnit::Millisecond), 1.0, "totalTD: 1ms"); +} + +// --------------------------------------------------------------------------- +// balanceDuration — redistribute time fields +// --------------------------------------------------------------------------- + +static void testBalanceDuration() +{ + // temporal_rs: Duration::balance — redistributes seconds/minutes/hours + // 90min -> 1h30m when largestUnit=Hour + ISO8601::Duration d1(0, 0, 0, 0, 0, 90, 0, 0, 0, 0); + balanceDuration(d1, TemporalUnit::Hour); + TCHECK_EQ(static_cast(d1.hours()), 1LL, "balance: 90m -> 1h"); + TCHECK_EQ(static_cast(d1.minutes()), 30LL, "balance: 90m -> 30m"); + + // 3600s -> 1h when largestUnit=Hour + ISO8601::Duration d2(0, 0, 0, 0, 0, 0, 3600, 0, 0, 0); + balanceDuration(d2, TemporalUnit::Hour); + TCHECK_EQ(static_cast(d2.hours()), 1LL, "balance: 3600s -> 1h"); + TCHECK_EQ(static_cast(d2.seconds()), 0LL, "balance: 3600s -> 0s"); + + // 2000ms -> 2s when largestUnit=Second (ms overflow folds into seconds) + ISO8601::Duration d3(0, 0, 0, 0, 0, 0, 0, 2000, 0, 0); + balanceDuration(d3, TemporalUnit::Second); + TCHECK_EQ(static_cast(d3.seconds()), 2LL, "balance: 2000ms -> 2s"); + TCHECK_EQ(static_cast(d3.milliseconds()), 0LL, "balance: 2000ms -> 0ms"); + + // 500ms with largestUnit=Millisecond -> unchanged + ISO8601::Duration d4(0, 0, 0, 0, 0, 0, 0, 500, 0, 0); + balanceDuration(d4, TemporalUnit::Millisecond); + TCHECK_EQ(static_cast(d4.milliseconds()), 500LL, "balance: 500ms unchanged"); +} + +// --------------------------------------------------------------------------- +// toInternalDuration / toInternalDurationRecordWith24HourDays +// --------------------------------------------------------------------------- + +static void testToInternalDuration() +{ + // temporal_rs: internal conversion helpers + // P1DT2H -> InternalDuration with time portion = 2h in ns, date = 1 day (NOT folded) + ISO8601::Duration d(0, 0, 0, 1, 2, 0, 0, 0, 0, 0); + auto internal = toInternalDuration(d); + TCHECK_EQ(static_cast(internal.dateDuration().days()), 1LL, "toInternal: days=1"); + TCHECK_EQ(internal.time(), Int128(7200000000000LL), "toInternal: time=2h ns"); + + // toInternalDurationRecordWith24HourDays: folds days into time + auto r = toInternalDurationRecordWith24HourDays(d); + TCHECK_TRUE(r.has_value(), "toInternal24h: ok"); + // days folded into time: 1d + 2h = 26h = 93600000000000 ns, date part = 0 days + TCHECK_EQ(static_cast(r->dateDuration().days()), 0LL, "toInternal24h: days=0"); + TCHECK_EQ(r->time(), Int128(93600000000000LL), "toInternal24h: time=26h"); +} + +// --------------------------------------------------------------------------- +// diffISODateTime — unrounded internal duration between datetimes +// --------------------------------------------------------------------------- + +static void testDiffISODateTime() +{ + // temporal_rs: IsoDateTime::diff (unrounded portion) + ISO8601::PlainDate d1 { 2019, 1, 8 }, d2 { 2021, 9, 7 }; + ISO8601::PlainTime t1 { 8, 22, 36, 0, 0, 0 }, t2 { 12, 39, 40, 0, 0, 0 }; + + // 2019-01-08T08:22:36 until 2021-09-07T12:39:40, largestUnit=Day + auto r = diffISODateTime(d1, t1, d2, t2, TemporalUnit::Day); + // 973 days + 4h 17m 4s + TCHECK_EQ(static_cast(r.dateDuration().days()), 973LL, "diffIDT: days=973"); + Int128 expected4h17m4s = timeDurationFromComponents(4, 17, 4, 0, 0, 0); + TCHECK_EQ(r.time(), expected4h17m4s, "diffIDT: time=4h17m4s"); + + // Same datetime -> zero + auto r2 = diffISODateTime(d1, t1, d1, t1, TemporalUnit::Day); + TCHECK_EQ(static_cast(r2.dateDuration().days()), 0LL, "diffIDT: same=0 days"); + TCHECK_EQ(r2.time(), Int128(0LL), "diffIDT: same=0 time"); + + // Negative: later until earlier + auto r3 = diffISODateTime(d2, t2, d1, t1, TemporalUnit::Day); + TCHECK_EQ(static_cast(r3.dateDuration().days()), -973LL, "diffIDT: neg days"); +} + // --------------------------------------------------------------------------- // validateTemporalRoundingIncrement // --------------------------------------------------------------------------- @@ -758,6 +1163,37 @@ static void testISOEpochDayLimits() // 275760-09-14 = abs(days) = MAX_DAYS_BASE + 1 -> out of range auto rMax14 = isoDateAdd({ 275760, 9, 14 }, ISO8601::Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); TCHECK_TRUE(!rMax14.has_value(), "epochDays: 275760-09-14 is out of range"); + + // temporal_rs: test_month_limits + // 1970-01-01 = epoch day 0 + auto rEpoch = getUTCEpochNanoseconds({ 1970, 1, 1 }, { 0, 0, 0, 0, 0, 0 }); + TCHECK_EQ(rEpoch, Int128(0LL), "epochDays: 1970-01-01 epoch ns = 0"); + + // 1969-12-31 = epoch day -1 + auto rPrev = getUTCEpochNanoseconds({ 1969, 12, 31 }, { 0, 0, 0, 0, 0, 0 }); + TCHECK_EQ(rPrev, Int128(-86400000000000LL), "epochDays: 1969-12-31 epoch ns = -1 day"); + + // temporal_rs: iso_date_to_epoch_days_limits — exact day counts + // -271821-04-20: abs(epochDays) = 100_000_000 exactly + auto rD20 = getUTCEpochNanoseconds({ -271821, 4, 20 }, { 0, 0, 0, 0, 0, 0 }); + Int128 nsPerDay = Int128(86400000000000LL); + Int128 days20 = (rD20 < Int128(0LL) ? -rD20 : rD20) / nsPerDay; + TCHECK_EQ(days20, Int128(100000000LL), "epochDays: -271821-04-20 = abs 1e8 days"); + + // -271821-04-19: abs(epochDays) = 100_000_001 + auto rD19 = getUTCEpochNanoseconds({ -271821, 4, 19 }, { 0, 0, 0, 0, 0, 0 }); + Int128 days19 = (rD19 < Int128(0LL) ? -rD19 : rD19) / nsPerDay; + TCHECK_EQ(days19, Int128(100000001LL), "epochDays: -271821-04-19 = abs 1e8+1 days"); + + // 275760-09-13: abs(epochDays) = 100_000_000 exactly + auto rD13 = getUTCEpochNanoseconds({ 275760, 9, 13 }, { 0, 0, 0, 0, 0, 0 }); + Int128 days13 = (rD13 < Int128(0LL) ? -rD13 : rD13) / nsPerDay; + TCHECK_EQ(days13, Int128(100000000LL), "epochDays: 275760-09-13 = abs 1e8 days"); + + // 275760-09-14: abs(epochDays) = 100_000_001 + auto rD14 = getUTCEpochNanoseconds({ 275760, 9, 14 }, { 0, 0, 0, 0, 0, 0 }); + Int128 days14 = (rD14 < Int128(0LL) ? -rD14 : rD14) / nsPerDay; + TCHECK_EQ(days14, Int128(100000001LL), "epochDays: 275760-09-14 = abs 1e8+1 days"); } // --------------------------------------------------------------------------- @@ -796,8 +1232,7 @@ static void testRoundingExactMultiples() TCHECK_EQ(roundNumberToIncrementInt128(Int128(-100), Int128(10), RoundingMode::HalfEven), Int128(-100), "exact: -100 HalfEven=-100"); // temporal_rs: x=-14, inc=3 (non-exact, between -15 and -12, closer to -15) - // -14 / 3: floor = -5 -> -15; ceil = -4 -> -12; remainder = (-14) mod 3 = 1 (one away from -15) - // midpoint of 3 = 1.5, so 1 < 1.5 -> closer to floor (-15) + // -14 / 3: remainder=1 < midpoint 1.5 → rounds toward floor (-15) for all Half* modes TCHECK_EQ(roundNumberToIncrementInt128(Int128(-14), Int128(3), RoundingMode::Ceil), Int128(-12), "14/3: Ceil=-12"); TCHECK_EQ(roundNumberToIncrementInt128(Int128(-14), Int128(3), RoundingMode::Floor), Int128(-15), "14/3: Floor=-15"); TCHECK_EQ(roundNumberToIncrementInt128(Int128(-14), Int128(3), RoundingMode::Expand), Int128(-15), "14/3: Expand=-15"); @@ -882,9 +1317,7 @@ static void testNewDateLimits() static void testDateRoundingIncrement() { - // temporal_rs: rounding_increment_observed — date since with rounding increments - // 2021-09-07 since 2019-01-08, smallest=Year, inc=4, HalfExpand -> 4 years - // Actual diff ≈ 2.66 years -> rounds to 4 (nearest multiple of 4) + // temporal_rs: rounding_increment_observed — diff ≈ 2.66 years, inc=4 HalfExpand → 4 years. { auto diff = diffISODate({ 2019, 1, 8 }, { 2021, 9, 7 }, TemporalUnit::Year); // 2y 7m 29d ≈ 2.66y, rounded to inc=4 -> 4 @@ -902,10 +1335,7 @@ static void testDateRoundingIncrement() TCHECK_EQ(static_cast(roundedMonths), 30LL, "dateRoundInc: months inc=10 HalfExpand=30"); } - // smallest=Week, inc=12, HalfExpand -> 144 weeks - // temporal_rs: rounding_increment_observed — Week case - // 2019-01-08 to 2021-09-07 = 973 days = 139 weeks + 0 days - // 139 / 12 = 11.583... -> HalfExpand -> 12 -> 144 weeks + // temporal_rs: rounding_increment_observed — Week case: 973 days = 139 weeks, inc=12 HalfExpand → 144 weeks. { auto diff = diffISODate({ 2019, 1, 8 }, { 2021, 9, 7 }, TemporalUnit::Week); // 973 days = 139 weeks exactly (973 = 139 * 7) @@ -925,8 +1355,633 @@ static void testDateRoundingIncrement() } } -// Note: testInvalidDateStrings and testCriticalUnknownAnnotation use -// ISO8601::parseCalendarDateTime — confirmed JS_EXPORT_PRIVATE in ISO8601.h. +// --------------------------------------------------------------------------- +// plain_date_time.rs: limits, add/subtract overflow, since conflicting signs, +// round basic +// --------------------------------------------------------------------------- + +static void testPlainDateTimeLimits() +{ + // temporal_rs: plain_date_time_limits + // -271821-04-19 at midnight -> just outside limit (same as date limit check) + auto rErr1 = isoDateAdd({ -271821, 4, 19 }, ISO8601::Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(rErr1.has_value(), "pdtLimits: -271821-04-19 date valid"); + // That date itself is the boundary; with time=noon it's out of range for datetime + // but pure date operations still work. Verify the border-crossing behavior: + // -271821-04-20T00:00:00 is valid (date is within limits) + auto rOk1 = regulateISODate(-271821, 4, 20, TemporalOverflow::Reject); + TCHECK_TRUE(rOk1.has_value(), "pdtLimits: -271821-04-20 ok"); + + // 275760-09-14 -> invalid + auto rErr2 = regulateISODate(275760, 9, 14, TemporalOverflow::Reject); + TCHECK_TRUE(rErr2.has_value(), "pdtLimits: 275760-09-14 date fields valid"); + // date-only regulation doesn't check epoch limits; isoDateAdd does + auto rOver = isoDateAdd({ 275760, 9, 14 }, ISO8601::Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(!rOver.has_value(), "pdtLimits: 275760-09-14 out of range"); + + // 275760-09-13 is the last valid date + auto rMax = isoDateAdd({ 275760, 9, 13 }, ISO8601::Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(rMax.has_value(), "pdtLimits: 275760-09-13 within limits"); + TCHECK_EQ(rMax->year(), 275760, "pdtLimits: max year"); + TCHECK_EQ(rMax->month(), 9u, "pdtLimits: max month"); + TCHECK_EQ(rMax->day(), 13u, "pdtLimits: max day"); +} + +static void testDateTimeAddSubtract() +{ + // temporal_rs: datetime_add_test — 2020-01-31 + P1M -> 2020-02-29 (leap year constrain) + auto rAdd = isoDateAdd({ 2020, 1, 31 }, ISO8601::Duration(0, 1, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(rAdd.has_value(), "dtAddSub: 2020-01-31+1M ok"); + TCHECK_EQ(rAdd->month(), 2u, "dtAddSub: +1M month=2"); + TCHECK_EQ(rAdd->day(), 29u, "dtAddSub: +1M day=29 (leap)"); + + // temporal_rs: datetime_subtract_test — 2000-03-31 - P1M -> 2000-02-29 (Y2K leap) + auto rSub = isoDateAdd({ 2000, 3, 31 }, ISO8601::Duration(0, -1, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(rSub.has_value(), "dtAddSub: 2000-03-31-1M ok"); + TCHECK_EQ(rSub->month(), 2u, "dtAddSub: -1M month=2"); + TCHECK_EQ(rSub->day(), 29u, "dtAddSub: -1M day=29 (Y2K leap)"); + + // temporal_rs: datetime_subtract_hour_overflows — verify date rolls back when subtracting crosses midnight. + { + ISO8601::PlainDate d1 { 2019, 10, 28 }, d2 { 2019, 10, 29 }; + ISO8601::PlainTime t1 { 22, 46, 38, 271, 986, 102 }, t2 { 10, 46, 38, 271, 986, 102 }; + auto diff = diffISODateTime(d1, t1, d2, t2, TemporalUnit::Hour); + Int128 expected12h = timeDurationFromComponents(12, 0, 0, 0, 0, 0); + TCHECK_EQ(diff.time(), expected12h, "dtHourOverflow: diff = 12h"); + } + + // temporal_rs: datetime_add (2024-01-15T12:00 + P1M2DT3H4M = 2024-02-17T15:04) + auto rAdd2 = isoDateAdd({ 2024, 1, 15 }, ISO8601::Duration(0, 1, 0, 2, 3, 4, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(rAdd2.has_value(), "dtAdd: 2024-01-15+P1M2DT3H4M ok"); + TCHECK_EQ(rAdd2->month(), 2u, "dtAdd: month=2"); + TCHECK_EQ(rAdd2->day(), 17u, "dtAdd: day=17"); + // Time portion: hour+3=15, min+4=4 + auto timePart = timeDurationFromComponents(3, 4, 0, 0, 0, 0); + TCHECK_EQ(timePart, Int128(11040000000000LL), "dtAdd: 3h4m in ns"); +} + +static void testDtSinceConflictingSigns() +{ + // temporal_rs: dt_since_conflicting_signs — time sign differs from date sign; adjustedD2 = 2023-02-28. + ISO8601::PlainDate da { 2023, 1, 1 }, db { 2023, 3, 1 }; + ISO8601::PlainTime ta { 3, 0, 0, 0, 0, 0 }, tb { 2, 0, 0, 0, 0, 0 }; + auto r = diffISODateTime(da, ta, db, tb, TemporalUnit::Month); + TCHECK_EQ(static_cast(r.dateDuration().months()), 1LL, "conflictSign: months=1"); + TCHECK_EQ(static_cast(r.dateDuration().days()), 27LL, "conflictSign: days=27 (ISO path)"); + Int128 expected23h = timeDurationFromComponents(23, 0, 0, 0, 0, 0); + TCHECK_EQ(r.time(), expected23h, "conflictSign: time=23h"); +} + +static void testRoundISODateTime() +{ + // RoundTime correctness: quantity is sub-unit-relative, not from-midnight. + // 11:26 rounded to 4-min: sub-minute offset = 26min → 6.5 increments → HalfExpand → 7 → 11:28. + { + ISO8601::PlainDate date(2020, 1, 1); + ISO8601::PlainTime time(11, 26, 0, 0, 0, 0); + auto r = roundISODateTime(date, time, ISO8601::ExactTime::nsPerMinute * 4, TemporalUnit::Minute, RoundingMode::HalfExpand); + TCHECK_EQ(r.time.hour(), 11u, "roundISO: 11:26 +4min HalfExpand -> hour=11"); + TCHECK_EQ(r.time.minute(), 28u, "roundISO: 11:26 +4min HalfExpand -> min=28"); + } + // HalfEven: 11:26 with 4-min -> sub-unit quotient 6.5, r1=6 (even) -> 11:24 + { + ISO8601::PlainDate date(2020, 1, 1); + ISO8601::PlainTime time(11, 26, 0, 0, 0, 0); + auto r = roundISODateTime(date, time, ISO8601::ExactTime::nsPerMinute * 4, TemporalUnit::Minute, RoundingMode::HalfEven); + TCHECK_EQ(r.time.minute(), 24u, "roundISO: 11:26 +4min HalfEven -> min=24 (r1=6 even)"); + } + // Round to hour: 14:30:00 -> 15:00:00 + { + ISO8601::PlainDate date(2020, 1, 1); + ISO8601::PlainTime time(14, 30, 0, 0, 0, 0); + auto r = roundISODateTime(date, time, ISO8601::ExactTime::nsPerHour, TemporalUnit::Hour, RoundingMode::HalfExpand); + TCHECK_EQ(r.time.hour(), 15u, "roundISO: 14:30 +1h HalfExpand -> 15:00"); + TCHECK_EQ(r.time.minute(), 0u, "roundISO: 14:30 +1h -> min=0"); + TCHECK_TRUE(r.date == date, "roundISO: same date"); + } + // Day overflow: 23:45:00 rounded to hour -> next day 00:00:00 + { + ISO8601::PlainDate date(2020, 1, 15); + ISO8601::PlainTime time(23, 45, 0, 0, 0, 0); + auto r = roundISODateTime(date, time, ISO8601::ExactTime::nsPerHour, TemporalUnit::Hour, RoundingMode::HalfExpand); + TCHECK_EQ(r.time.hour(), 0u, "roundISO: 23:45 overflow -> 00:00"); + TCHECK_EQ(r.date.day(), 16u, "roundISO: 23:45 overflow -> next day"); + } + // Second rounding: 14:23:30.600 -> 14:23:31 + { + ISO8601::PlainDate date(2020, 1, 1); + ISO8601::PlainTime time(14, 23, 30, 600, 0, 0); + auto r = roundISODateTime(date, time, ISO8601::ExactTime::nsPerSecond, TemporalUnit::Second, RoundingMode::HalfExpand); + TCHECK_EQ(r.time.second(), 31u, "roundISO: 30.6s HalfExpand -> 31s"); + } +} + +static void testDtRoundBasic() +{ + // temporal_rs: dt_round_basic — 1976-11-18T14:23:30.123456789 + // Rounding to various units with HalfExpand (default) + UNUSED_PARAM(0); // date/time used via roundTimeQuantity + + // Round to Hour inc=4, HalfExpand: 14h23m30.123456789 / 4h ≈ 3.597 → rounds to 4 → 16h + { + Int128 totalNs = Int128(((int64_t)14 * 3600 + (int64_t)23 * 60 + 30) * 1000000000LL) + + Int128(123456789LL); + Int128 inc4h = Int128(4LL * 3600000000000LL); + Int128 rounded = roundNumberToIncrementInt128(totalNs, inc4h, RoundingMode::HalfExpand); + // 57600000000000 = 16h + auto resultTime = plainTimeFromSubdayNs(rounded); + TCHECK_EQ(resultTime.hour(), 16u, "dtRound: inc4h hour=16"); + TCHECK_EQ(resultTime.minute(), 0u, "dtRound: inc4h min=0"); + } + + // Round to Minute inc=15, HalfExpand: sub-hour ns = 23m30.123456789s -> round to 30m + { + // sub-hour nanoseconds = 23*60e9 + 30e9 + 123456789 = 1410123456789 + Int128 subHourNs = Int128((int64_t)23 * 60000000000LL + (int64_t)30 * 1000000000LL + 123456789LL); + Int128 inc15m = Int128(15LL * 60000000000LL); + Int128 rounded = roundNumberToIncrementInt128(subHourNs, inc15m, RoundingMode::HalfExpand); + // 1410123456789 / 900000000000 = 1.567 -> HalfExpand -> 2 -> 1800000000000 = 30m + Int128 base14h = Int128(14LL * 3600000000000LL); + Int128 totalRounded = base14h + rounded; + auto rt = plainTimeFromSubdayNs(totalRounded); + TCHECK_EQ(rt.hour(), 14u, "dtRound: inc15m hour=14"); + TCHECK_EQ(rt.minute(), 30u, "dtRound: inc15m min=30"); + } + + // Round to Nanosecond inc=10, HalfExpand: ns=789 -> 790 + { + Int128 nsOnly = Int128(789LL); + Int128 inc10 = Int128(10LL); + Int128 rounded = roundNumberToIncrementInt128(nsOnly, inc10, RoundingMode::HalfExpand); + TCHECK_EQ(rounded, Int128(790LL), "dtRound: inc10ns ns=790"); + } + + // Round to Millisecond inc=10, HalfExpand: ms=123 -> 120 + { + Int128 msOnly = Int128(123LL * 1000000LL + 456LL * 1000LL + 789LL); + Int128 inc10ms = Int128(10LL * 1000000LL); + Int128 rounded = roundNumberToIncrementInt128(msOnly, inc10ms, RoundingMode::HalfExpand); + // 123456789 / 10000000 = 12.3456789 -> 12 -> 120ms + TCHECK_EQ(rounded / Int128(1000000LL), Int128(120LL), "dtRound: inc10ms=120"); + } + + // Round to Microsecond inc=10, HalfExpand: µs=456 -> 460 + { + Int128 usOnly = Int128(456LL * 1000LL + 789LL); + Int128 inc10us = Int128(10LL * 1000LL); + Int128 rounded = roundNumberToIncrementInt128(usOnly, inc10us, RoundingMode::HalfExpand); + TCHECK_EQ(rounded / Int128(1000LL), Int128(460LL), "dtRound: inc10us=460"); + } +} + +static void testDifferenceTemporalPlainDateTime() +{ + // temporal_rs: dt_until_basic — tests differenceTemporalPlainDateTime directly + ISO8601::PlainDate d1 { 2019, 1, 8 }, d2 { 2021, 9, 7 }; + ISO8601::PlainTime t1 { 8, 22, 36, 123, 456, 789 }, t2 { 12, 39, 40, 987, 654, 321 }; + auto id = calendarIDFromString("iso8601"_s); + + // until: largestUnit=Day, no rounding + { + auto r = differenceTemporalPlainDateTime(DifferenceOperation::Until, d1, t1, d2, t2, + id, TemporalUnit::Nanosecond, TemporalUnit::Day, RoundingMode::HalfExpand, 1); + TCHECK_TRUE(r.has_value(), "dtDiff: until ok"); + TCHECK_EQ(static_cast(r->days()), 973LL, "dtDiff: until days=973"); + } + + // since: result is negated — temporal_rs: dt_since_basic + { + auto r = differenceTemporalPlainDateTime(DifferenceOperation::Since, d2, t2, d1, t1, + id, TemporalUnit::Nanosecond, TemporalUnit::Day, RoundingMode::HalfExpand, 1); + TCHECK_TRUE(r.has_value(), "dtDiff: since ok"); + TCHECK_EQ(static_cast(r->days()), 973LL, "dtDiff: since days=973"); + } + + // equal datetimes -> zero duration (step 5) + { + auto r = differenceTemporalPlainDateTime(DifferenceOperation::Until, d1, t1, d1, t1, + id, TemporalUnit::Nanosecond, TemporalUnit::Day, RoundingMode::HalfExpand, 1); + TCHECK_TRUE(r.has_value() && !r->years() && !r->days(), "dtDiff: equal=zero"); + } + + // with rounding: inc=3h HalfExpand -> 973 days 3 hours (from dt_until_basic) + { + auto r = differenceTemporalPlainDateTime(DifferenceOperation::Until, d1, t1, d2, t2, + id, TemporalUnit::Hour, TemporalUnit::Day, RoundingMode::HalfExpand, 3); + TCHECK_TRUE(r.has_value(), "dtDiff: round 3h ok"); + TCHECK_EQ(static_cast(r->days()), 973LL, "dtDiff: round 3h days=973"); + TCHECK_EQ(static_cast(r->hours()), 3LL, "dtDiff: round 3h hours=3"); + } + + // temporal_rs: dt_since_conflicting_signs + // 2023-03-01T02:00 since 2023-01-01T03:00, largestUnit=Year -> 1 month 30 days 23 hours + { + ISO8601::PlainDate da { 2023, 3, 1 }, db { 2023, 1, 1 }; + ISO8601::PlainTime ta { 2, 0, 0, 0, 0, 0 }, tb { 3, 0, 0, 0, 0, 0 }; + auto r = differenceTemporalPlainDateTime(DifferenceOperation::Since, da, ta, db, tb, + id, TemporalUnit::Nanosecond, TemporalUnit::Year, RoundingMode::HalfExpand, 1); + TCHECK_TRUE(r.has_value(), "dtDiff: conflicting signs ok"); + TCHECK_EQ(static_cast(r->months()), 1LL, "dtDiff: conflicting months=1"); + TCHECK_EQ(static_cast(r->days()), 30LL, "dtDiff: conflicting days=30"); + TCHECK_EQ(static_cast(r->hours()), 23LL, "dtDiff: conflicting hours=23"); + } +} + +static void testDtUntilBasic() +{ + // temporal_rs: dt_until_basic + // 2019-01-08T08:22:36.123456789 until 2021-09-07T12:39:40.987654321 + ISO8601::PlainDate d1 { 2019, 1, 8 }, d2 { 2021, 9, 7 }; + ISO8601::PlainTime t1 { 8, 22, 36, 123, 456, 789 }, t2 { 12, 39, 40, 987, 654, 321 }; + + // largestUnit=Hour, inc=3, HalfExpand -> 973 days, 3 hours + // diff = 973 days + 4h 17m 4s 864ms 197µs 532ns (approx) + auto r = diffISODateTime(d1, t1, d2, t2, TemporalUnit::Day); + TCHECK_EQ(static_cast(r.dateDuration().days()), 973LL, "dtUntil: days=973"); + // time component: 4h 17m 4s 864ms 197µs 532ns + Int128 expectedTime = timeDurationFromComponents(4, 17, 4, 864, 197, 532); + TCHECK_EQ(r.time(), expectedTime, "dtUntil: time=4h17m4s864ms197µs532ns"); + + // Round time to inc=3h, HalfExpand + // time = 4h 17m 4s ... ≈ 4.284h; 4.284/3 ≈ 1.428 -> 3h + Int128 inc3h = Int128(3LL * 3600000000000LL); + Int128 roundedTime = roundNumberToIncrementInt128(r.time(), inc3h, RoundingMode::HalfExpand); + TCHECK_EQ(roundedTime, Int128(3LL * 3600000000000LL), "dtUntil: time rounded inc3h=3h"); + + // Round time to inc=30m, HalfExpand + // 4h17m4s -> 4h17m = 15424s -> 15424/1800 ≈ 8.57 -> 9 -> 4h30m + Int128 inc30m = Int128(30LL * 60000000000LL); + Int128 roundedTime2 = roundNumberToIncrementInt128(r.time(), inc30m, RoundingMode::HalfExpand); + Int128 expected4h30m = timeDurationFromComponents(4, 30, 0, 0, 0, 0); + TCHECK_EQ(roundedTime2, expected4h30m, "dtUntil: time rounded inc30m=4h30m"); +} + +// --------------------------------------------------------------------------- +// duration/tests.rs pure tests: rounding without ZonedDateTime +// --------------------------------------------------------------------------- + +static void testRoundingToDayOnly() +{ + // temporal_rs: rounding_to_fractional_day_tests — 25h splits into 1d + 1h remainder. + { + auto [days, rem] = splitTimeDuration(Int128(90000000000000LL)); + TCHECK_EQ(days, 1LL, "roundDay: 25h split days=1"); + TCHECK_EQ(rem, Int128(3600000000000LL), "roundDay: 25h split rem=1h"); + } + + // 64 days, inc=5, Floor -> 60d + { + Int128 sixtyfour = Int128(64LL); + Int128 inc5 = Int128(5LL); + Int128 result = roundNumberToIncrementInt128(sixtyfour, inc5, RoundingMode::Floor); + TCHECK_EQ(result, Int128(60LL), "roundDay: 64d inc5 Floor=60"); + } + + // 64 days, inc=10, Floor -> 60d + { + Int128 result = roundNumberToIncrementInt128(Int128(64), Int128(10), RoundingMode::Floor); + TCHECK_EQ(result, Int128(60LL), "roundDay: 64d inc10 Floor=60"); + } + + // 64 days, inc=10, Ceil -> 70d + { + Int128 result = roundNumberToIncrementInt128(Int128(64), Int128(10), RoundingMode::Ceil); + TCHECK_EQ(result, Int128(70LL), "roundDay: 64d inc10 Ceil=70"); + } + + // 1000 days, inc=1_000_000_000, Expand -> 1_000_000_000d + { + Int128 result = roundNumberToIncrementInt128(Int128(1000), Int128(1000000000), RoundingMode::Expand); + TCHECK_EQ(result, Int128(1000000000LL), "roundDay: 1000d inc1e9 Expand=1e9"); + } +} + +static void testDurationAddSubtract() +{ + // temporal_rs: basic_add_duration + // P1DT5M + P2DT5M = P3DT10M + { + ISO8601::Duration base(0, 0, 0, 1, 0, 5, 0, 0, 0, 0); + ISO8601::Duration other(0, 0, 0, 2, 0, 5, 0, 0, 0, 0); + // Combine via toInternal + add + auto baseInt = toInternalDurationRecordWith24HourDays(base); + auto otherInt = toInternalDurationRecordWith24HourDays(other); + TCHECK_TRUE(baseInt.has_value(), "durationAdd: base ok"); + TCHECK_TRUE(otherInt.has_value(), "durationAdd: other ok"); + // P1DT5M + P2DT5M = P3DT10M. + Int128 sumTime = baseInt->time() + otherInt->time(); + auto [days, rem] = splitTimeDuration(sumTime); + TCHECK_EQ(days, 3LL, "durationAdd: days=3"); + Int128 expected10m = timeDurationFromComponents(0, 10, 0, 0, 0, 0); + TCHECK_EQ(rem, expected10m, "durationAdd: rem=10m"); + } + + // P1DT5M + P-3DT-15M = P-2DT-10M + { + ISO8601::Duration base(0, 0, 0, 1, 0, 5, 0, 0, 0, 0); + ISO8601::Duration neg(0, 0, 0, -3, 0, -15, 0, 0, 0, 0); + auto baseInt = toInternalDurationRecordWith24HourDays(base); + auto negInt = toInternalDurationRecordWith24HourDays(neg); + TCHECK_TRUE(baseInt.has_value(), "durationAdd: neg base ok"); + TCHECK_TRUE(negInt.has_value(), "durationAdd: neg other ok"); + Int128 sumTime = baseInt->time() + negInt->time(); + auto [days, rem] = splitTimeDuration(sumTime); + TCHECK_EQ(days, -3LL, "durationAdd: neg days=-3 (floor(-173400000000000/86400000000000))"); + // splitTimeDuration uses floor division: -173400000000000 ns → days=-3, rem=23h50m. + Int128 expectedNeg = Int128(-2LL) * Int128(86400000000000LL) - Int128(600000000000LL); + TCHECK_EQ(sumTime, expectedNeg, "durationAdd: neg total=-2d10m"); + } + + // temporal_rs: basic_subtract_duration — P3DT15M - P1DT5M = P2DT10M + { + ISO8601::Duration base(0, 0, 0, 3, 0, 15, 0, 0, 0, 0); + ISO8601::Duration other(0, 0, 0, 1, 0, 5, 0, 0, 0, 0); + auto baseInt = toInternalDurationRecordWith24HourDays(base); + auto otherInt = toInternalDurationRecordWith24HourDays(other); + TCHECK_TRUE(baseInt.has_value(), "durationSub: ok"); + Int128 diffTime = baseInt->time() - otherInt->time(); + auto [days, rem] = splitTimeDuration(diffTime); + TCHECK_EQ(days, 2LL, "durationSub: days=2"); + Int128 expected10m = timeDurationFromComponents(0, 10, 0, 0, 0, 0); + TCHECK_EQ(rem, expected10m, "durationSub: rem=10m"); + } +} + +static void testRoundingCrossBoundary() +{ + // temporal_rs: rounding_cross_boundary — P1Y11M24D Expand/Month → 24 months (2 years). + { + // 2022-01-01 + P1Y11M24D = 2023-12-25 + auto r = isoDateAdd({ 2022, 1, 1 }, ISO8601::Duration(1, 11, 0, 24, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value(), "crossBound: 2022-01-01+P1Y11M24D ok"); + TCHECK_EQ(r->year(), 2023, "crossBound: year=2023"); + TCHECK_EQ(r->month(), 12u, "crossBound: month=12"); + TCHECK_EQ(r->day(), 25u, "crossBound: day=25"); + + // The diff from relative 2022-01-01 to 2023-12-25 in months is 23.8... -> expand -> 24m = 2y + auto diff = diffISODate({ 2022, 1, 1 }, { 2023, 12, 25 }, TemporalUnit::Month); + double months = diff.months(); + double rounded = roundNumberToIncrementDouble(months, 12.0, RoundingMode::Expand); + // 23 months -> 24 months = 2 years + TCHECK_EQ(static_cast(rounded / 12.0), 2LL, "crossBound: months/12 rounded=2y"); + } + + // temporal_rs: rounding_cross_boundary_time_units — P0DT1H59M59.9S, Expand, smallest=Second -> P2H + { + // 1h59m59.900s in nanoseconds = (1*3600 + 59*60 + 59)*1e9 + 900000000 = 7199900000000 + 900000000 = 7199900000000 (approx) + Int128 t = timeDurationFromComponents(1, 59, 59, 900, 0, 0); + // = (1*3600+59*60+59)*1e9 + 900e6 = 7199*1e9 + 900e6 = 7199000000000+900000000 = 7199900000000 + Int128 expected = Int128(7199900000000LL); + TCHECK_EQ(t, expected, "crossBoundTime: 1h59m59.900s ns"); + // Round to second, Expand: 7199900000000 / 1000000000 = 7199.9 -> 7200s = 2h + Int128 inc1s = Int128(1000000000LL); + Int128 rounded = roundNumberToIncrementInt128(t, inc1s, RoundingMode::Expand); + TCHECK_EQ(rounded, Int128(7200000000000LL), "crossBoundTime: Expand->7200s=2h"); + auto rt = plainTimeFromSubdayNs(rounded); + TCHECK_EQ(rt.hour(), 2u, "crossBoundTime: hour=2"); + TCHECK_EQ(rt.minute(), 0u, "crossBoundTime: min=0"); + } + + // Negative: P-1H-59M-59.9S, Expand -> -2h + { + Int128 tneg = timeDurationFromComponents(-1, -59, -59, -900, 0, 0); + Int128 expectedNeg = Int128(-7199900000000LL); + TCHECK_EQ(tneg, expectedNeg, "crossBoundTimeNeg: -1h59m59.900s ns"); + Int128 roundedNeg = roundNumberToIncrementInt128(tneg, Int128(1000000000LL), RoundingMode::Expand); + TCHECK_EQ(roundedNeg, Int128(-7200000000000LL), "crossBoundTimeNeg: Expand->-7200s=-2h"); + } +} + +static void testBubbleSmallestBecomesDay() +{ + // temporal_rs: bubble_smallest_becomes_day — P14H, inc=12h, Ceil -> P24H (bubbles to next day) + // 14h / 12h = 1.166... -> Ceil -> 2 -> 24h + { + Int128 t14h = timeDurationFromComponents(14, 0, 0, 0, 0, 0); + Int128 inc12h = Int128(12LL * 3600000000000LL); + Int128 rounded = roundNumberToIncrementInt128(t14h, inc12h, RoundingMode::Ceil); + TCHECK_EQ(rounded, Int128(24LL * 3600000000000LL), "bubble: 14h Ceil inc12h = 24h"); + // The result is 24h which is 1 full day + auto [days, rem] = splitTimeDuration(rounded); + TCHECK_EQ(days, 1LL, "bubble: 24h = 1 day overflow"); + TCHECK_EQ(rem, Int128(0LL), "bubble: 24h rem = 0"); + } +} + +static void testRoundZeroDuration() +{ + // temporal_rs: round_zero_duration — zero duration rounded to any unit = zero + Int128 zero(0LL); + TCHECK_EQ(roundNumberToIncrementInt128(zero, Int128(3600000000000LL), RoundingMode::HalfExpand), Int128(0LL), "roundZero: 0 inc1h = 0"); + TCHECK_EQ(roundNumberToIncrementInt128(zero, Int128(86400000000000LL), RoundingMode::Floor), Int128(0LL), "roundZero: 0 inc1d = 0"); + TCHECK_EQ(roundNumberToIncrementInt128(zero, Int128(1000000000LL), RoundingMode::Ceil), Int128(0LL), "roundZero: 0 inc1s = 0"); + TCHECK_EQ(roundNumberToIncrementInt128(zero, Int128(1LL), RoundingMode::Expand), Int128(0LL), "roundZero: 0 inc1ns = 0"); +} + +static void testRoundIncrementRegression() +{ + // temporal_rs: round_increment_regression_test — 48h, inc=2days, no relativeTo + // 48h = 2 × 86400000000000 ns exactly divisible by 2d + Int128 h48 = timeDurationFromComponents(48, 0, 0, 0, 0, 0); + Int128 inc2d = Int128(2LL * 86400000000000LL); + Int128 result = roundNumberToIncrementInt128(h48, inc2d, RoundingMode::HalfExpand); + TCHECK_EQ(result, Int128(2LL * 86400000000000LL), "roundReg: 48h inc=2d = 2d"); + // splitTimeDuration: 2d exactly + auto [days, rem] = splitTimeDuration(result); + TCHECK_EQ(days, 2LL, "roundReg: days=2"); + TCHECK_EQ(rem, Int128(0LL), "roundReg: rem=0"); +} + +static void testDurationTotalBasic() +{ + // temporal_rs: test_duration_total — basic totals without ZonedDateTime + // 130h20m = total seconds = 130*3600 + 20*60 = 468000+1200 = 469200s + Int128 h130m20 = timeDurationFromComponents(130, 20, 0, 0, 0, 0); + TCHECK_EQ(totalTimeDuration(h130m20, TemporalUnit::Second), 469200.0, "durationTotal: 130h20m = 469200s"); + + // PT123456789S = 123456789s = 1428.898... days + Int128 s123456789 = timeDurationFromComponents(0, 0, 123456789, 0, 0, 0); + double days = totalTimeDuration(s123456789, TemporalUnit::Day); + // 123456789s / 86400s = 1428.8980208... + TCHECK_TRUE(days > 1428.89 && days < 1428.90, "durationTotal: 123456789s in days"); + + // balance_subseconds positive: 999ms+999999µs+999999999ns = 2.998998999s + Int128 subsec = timeDurationFromComponents(0, 0, 0, 999, 999999, 999999999); + double secs = totalTimeDuration(subsec, TemporalUnit::Second); + TCHECK_TRUE(secs > 2.998 && secs < 2.999, "durationTotal: balance subseconds pos"); + + // balance_subseconds negative: -999ms-999999µs-999999999ns = -2.998998999s + Int128 negSubsec = timeDurationFromComponents(0, 0, 0, -999, -999999, -999999999); + double negSecs = totalTimeDuration(negSubsec, TemporalUnit::Second); + TCHECK_TRUE(negSecs < -2.998 && negSecs > -2.999, "durationTotal: balance subseconds neg"); +} + +static void testDurationTotalWithRelativeTo() +{ + // temporal_rs: balance_days_up_to_both_years_and_months — relativeTo=PlainDate + // 11 months + 396 days from 2017-01-01 = exactly 2 years + // This verifies that calendarDateAdd folds correctly + auto r1 = isoDateAdd({ 2017, 1, 1 }, ISO8601::Duration(0, 11, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(r1.has_value(), "durationRelTo: +11m ok"); + TCHECK_EQ(r1->year(), 2017, "durationRelTo: +11m year=2017"); + TCHECK_EQ(r1->month(), 12u, "durationRelTo: +11m month=12"); + + // Then add 396 days from 2017-12-01 + auto r2 = isoDateAdd({ 2017, 12, 1 }, ISO8601::Duration(0, 0, 0, 396, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(r2.has_value(), "durationRelTo: +396d ok"); + TCHECK_EQ(r2->year(), 2019, "durationRelTo: +396d year=2019"); + TCHECK_EQ(r2->month(), 1u, "durationRelTo: +396d month=1"); + // 2017-12-01 + 396d lands in 2019-01 (verified below). + + // From 2017-01-01: adding P0M11D + 396 days = ending at 2019-01-01 + // diff from 2017-01-01 to 2019-01-01 = 2 years + auto diff = diffISODate({ 2017, 1, 1 }, { 2019, 1, 1 }, TemporalUnit::Year); + TCHECK_EQ(static_cast(diff.years()), 2LL, "durationRelTo: 2019-2017=2 years"); + + // Negative: -11m-396d from 2017-01-01 = -2 years + auto r3 = isoDateAdd({ 2017, 1, 1 }, ISO8601::Duration(0, -11, 0, -396, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(r3.has_value(), "durationRelTo: -11m-396d ok"); + TCHECK_EQ(r3->year(), 2015, "durationRelTo: -11m-396d year=2015"); + TCHECK_EQ(r3->month(), 1u, "durationRelTo: -11m-396d month=1"); + + auto diffNeg = diffISODate({ 2015, 1, 1 }, { 2017, 1, 1 }, TemporalUnit::Year); + TCHECK_EQ(static_cast(diffNeg.years()), 2LL, "durationRelTo: diff=2 years (neg path)"); +} + +static void testAddNormTimeDurationOutOfRange() +{ + // temporal_rs: add_normalized_time_duration_out_of_range + // maxTimeDuration ≈ 9007199254740992e9 ns; to exceed it, need days > ~104249991374.3 + // Use 104249991375 days which is just over the limit + auto r = add24HourDaysToTimeDuration(Int128(0), 104249991375.0); + TCHECK_TRUE(!r.has_value(), "outOfRange: 104249991375 days rejects"); +} + +// --------------------------------------------------------------------------- +// test_rounding_boundaries — temporal_rs duration/tests.rs +// Overflow detection when Duration.round is called with extreme calendar values +// --------------------------------------------------------------------------- + +static void testRoundingBoundaries() +{ + // temporal_rs: test_rounding_boundaries + // Duration with calendar fields at u32::MAX - 1 = 4294967294 from relativeTo 2000-01-01. + // Each case should produce an epoch-day overflow in isoDateAdd/balanceISOYearMonth. + + // year overflow: 2000 + 4294967294 >> maxYear=275760 -> balanceISOYearMonth clamps to outOfRangeYear + { + auto r = isoDateAdd({ 2000, 1, 1 }, ISO8601::Duration(4294967294.0, 0, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(!r.has_value(), "roundBounds: year=4294967294 from 2000 rejects"); + } + + // month overflow: 4294967294 months / 12 ≈ 357913941 years, total year >> maxYear + { + auto r = isoDateAdd({ 2000, 1, 1 }, ISO8601::Duration(0, 4294967294.0, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(!r.has_value(), "roundBounds: month=4294967294 from 2000 rejects"); + } + + // week overflow: 4294967294 weeks × 7 = 30064771058 days from 2000-01-01 + // Total epoch day ≈ 30064782015 >> Temporal limit 1e8 -> isDateTimeWithinLimits rejects + { + auto r = isoDateAdd({ 2000, 1, 1 }, ISO8601::Duration(0, 0, 4294967294.0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(!r.has_value(), "roundBounds: week=4294967294 from 2000 rejects"); + } + + // days overflow: 104249991374 days (= max safe days) from 2000-01-01 + // 2000-01-01 epoch day ≈ 10957, total ≈ 104249991374 + 10957 >> 1e8 limit + { + auto r = isoDateAdd({ 2000, 1, 1 }, ISO8601::Duration(0, 0, 0, 104249991374.0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(!r.has_value(), "roundBounds: day=104249991374 from 2000 rejects"); + } + + // Combined extreme: years + months + weeks + days all extreme + { + auto r = isoDateAdd({ 2000, 1, 1 }, ISO8601::Duration(4294967294.0, 4294967294.0, 4294967294.0, 104249991374.0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(!r.has_value(), "roundBounds: all-extreme from 2000 rejects"); + } + + // Negative extreme: all fields at -(u32::MAX - 1) + { + auto r = isoDateAdd({ 2000, 1, 1 }, ISO8601::Duration(-4294967294.0, -4294967294.0, -4294967294.0, -104249991374.0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Reject); + TCHECK_TRUE(!r.has_value(), "roundBounds: negative all-extreme from 2000 rejects"); + } +} + +// --------------------------------------------------------------------------- +// test_duration_compare_boundary — temporal_rs duration/tests.rs +// Overflow detection: Duration with weeks=1, days=max_days exceeds timeDuration limit +// --------------------------------------------------------------------------- + +static void testDurationCompareBoundary() +{ + // temporal_rs: test_duration_compare_boundary — weeks=1 + max_days folds to max_days+7, exceeding the limit. + + const double maxDays = 104249991374.0; // 2^53 / 86400 + + // weeks=1, days=max_days -> total = 1*7 + max_days = max_days + 7 -> overflow + { + // Equivalent to: toInternalDurationRecordWith24HourDays(Duration{weeks=1, days=maxDays}) + // Time portion = 0 (no hours), then add (1*7 + maxDays) days = maxDays + 7 + auto r = add24HourDaysToTimeDuration(Int128(0), maxDays + 7.0); + TCHECK_TRUE(!r.has_value(), "compareBound: maxDays+7 days rejects"); + } + + // Just at the boundary: max_days alone (no weeks) is still within limit + { + // temporal_rs: max is inclusive — exactly max_days succeeds. + auto r = add24HourDaysToTimeDuration(Int128(0), maxDays); + TCHECK_TRUE(r.has_value(), "compareBound: exactly maxDays ok"); + } + + // Just one over the boundary + { + auto r = add24HourDaysToTimeDuration(Int128(0), maxDays + 1.0); + TCHECK_TRUE(!r.has_value(), "compareBound: maxDays+1 rejects"); + } + + // Negative: -(maxDays + 7) also overflows + { + auto r = add24HourDaysToTimeDuration(Int128(0), -(maxDays + 7.0)); + TCHECK_TRUE(!r.has_value(), "compareBound: -(maxDays+7) days rejects"); + } +} + +// --------------------------------------------------------------------------- +// add_large_durations — ports temporal_rs duration/tests.rs add_large_durations +// Tests that huge durations added via non-ISO calendarDateAdd fail correctly. +// Uses dangi calendar (lunisolar) with exact temporal_rs overflow values. +// --------------------------------------------------------------------------- + +static void testAddLargeDurations() +{ + // temporal_rs: add_large_durations (duration/tests.rs) + // Base date: 2000-01-01 (ISO, which corresponds to dangi calendar) + ISO8601::PlainDate base { 2000, 1, 1 }; + + // Case 1: Duration(years=4294901760, months=256) -> overflow + auto r1 = calendarDateAdd(calendarIDFromString("dangi"_s), base, + ISO8601::Duration(4294901760.0, 256, 0, 0, 0, 0, 0, 0, 0, 0), + TemporalOverflow::Constrain); + TCHECK_TRUE(!r1.has_value(), "addLarge: dangi 4294901760y+256m rejects"); + + // Case 2: Duration(weeks=2516582400, days=8589934592) -> overflow + auto r2 = calendarDateAdd(calendarIDFromString("dangi"_s), base, + ISO8601::Duration(0, 0, 2516582400.0, 8589934592.0, 0, 0, 0, 0, 0, 0), + TemporalOverflow::Constrain); + TCHECK_TRUE(!r2.has_value(), "addLarge: dangi 2516582400w+8589934592d rejects"); + + // Case 3: Duration(years=2046820352) -> overflow + auto r3 = calendarDateAdd(calendarIDFromString("dangi"_s), base, + ISO8601::Duration(2046820352.0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + TemporalOverflow::Constrain); + TCHECK_TRUE(!r3.has_value(), "addLarge: dangi 2046820352y rejects"); + + // Case 4: Duration(weeks=2516582400) -> overflow + auto r4 = calendarDateAdd(calendarIDFromString("dangi"_s), base, + ISO8601::Duration(0, 0, 2516582400.0, 0, 0, 0, 0, 0, 0, 0), + TemporalOverflow::Constrain); + TCHECK_TRUE(!r4.has_value(), "addLarge: dangi 2516582400w rejects"); +} // --------------------------------------------------------------------------- // invalid_strings — mirrors temporal_rs plain_date.rs test invalid_strings @@ -1087,13 +2142,115 @@ static void testCalendarISO8601Fields() TCHECK_EQ(*rMIY, 12, "calMIY: iso8601=12"); } +static void testCalendarFieldsFunctions() +{ + using MC = ParsedMonthCode; + auto id = calendarIDFromString; + CalendarFieldsIn f; + + // --- yearMonthFromFields --- + // temporal_rs: test_plain_year_month_with (basic field construction) + f = { }; + f.year = 1999; + f.month = 12; + auto rYM = yearMonthFromFields(id("iso8601"_s), f, TemporalOverflow::Reject); + TCHECK_TRUE(rYM.has_value() && rYM->isoDate.year() == 1999 && rYM->isoDate.month() == 12 && rYM->isoDate.day() == 1, "yearMonthFromFields: iso 1999-12"); + + // yearMonthFromFields: constrain out-of-range month + f = { }; + f.year = 2020; + f.month = 13; + auto rYMC = yearMonthFromFields(id("iso8601"_s), f, TemporalOverflow::Constrain); + TCHECK_TRUE(rYMC.has_value() && rYMC->isoDate.month() == 12, "yearMonthFromFields: constrain month 13->12"); + + // yearMonthFromFields: reject out-of-range month + f = { }; + f.year = 2020; + f.month = 13; + auto rYMR = yearMonthFromFields(id("iso8601"_s), f, TemporalOverflow::Reject); + TCHECK_TRUE(!rYMR.has_value(), "yearMonthFromFields: reject month 13 -> error"); + + // --- monthDayFromFields --- + // temporal_rs: month_day_from_fields basic case + f = { }; + f.monthCode = MC { 3, false }; // M03 + f.day = 15; + auto rMD = monthDayFromFields(id("iso8601"_s), f, TemporalOverflow::Reject); + TCHECK_TRUE(rMD.has_value() && rMD->isoDate.month() == 3 && rMD->isoDate.day() == 15, "monthDayFromFields: M03 day=15"); + + // monthDayFromFields: constrain day — reference year 1972 is a leap year, so Feb has 29 days + f = { }; + f.monthCode = MC { 2, false }; // Feb + f.day = 30; + auto rMDC = monthDayFromFields(id("iso8601"_s), f, TemporalOverflow::Constrain); + TCHECK_TRUE(rMDC.has_value() && rMDC->isoDate.month() == 2 && rMDC->isoDate.day() == 29, "monthDayFromFields: constrain Feb 30->29 (ref year 1972 is leap)"); + + // --- differenceYearMonth --- + // temporal_rs: test_year_month_diff_range + auto rDiff = differenceYearMonth(id("iso8601"_s), { 2020, 1, 1 }, { 2021, 3, 1 }, TemporalUnit::Month); + TCHECK_TRUE(rDiff.has_value() && rDiff->months() == 14, "differenceYearMonth: 14 months"); + + // differenceYearMonth large span + auto rDiffY = differenceYearMonth(id("iso8601"_s), { 2020, 1, 1 }, { 2022, 1, 1 }, TemporalUnit::Year); + TCHECK_TRUE(rDiffY.has_value() && rDiffY->years() == 2, "differenceYearMonth: 2 years"); + + // differenceYearMonth: day=1 out of range — temporal_rs: test_year_month_diff_range + // min PlainYearMonth is -271821-04; setting day=1 gives -271821-04-01 which is before Temporal min (-271821-04-20) + auto rDiffLimit = differenceYearMonth(id("iso8601"_s), { -271821, 4, 20 }, { 1970, 1, 1 }, TemporalUnit::Year); + TCHECK_TRUE(!rDiffLimit.has_value(), "differenceYearMonth: min PlainYearMonth day=1 out of range"); + + // --- plainYearMonthAdd --- + // Add P1Y to 2020-06 -> 2021-06 + auto rAdd = plainYearMonthAdd(id("iso8601"_s), { 2020, 6, 1 }, ISO8601::Duration(1, 0, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(rAdd.has_value() && rAdd->isoDate.year() == 2021 && rAdd->isoDate.month() == 6, "plainYearMonthAdd: +1Y"); + + // Add P3M to 2020-11 -> 2021-02 + auto rAdd2 = plainYearMonthAdd(id("iso8601"_s), { 2020, 11, 1 }, ISO8601::Duration(0, 3, 0, 0, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(rAdd2.has_value() && rAdd2->isoDate.year() == 2021 && rAdd2->isoDate.month() == 2, "plainYearMonthAdd: +3M rollover"); + + // --- plainYearMonthToPlainDate --- + // temporal_rs: PlainYearMonth::to_plain_date + auto rYMToD = plainYearMonthToPlainDate(id("iso8601"_s), { 2020, 6, 1 }, 15); + TCHECK_TRUE(rYMToD.has_value() && rYMToD->isoDate.year() == 2020 && rYMToD->isoDate.month() == 6 && rYMToD->isoDate.day() == 15, "plainYearMonthToPlainDate: 2020-06 day=15"); + + // --- plainYearMonthFromISODate --- + auto rYMFrom = plainYearMonthFromISODate(id("iso8601"_s), { 2020, 6, 15 }); + TCHECK_TRUE(rYMFrom.has_value() && rYMFrom->isoDate.year() == 2020 && rYMFrom->isoDate.month() == 6 && rYMFrom->isoDate.day() == 1, "plainYearMonthFromISODate: day -> 1"); + + // --- plainMonthDayFromISODate --- + auto rMDFrom = plainMonthDayFromISODate(id("iso8601"_s), { 2020, 6, 15 }, TemporalOverflow::Reject); + TCHECK_TRUE(rMDFrom.has_value() && rMDFrom->isoDate.month() == 6 && rMDFrom->isoDate.day() == 15, "plainMonthDayFromISODate: month=6 day=15"); + + // --- plainMonthDayToPlainDate --- + auto rMDToD = plainMonthDayToPlainDate(id("iso8601"_s), { 1972, 6, 15 }, 2020); + TCHECK_TRUE(rMDToD.has_value() && rMDToD->isoDate.year() == 2020 && rMDToD->isoDate.month() == 6 && rMDToD->isoDate.day() == 15, "plainMonthDayToPlainDate: day=15 year=2020"); + + // --- plainYearMonthWith --- + // temporal_rs: test_plain_year_month_with — override year only + CalendarFieldsIn partialYear; + partialYear.year = 2001; + auto rWith = plainYearMonthWith(id("iso8601"_s), { 2025, 3, 1 }, partialYear, TemporalOverflow::Constrain); + TCHECK_TRUE(rWith.has_value() && rWith->isoDate.year() == 2001 && rWith->isoDate.month() == 3, "plainYearMonthWith: override year -> 2001-03"); + + // override month only + CalendarFieldsIn partialMonth; + partialMonth.month = 7; + auto rWith2 = plainYearMonthWith(id("iso8601"_s), { 2025, 3, 1 }, partialMonth, TemporalOverflow::Constrain); + TCHECK_TRUE(rWith2.has_value() && rWith2->isoDate.year() == 2025 && rWith2->isoDate.month() == 7, "plainYearMonthWith: override month -> 2025-07"); + + // empty partial fields: ISO falls back year+monthCode from current, succeeds + CalendarFieldsIn emptyPartial; + auto rWithEmpty = plainYearMonthWith(id("iso8601"_s), { 2025, 3, 1 }, emptyPartial, TemporalOverflow::Constrain); + TCHECK_TRUE(!rWithEmpty.has_value(), "plainYearMonthWith: empty partial -> TypeError (temporal_rs: fields.is_empty())"); +} + static void testCalendarDateFromFields() { using MC = ParsedMonthCode; auto id = calendarIDFromString; // --- Non-lunisolar: year + month + day --- - // Gregory year→ISO + // Gregory year->ISO auto r = calendarDateFromFields(id("gregory"_s), 2024, 3, 15, std::nullopt, std::nullopt, std::nullopt, TemporalOverflow::Reject); TCHECK_TRUE(r.has_value() && r->year() == 2024 && r->month() == 3 && r->day() == 15, "gregory: year+month+day"); @@ -1118,7 +2275,7 @@ static void testCalendarDateFromFields() auto rRoc = calendarDateFromFields(id("roc"_s), 113, 1, 1, std::nullopt, std::nullopt, std::nullopt, TemporalOverflow::Reject); TCHECK_TRUE(rRoc.has_value() && rRoc->year() == 2024, "roc: year 113 = 2024"); - // ROC: year 0 → broc era (ISO 1911) + // ROC: year 0 -> broc era (ISO 1911) auto rRocBroc = calendarDateFromFields(id("roc"_s), 0, 1, 1, std::nullopt, std::nullopt, std::nullopt, TemporalOverflow::Reject); TCHECK_TRUE(rRocBroc.has_value() && rRocBroc->year() == 1911, "roc: year 0 = ISO 1911"); @@ -1132,26 +2289,26 @@ static void testCalendarDateFromFields() auto rHebLeap = calendarDateFromFields(id("hebrew"_s), 5784, 0, 1, std::nullopt, std::nullopt, MC { 5, true }, TemporalOverflow::Reject); TCHECK_TRUE(rHebLeap.has_value(), "hebrew: M05L in leap year 5784"); - // Hebrew 5783 is NOT a leap year; M05L constrain → same month + // Hebrew 5783 is NOT a leap year; M05L constrain -> same month auto rHebConstrain = calendarDateFromFields(id("hebrew"_s), 5783, 0, 1, std::nullopt, std::nullopt, MC { 5, true }, TemporalOverflow::Constrain); TCHECK_TRUE(rHebConstrain.has_value(), "hebrew: M05L constrain in non-leap year 5783"); - // Hebrew 5783 non-leap + M05L reject → error + // Hebrew 5783 non-leap + M05L reject -> error auto rHebReject = calendarDateFromFields(id("hebrew"_s), 5783, 0, 1, std::nullopt, std::nullopt, MC { 5, true }, TemporalOverflow::Reject); TCHECK_TRUE(!rHebReject.has_value(), "hebrew: M05L reject in non-leap year"); // --- Overflow: constrain --- - // Gregory: day 32 in January → day 31 + // Gregory: day 32 in January -> day 31 auto rConstrain = calendarDateFromFields(id("gregory"_s), 2024, 1, 32, std::nullopt, std::nullopt, std::nullopt, TemporalOverflow::Constrain); - TCHECK_TRUE(rConstrain.has_value() && rConstrain->day() == 31, "gregory: day 32 constrain → 31"); + TCHECK_TRUE(rConstrain.has_value() && rConstrain->day() == 31, "gregory: day 32 constrain -> 31"); - // Gregory: day 32 in January reject → error + // Gregory: day 32 in January reject -> error auto rReject = calendarDateFromFields(id("gregory"_s), 2024, 1, 32, std::nullopt, std::nullopt, std::nullopt, TemporalOverflow::Reject); - TCHECK_TRUE(!rReject.has_value(), "gregory: day 32 reject → error"); + TCHECK_TRUE(!rReject.has_value(), "gregory: day 32 reject -> error"); // --- Invalid era --- auto rBadEra = calendarDateFromFields(id("gregory"_s), 0, 1, 1, StringView("invalid"_s), 2024, std::nullopt, TemporalOverflow::Reject); - TCHECK_TRUE(!rBadEra.has_value(), "gregory: invalid era → error"); + TCHECK_TRUE(!rBadEra.has_value(), "gregory: invalid era -> error"); } static void testCalendarICUNonISO() @@ -1264,6 +2421,82 @@ static void testExactTimeToLocalDateAndTime() TCHECK_EQ(time.hour(), 19u, "localDT: UTC-5 epoch hour"); } +static void testInterpretISODateTimeOffset() +{ + // temporal_rs: interpret_isodatetime_offset tested via zdt_from_partial, zdt_offset_match_minutes + auto utcOpt = ISO8601::parseTemporalTimeZoneIdentifier("UTC"_s); + if (!utcOpt) { + fprintf(stderr, "SKIP [interpretISODateTimeOffset]: UTC not available\n"); + return; + } + auto utc = *utcOpt; + // 2020-01-01T12:00:00Z = 1577880000000000000 ns + constexpr Int128 epoch2020Jan1Noon { 1577880000000000000LL }; + + // Step 1: start-of-day -> getStartOfDay + { + auto r = interpretISODateTimeOffset({ 2020, 1, 1 }, { }, true, OffsetBehaviour::Wall, + TemporalOffsetDisambiguation::Ignore, 0, false, utc, TemporalDisambiguation::Compatible); + TCHECK_TRUE(r.has_value(), "interpretISO: start-of-day ok"); + TCHECK_EQ(r->epochNanoseconds(), Int128(1577836800000000000LL), "interpretISO: start-of-day = midnight UTC"); + } + + // Step 3: Wall -> GetEpochNanosecondsFor (ignore offset entirely) + { + auto r = interpretISODateTimeOffset({ 2020, 1, 1 }, { 12, 0, 0, 0, 0, 0 }, false, + OffsetBehaviour::Wall, TemporalOffsetDisambiguation::Ignore, 3600000000000LL, false, + utc, TemporalDisambiguation::Compatible); + TCHECK_TRUE(r.has_value(), "interpretISO: Wall ok"); + TCHECK_EQ(r->epochNanoseconds(), epoch2020Jan1Noon, "interpretISO: Wall = noon UTC (offset ignored)"); + } + + // Step 4: Use -> epoch = GetUTCEpochNanoseconds(date, time) - offset + // 2020-01-01T12:00:00+01:00 -> epoch = noon_UTC - 1h = 11:00 UTC + { + constexpr Int128 epoch2020Jan1_11UTC { 1577876400000000000LL }; + auto r = interpretISODateTimeOffset({ 2020, 1, 1 }, { 12, 0, 0, 0, 0, 0 }, false, + OffsetBehaviour::Option, TemporalOffsetDisambiguation::Use, 3600000000000LL, false, + utc, TemporalDisambiguation::Compatible); + TCHECK_TRUE(r.has_value(), "interpretISO: Use ok"); + TCHECK_EQ(r->epochNanoseconds(), epoch2020Jan1_11UTC, "interpretISO: Use = noon - 1h offset"); + } + + // Step 10b: Prefer — offset matches UTC (0), returns the UTC candidate + { + auto r = interpretISODateTimeOffset({ 2020, 1, 1 }, { 12, 0, 0, 0, 0, 0 }, false, + OffsetBehaviour::Option, TemporalOffsetDisambiguation::Prefer, 0, false, + utc, TemporalDisambiguation::Compatible); + TCHECK_TRUE(r.has_value(), "interpretISO: Prefer UTC=0 ok"); + TCHECK_EQ(r->epochNanoseconds(), epoch2020Jan1Noon, "interpretISO: Prefer UTC=0 = noon UTC"); + } + + // Step 11: Reject — offset (+1h) doesn't match UTC (0) -> error + { + auto r = interpretISODateTimeOffset({ 2020, 1, 1 }, { 12, 0, 0, 0, 0, 0 }, false, + OffsetBehaviour::Option, TemporalOffsetDisambiguation::Reject, 3600000000000LL, false, + utc, TemporalDisambiguation::Compatible); + TCHECK_TRUE(!r.has_value(), "interpretISO: Reject mismatch -> error"); + } + + // Step 10c: match-minutes — Africa/Monrovia has offset -44:30 in 1970; -45:00 (rounded) is accepted + // temporal_rs: zdt_offset_match_minutes test + auto monroviaOpt = ISO8601::parseTemporalTimeZoneIdentifier("Africa/Monrovia"_s); + if (monroviaOpt) { + constexpr int64_t minus44m30s = -(44 * 60 + 30) * 1000000000LL; // -44min 30sec in ns + constexpr int64_t minus45m = -(45 * 60) * 1000000000LL; // -45min in ns + // Exact match (-44:30) accepted — has sub-minute precision -> matchMinutes=false (exact match only) + auto rExact = interpretISODateTimeOffset({ 1970, 1, 1 }, { 0, 0, 0, 0, 0, 0 }, false, + OffsetBehaviour::Option, TemporalOffsetDisambiguation::Reject, minus44m30s, true, + *monroviaOpt, TemporalDisambiguation::Compatible); + TCHECK_TRUE(rExact.has_value(), "interpretISO: Monrovia exact -44:30 accepted"); + // Rounded match (-45:00) accepted with matchMinutes=true — no sub-minute precision -> matchMinutes=true + auto rRounded = interpretISODateTimeOffset({ 1970, 1, 1 }, { 0, 0, 0, 0, 0, 0 }, false, + OffsetBehaviour::Option, TemporalOffsetDisambiguation::Reject, minus45m, false, + *monroviaOpt, TemporalDisambiguation::Compatible); + TCHECK_TRUE(rRounded.has_value(), "interpretISO: Monrovia rounded -45:00 accepted (match-minutes)"); + } +} + static void testGetOffsetNanosecondsForUTC() { // UTC-offset timezone with offset=0 always returns 0 @@ -1312,6 +2545,228 @@ static void testTimeZoneICUWithIANA() TCHECK_EQ(rDstGap->epochNanoseconds(), Int128(1583652600000000000LL), "IANA: DST gap = 03:30 EDT"); } +// --------------------------------------------------------------------------- +// date_with_empty_error — mirrors temporal_rs plain_date.rs test date_with_empty_error +// Empty CalendarFieldsIn (all nullopt) must return a TypeError from dateFromFields. +// --------------------------------------------------------------------------- + +static void testDateWithEmptyError() +{ + // temporal_rs: let err = base.with(CalendarFields::default(), None); assert!(err.is_err()) + // CalendarFieldsIn{} has all fields nullopt -> missing year, month, day -> TypeError + CalendarFieldsIn emptyFields; + auto r = dateFromFields(calendarIDFromString("iso8601"_s), emptyFields, TemporalOverflow::Constrain); + TCHECK_TRUE(!r.has_value(), "dateWithEmpty: empty fields -> error"); +} + +// --------------------------------------------------------------------------- +// basic_date_with — mirrors temporal_rs plain_date.rs test basic_date_with +// Base: 1976-11-18. Override each field independently via dateFromFields, +// providing the non-overridden fields from the base date. +// --------------------------------------------------------------------------- + +static void testBasicDateWith() +{ + // temporal_rs: plain_date.rs test basic_date_with + // Base: 1976-11-18 + const int32_t baseYear = 1976; + const uint32_t baseMonth = 11; + const uint8_t baseDay = 18; + + // Override year -> 2019-11-18 + { + CalendarFieldsIn fields; + fields.year = 2019; + fields.month = baseMonth; + fields.day = baseDay; + auto r = dateFromFields(calendarIDFromString("iso8601"_s), fields, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value(), "basicWith: override year ok"); + TCHECK_EQ(r->isoDate.year(), 2019, "basicWith: year=2019"); + TCHECK_EQ(r->isoDate.month(), 11u, "basicWith: month=11"); + TCHECK_EQ(r->isoDate.day(), 18u, "basicWith: day=18"); + } + + // Override month -> 1976-05-18 + { + CalendarFieldsIn fields; + fields.year = baseYear; + fields.month = 5; + fields.day = baseDay; + auto r = dateFromFields(calendarIDFromString("iso8601"_s), fields, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value(), "basicWith: override month ok"); + TCHECK_EQ(r->isoDate.year(), 1976, "basicWith: month override year=1976"); + TCHECK_EQ(r->isoDate.month(), 5u, "basicWith: month=5"); + TCHECK_EQ(r->isoDate.day(), 18u, "basicWith: day=18"); + } + + // Override month via monthCode M05 -> 1976-05-18 + { + CalendarFieldsIn fields; + fields.year = baseYear; + fields.monthCode = ParsedMonthCode { 5, false }; + fields.day = baseDay; + auto r = dateFromFields(calendarIDFromString("iso8601"_s), fields, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value(), "basicWith: override monthCode ok"); + TCHECK_EQ(r->isoDate.year(), 1976, "basicWith: monthCode year=1976"); + TCHECK_EQ(r->isoDate.month(), 5u, "basicWith: monthCode month=5"); + TCHECK_EQ(r->isoDate.day(), 18u, "basicWith: monthCode day=18"); + } + + // Override day -> 1976-11-17 + { + CalendarFieldsIn fields; + fields.year = baseYear; + fields.month = baseMonth; + fields.day = 17; + auto r = dateFromFields(calendarIDFromString("iso8601"_s), fields, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value(), "basicWith: override day ok"); + TCHECK_EQ(r->isoDate.year(), 1976, "basicWith: day override year=1976"); + TCHECK_EQ(r->isoDate.month(), 11u, "basicWith: day override month=11"); + TCHECK_EQ(r->isoDate.day(), 17u, "basicWith: day=17"); + } +} + +// --------------------------------------------------------------------------- +// datetime_with_empty_partial — mirrors temporal_rs plain_date_time.rs +// test datetime_with_empty_partial +// Empty DateTimeFields (all nullopt) must return an error from dateFromFields. +// --------------------------------------------------------------------------- + +static void testDateTimeWithEmptyPartial() +{ + // temporal_rs: let err = pdt.with(DateTimeFields::default(), None); assert!(err.is_err()) + // In C++: dateFromFields with all-nullopt fields -> TypeError (no year/month/day) + CalendarFieldsIn emptyFields; + auto r = dateFromFields(calendarIDFromString("iso8601"_s), emptyFields, TemporalOverflow::Constrain); + TCHECK_TRUE(!r.has_value(), "dtWithEmpty: empty partial fields -> error"); + // Confirm it's a TypeError (missing required fields) + TCHECK_TRUE(!r.has_value() && r.error().kind == TemporalErrorKind::TypeError, + "dtWithEmpty: error kind is TypeError"); +} + +// --------------------------------------------------------------------------- +// datetime_round_options — mirrors temporal_rs plain_date_time.rs +// test datetime_round_options +// RoundingOptions without smallest_unit must fail. We test the pure C++ +// equivalent: validateTemporalRoundingIncrement rejects increment=0 +// (which is the increment produced by an unset/zero increment), and also +// that a non-divisor increment rejects with a valid dividend. +// --------------------------------------------------------------------------- + +static void testDateTimeRoundOptions() +{ + // temporal_rs: dt.round(bad_options) — increment=0 is always invalid. + auto rZero = validateTemporalRoundingIncrement(0.0, std::nullopt, Inclusivity::Exclusive); + TCHECK_TRUE(!rZero.has_value(), "dtRoundOpts: increment=0 rejects"); + + // Negative increment also invalid. + auto rNeg = validateTemporalRoundingIncrement(-1.0, std::nullopt, Inclusivity::Exclusive); + TCHECK_TRUE(!rNeg.has_value(), "dtRoundOpts: increment=-1 rejects"); + + // increment=1, no dividend -> ok (valid smallest-unit-like state) + auto rOne = validateTemporalRoundingIncrement(1.0, std::nullopt, Inclusivity::Exclusive); + TCHECK_TRUE(rOne.has_value(), "dtRoundOpts: increment=1 no dividend ok"); + + // temporal_rs: RoundingOptions::default() -> also error (no unit, no increment set). + // Equivalent: increment=0 with a dividend also invalid. + auto rZeroDiv = validateTemporalRoundingIncrement(0.0, 60.0, Inclusivity::Exclusive); + TCHECK_TRUE(!rZeroDiv.has_value(), "dtRoundOpts: increment=0 with dividend=60 rejects"); + + // Valid round-like options: increment=1 with dividend=60 exclusive -> ok. + auto rValid = validateTemporalRoundingIncrement(1.0, 60.0, Inclusivity::Exclusive); + TCHECK_TRUE(rValid.has_value(), "dtRoundOpts: increment=1 dividend=60 ok"); + + // Non-divisor with dividend: increment=7, dividend=60 -> rejects. + auto rNonDiv = validateTemporalRoundingIncrement(7.0, 60.0, Inclusivity::Exclusive); + TCHECK_TRUE(!rNonDiv.has_value(), "dtRoundOpts: increment=7 not divisor of 60 rejects"); +} + +// --------------------------------------------------------------------------- +// to_string_precision_digits — mirrors temporal_rs plain_date_time.rs +// test to_string_precision_digits (fractionaldigits-number.js) +// Tests temporalDateTimeToString with Precision::Fixed for various digit counts. +// --------------------------------------------------------------------------- + +static void testToStringPrecisionDigits() +{ + // temporal_rs: plain_date_time.rs test to_string_precision_digits + // These mirror fractionaldigits-number.js test cases. + + // few_seconds: 1976-02-04T05:03:01 (all zeros for subseconds) + ISO8601::PlainDate fewSecondsDate(1976, 2, 4); + ISO8601::PlainTime fewSecondsTime(5, 3, 1, 0, 0, 0); + + // zero_seconds: 1976-11-18T15:23:00 + ISO8601::PlainDate zeroSecondsDate(1976, 11, 18); + ISO8601::PlainTime zeroSecondsTime(15, 23, 0, 0, 0, 0); + + // whole_seconds: 1976-11-18T15:23:30 + ISO8601::PlainDate wholeSecondsDate(1976, 11, 18); + ISO8601::PlainTime wholeSecondsTime(15, 23, 30, 0, 0, 0); + + // subseconds: 1976-11-18T15:23:30.123400 (ms=123, µs=400, ns=0) + ISO8601::PlainDate subsecondsDate(1976, 11, 18); + ISO8601::PlainTime subsecondsTime(15, 23, 30, 123, 400, 0); + + // Precision::Fixed(0): pads parts, no fractional seconds + { + auto s = ISO8601::temporalDateTimeToString(fewSecondsDate, fewSecondsTime, std::make_tuple(Precision::Fixed, 0u)); + TCHECK_EQ(s, "1976-02-04T05:03:01"_s, "toStrPrec: few_seconds digit=0 pads 0s"); + } + + // Precision::Fixed(0) on subseconds: truncates to 0 decimal places + { + auto s = ISO8601::temporalDateTimeToString(subsecondsDate, subsecondsTime, std::make_tuple(Precision::Fixed, 0u)); + TCHECK_EQ(s, "1976-11-18T15:23:30"_s, "toStrPrec: subseconds digit=0 truncates"); + } + + // Precision::Fixed(2) on zero_seconds: pads to 2 decimal places + { + auto s = ISO8601::temporalDateTimeToString(zeroSecondsDate, zeroSecondsTime, std::make_tuple(Precision::Fixed, 2u)); + TCHECK_EQ(s, "1976-11-18T15:23:00.00"_s, "toStrPrec: zero_seconds digit=2 pads"); + } + + // Precision::Fixed(2) on whole_seconds: pads to 2 decimal places + { + auto s = ISO8601::temporalDateTimeToString(wholeSecondsDate, wholeSecondsTime, std::make_tuple(Precision::Fixed, 2u)); + TCHECK_EQ(s, "1976-11-18T15:23:30.00"_s, "toStrPrec: whole_seconds digit=2 pads"); + } + + // Precision::Fixed(2) on subseconds: truncates 4 places to 2 + { + auto s = ISO8601::temporalDateTimeToString(subsecondsDate, subsecondsTime, std::make_tuple(Precision::Fixed, 2u)); + TCHECK_EQ(s, "1976-11-18T15:23:30.12"_s, "toStrPrec: subseconds digit=2 truncates to 2"); + } + + // Precision::Fixed(3) on subseconds: truncates 4 places to 3 + { + auto s = ISO8601::temporalDateTimeToString(subsecondsDate, subsecondsTime, std::make_tuple(Precision::Fixed, 3u)); + TCHECK_EQ(s, "1976-11-18T15:23:30.123"_s, "toStrPrec: subseconds digit=3 truncates to 3"); + } + + // Precision::Fixed(6) on subseconds: pads 4 places to 6 + { + auto s = ISO8601::temporalDateTimeToString(subsecondsDate, subsecondsTime, std::make_tuple(Precision::Fixed, 6u)); + TCHECK_EQ(s, "1976-11-18T15:23:30.123400"_s, "toStrPrec: subseconds digit=6 pads to 6"); + } + + // Precision::Auto: omits trailing zeros + { + auto s = ISO8601::temporalDateTimeToString(wholeSecondsDate, wholeSecondsTime, std::make_tuple(Precision::Auto, 0u)); + TCHECK_EQ(s, "1976-11-18T15:23:30"_s, "toStrPrec: whole_seconds Auto omits zeros"); + } + + // Precision::Auto on subseconds: shows minimal significant digits + { + auto s = ISO8601::temporalDateTimeToString(subsecondsDate, subsecondsTime, std::make_tuple(Precision::Auto, 0u)); + TCHECK_EQ(s, "1976-11-18T15:23:30.1234"_s, "toStrPrec: subseconds Auto shows 4 sig digits"); + } +} + +// --------------------------------------------------------------------------- +// ZDT tests — requires ICU timezone + DST support +// --------------------------------------------------------------------------- + static void testToZonedDateTime() { // temporal_rs: plain_date.rs::to_zoned_date_time @@ -1344,10 +2799,8 @@ static void testToZonedDateTime() static void testToZonedDateTimeError() { - // temporal_rs: plain_date.rs::to_zoned_date_time_error - // Min date -271821-04-19 with UTC+00 -> start of day is at or before min epoch. - // -271821-04-18 is one day before the minimum valid Temporal date; getEpochNanosecondsFor - // must reject it . + // temporal_rs: to_zoned_date_time_error — min date -271821-04-19 start-of-day is at or before min epoch. + auto utcOpt = ISO8601::parseTemporalTimeZoneIdentifier("UTC"_s); if (!utcOpt) { fprintf(stderr, "SKIP [toZDTErr]: UTC not available\n"); @@ -1367,9 +2820,7 @@ static void testAddZonedDateTime() } auto utc = *utcOpt; - // 1. Time-only fast path (no date components): years=months=weeks=days=0 - // temporal_rs basic_zdt_add: start=-560174321098766 ns, duration=P0DT240H+800ns - // result = start + 240h + 800ns = 303825678902034 ns + // 1. Time-only fast path: temporal_rs basic_zdt_add start=-560174321098766 ns, P0DT240H+800ns. ISO8601::ExactTime start1(Int128(-560174321098766LL)); ISO8601::Duration d1(0, 0, 0, 0, 240, 0, 0, 0, 0, 800); // 240h + 800ns auto r1 = addZonedDateTime(start1, utc, d1, TemporalOverflow::Constrain); @@ -1384,10 +2835,7 @@ static void testAddZonedDateTime() if (r2.has_value()) TCHECK_EQ(r2->epochNanoseconds(), start1.epochNanoseconds(), "addZDT: zero = start"); - // 3. Date+time path (P1DT1H with UTC): verifies getEpochNanosecondsFor + calendarDateAdd - // start: 2020-01-15T12:00:00Z = 1579089600000000000 ns - // P1D -> addedDate = 2020-01-16; re-resolve + +1h -> 2020-01-16T13:00:00Z - // expected: 1579089600000000000 + 86400000000000 + 3600000000000 = 1579179600000000000 ns + // 3. Date+time path (P1DT1H with UTC): calendarDateAdd then re-resolve → 2020-01-16T13:00:00Z. ISO8601::ExactTime start3(Int128(1579089600000000000LL)); ISO8601::Duration d3(0, 0, 0, 1, 1, 0, 0, 0, 0, 0); // P1DT1H auto r3 = addZonedDateTime(start3, utc, d3, TemporalOverflow::Constrain); @@ -1516,11 +2964,11 @@ static void testPossibleEpochNsAtLimits() if (rMaxValid.has_value() && std::holds_alternative(*rMaxValid)) TCHECK_TRUE(std::get(*rMaxValid).isValid(), "epochNsLimits: max candidate isValid"); - // Just before min: -271821-04-19T23:59:59.999999999Z = NS_MIN_INSTANT - 1ns → out of range → error or GapOffsets + // Just before min: -271821-04-19T23:59:59.999999999Z = NS_MIN_INSTANT - 1ns -> out of range -> error or GapOffsets auto rTooEarly = getPossibleEpochNanosecondsFor(utc, { -271821, 4, 19 }, { 23, 59, 59, 999, 999, 999 }); TCHECK_TRUE(!rTooEarly.has_value() || isGap(*rTooEarly), "epochNsLimits: too-early = error/gap"); - // Just after max: +275760-09-13T00:00:00.000000001Z = NS_MAX_INSTANT + 1ns → out of range → error or GapOffsets + // Just after max: +275760-09-13T00:00:00.000000001Z = NS_MAX_INSTANT + 1ns -> out of range -> error or GapOffsets auto rTooLate = getPossibleEpochNanosecondsFor(utc, { 275760, 9, 13 }, { 0, 0, 0, 0, 0, 1 }); TCHECK_TRUE(!rTooLate.has_value() || isGap(*rTooLate), "epochNsLimits: too-late = error/gap"); @@ -1532,6 +2980,221 @@ static void testPossibleEpochNsAtLimits() } } +static void testNudgeFunctions() +{ + // temporal_rs: nudge functions tested indirectly via round_relative_to_zoned_datetime + // and test_nudge_relative_date_total. These tests cover each nudge path directly. + + auto id = calendarIDFromString("iso8601"_s); + + // --- nudgeToDayOrTime: no timezone, pure time rounding --- + // P0DT25H rounded to Day with increment=1, HalfExpand -> whole days=1, remainder=1h + { + ISO8601::Duration datePart; + Int128 time25h = Int128(90000000000000LL); // 25h in ns + auto dur = ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, time25h); + Int128 destEpochNs = time25h; // dest = 25h from epoch + auto r = nudgeToDayOrTime(dur, destEpochNs, TemporalUnit::Day, 1, TemporalUnit::Hour, RoundingMode::HalfExpand); + TCHECK_TRUE(r.has_value(), "nudgeDayOrTime: 25h ok"); + // NudgeResult.duration is InternalDuration; check days via dateDuration() + TCHECK_TRUE(r->duration.dateDuration().days() == 1 || !r->duration.dateDuration().days(), "nudgeDayOrTime: days reasonable"); + } + + // nudgeToDayOrTime: exactly 24h rounds to 1 day (no remainder) + { + ISO8601::Duration datePart; + Int128 time24h = Int128(86400000000000LL); + auto dur = ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, time24h); + auto r = nudgeToDayOrTime(dur, time24h, TemporalUnit::Day, 1, TemporalUnit::Day, RoundingMode::HalfExpand); + TCHECK_TRUE(r.has_value(), "nudgeDayOrTime: 24h ok"); + } + + // --- nudgeToCalendarUnit: P0DT1H with unit=Day relative to 2020-01-15 --- + // Using Day (not Year) to keep denominator (1 day = 8.64e13 ns) within fractionToDouble's safe range. + // Year-level denominators (~3e16 ns) exceed safe integer range and require __float128 per spec NOTE. + { + ISO8601::PlainDate date { 2020, 1, 15 }; + ISO8601::PlainTime time; + Int128 originEpochNs = getUTCEpochNanoseconds(date, time); + // P0DT1H: datePart=0 days, time=1h + ISO8601::Duration datePart; + Int128 oneHour = Int128(3600000000000LL); + auto dur = ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, oneHour); + Int128 destEpochNs = originEpochNs + oneHour; + int32_t sign = 1; + auto r = nudgeToCalendarUnit(sign, dur, originEpochNs, destEpochNs, + date, time, 1, TemporalUnit::Day, RoundingMode::HalfExpand, nullptr, id); + TCHECK_TRUE(r.has_value(), "nudgeCalUnit: P0DT1H Day ok"); + // 1h is well within 1 day → didExpandCalendarUnit=false + TCHECK_TRUE(!r->nudgeResult.didExpandCalendarUnit, "nudgeCalUnit: no expand"); + } + + // --- bubbleRelativeDuration: P1M30D relative 2020-01-01 -> bubble from Day up --- + // 2020-01-01 + P1M30D = 2020-03-01; bubbling from Day: is P1M30D >= P2M? No (Feb has 29d in 2020) + // So bubble should NOT collapse to P2M (30 days < 29d of Feb + remainder) — stays P1M30D + { + ISO8601::PlainDate date { 2020, 1, 1 }; + ISO8601::PlainTime time; + ISO8601::Duration datePart(0, 1, 0, 30, 0, 0, 0, 0, Int128(0), Int128(0)); + auto dur = ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, 0); + // nudgedEpochNs = 2020-03-01 UTC + ISO8601::PlainDate nudgedDate { 2020, 3, 1 }; + Int128 nudgedEpochNs = getUTCEpochNanoseconds(nudgedDate, time); + auto r = bubbleRelativeDuration(1, dur, nudgedEpochNs, date, time, + TemporalUnit::Month, TemporalUnit::Day, nullptr, id); + TCHECK_TRUE(r.has_value(), "bubbleRelDur: P1M30D ok"); + } + + // --- nudgeToZonedTime: UTC+0 timezone, P25H rounded to Hour --- + auto utcOpt = ISO8601::parseTemporalTimeZoneIdentifier("UTC"_s); + if (utcOpt) { + ISO8601::PlainDate date { 2020, 1, 1 }; + ISO8601::PlainTime time; + ISO8601::Duration datePart(0, 0, 0, 1, 0, 0, 0, 0, Int128(0), Int128(0)); // 1 day + Int128 timeComp = Int128(3600000000000LL); // 1h + auto dur = ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, timeComp); + auto r = nudgeToZonedTime(1, dur, date, time, *utcOpt, 1, TemporalUnit::Hour, RoundingMode::HalfExpand, id); + TCHECK_TRUE(r.has_value(), "nudgeZonedTime: P1DT1H ok"); + } +} + +static void testRoundRelativeToZonedDateTime() +{ + // temporal_rs: duration/tests.rs::round_relative_to_zoned_datetime + // P25H duration, ZDT at epoch=1_000_000_000_000_000_000, tz=+04:30 + // round to largestUnit=Day -> expected: 1 day 1 hour + + // +04:30 = 4.5h = 16200s = 16200000000000 ns + auto tzOpt = ISO8601::parseTemporalTimeZoneIdentifier("+04:30"_s); + if (!tzOpt) { + fprintf(stderr, "SKIP [roundRelZDT]: +04:30 not available\n"); + return; + } + auto tz = *tzOpt; + + // P25H = 90000000000000 ns + Int128 startNs = Int128(1000000000000000000LL); + Int128 endNs = startNs + Int128(90000000000000LL); // +25h + + // differenceZonedDateTimeForDuration(start, end, tz, Day, iso8601) + auto diff = differenceZonedDateTimeForDuration( + ISO8601::ExactTime(startNs), ISO8601::ExactTime(endNs), + tz, TemporalUnit::Day, calendarIDFromString("iso8601"_s)); + TCHECK_TRUE(diff.has_value(), "roundRelZDT: diff ok"); + // 25h with +04:30 (no DST) = 1 day + 1 hour + TCHECK_EQ(static_cast(diff->dateDuration().days()), 1LL, "roundRelZDT: days=1"); + TCHECK_EQ(diff->time(), Int128(3600000000000LL), "roundRelZDT: time=1h"); +} + +static void testDurationTotalZDT() +{ + // temporal_rs: test_duration_total (ZDT path) — P2756H in months with DST differs from PlainDate path. + + auto romeOpt = ISO8601::parseTemporalTimeZoneIdentifier("Europe/Rome"_s); + if (!romeOpt) { + fprintf(stderr, "SKIP [totalZDT]: Europe/Rome not available\n"); + return; + } + auto romeTz = *romeOpt; + + // 2020-01-01T00:00 in Rome (winter = UTC+1) → UTC 2019-12-31T23:00; use getEpochNanosecondsFor. + auto startR = getEpochNanosecondsFor(romeTz, { 2020, 1, 1 }, { 0, 0, 0, 0, 0, 0 }, TemporalDisambiguation::Compatible); + TCHECK_TRUE(startR.has_value(), "totalZDT: start epoch ok"); + + // end = start + 2756h in ns + Int128 p2756h = Int128(2756LL) * Int128(3600000000000LL); + Int128 endNs = startR->epochNanoseconds() + p2756h; + + // differenceZonedDateTimeForDuration(start, end, Rome, Month) + auto diff = differenceZonedDateTimeForDuration( + *startR, ISO8601::ExactTime(endNs), + romeTz, TemporalUnit::Month, calendarIDFromString("iso8601"_s)); + TCHECK_TRUE(diff.has_value(), "totalZDT: diff ok"); + + // Convert to total months: months + remaining time as fraction + // months * totalDaysInMonths + days portion -> complex; just verify months >= 3 + TCHECK_TRUE(static_cast(diff->dateDuration().months()) >= 3LL, "totalZDT: months>=3"); + + // PlainDate path (no DST): verify P2756H ≈ 3 months from 2020-01-01 using pure day arithmetic. + auto endDate = isoDateAdd({ 2020, 1, 1 }, ISO8601::Duration(0, 0, 0, 115, 0, 0, 0, 0, 0, 0), TemporalOverflow::Constrain); + TCHECK_TRUE(endDate.has_value(), "totalZDT: plain endDate ok"); + auto plainDiff = diffISODate({ 2020, 1, 1 }, *endDate, TemporalUnit::Month); + TCHECK_EQ(static_cast(plainDiff.months()), 3LL, "totalZDT: plain months=3"); +} + +static void testNudgePastEnd() +{ + // temporal_rs: duration/tests.rs::nudge_past_end + // Zero duration, ZDT at max epoch (8.64e21 ns = 1e8 days * nsPerDay), round to Day/Minute + // Rounding constructs end date = max + 1 day -> error + + auto utcOpt = ISO8601::parseTemporalTimeZoneIdentifier("UTC"_s); + if (!utcOpt) { + fprintf(stderr, "SKIP [nudgePast]: UTC not available\n"); + return; + } + auto utc = *utcOpt; + + // Max Temporal epoch = 1e8 days × nsPerDay; Int128 required (exceeds int64 range). + Int128 maxEpoch = Int128(86400000000000LL) * Int128(100000000LL); // 1e8 days * nsPerDay + + // Test via getStartOfDay: one day past max rejects, max date itself succeeds. + auto r = getStartOfDay(utc, { 275760, 9, 14 }); // one day past max + TCHECK_TRUE(!r.has_value(), "nudgePast: start-of-day past max rejects"); + + // Verify max date itself works + auto r2 = getStartOfDay(utc, { 275760, 9, 13 }); // max date + TCHECK_TRUE(r2.has_value(), "nudgePast: start-of-day at max ok"); + + // Verify a normal diff near max succeeds (the out-of-range error is in getStartOfDay above). + Int128 nearMax = maxEpoch - Int128(3600000000000LL); // 1h before max + Int128 nearMaxEnd = nearMax + Int128(60000000000LL); // +1min + auto diffNear = differenceZonedDateTimeForDuration( + ISO8601::ExactTime(nearMax), ISO8601::ExactTime(nearMaxEnd), + utc, TemporalUnit::Hour, calendarIDFromString("iso8601"_s)); + TCHECK_TRUE(diffNear.has_value(), "nudgePast: normal diff near max ok"); +} + +static void testRoundZeroDurationZDT() +{ + // temporal_rs: round_zero_duration with ZDT relativeTo + // P0 duration, ZDT at UTC epoch=0, round to Day/Hour -> result is still zero + auto utcOpt = ISO8601::parseTemporalTimeZoneIdentifier("UTC"_s); + if (!utcOpt) { + fprintf(stderr, "SKIP [roundZeroZDT]: UTC not available\n"); + return; + } + auto utc = *utcOpt; + + // Start and end are the same (zero duration) -> diff = zero + ISO8601::ExactTime epoch(Int128(0LL)); + auto diff = differenceZonedDateTimeForDuration(epoch, epoch, utc, TemporalUnit::Day, calendarIDFromString("iso8601"_s)); + TCHECK_TRUE(diff.has_value(), "roundZeroZDT: zero diff ok"); + TCHECK_EQ(static_cast(diff->dateDuration().days()), 0LL, "roundZeroZDT: days=0"); + TCHECK_EQ(diff->time(), Int128(0LL), "roundZeroZDT: time=0"); +} + +static void testRoundIncrementRegressionZDT() +{ + // temporal_rs: round_increment_regression_test ZDT path + // P48H, round to Day, increment=2, with ZDT UTC at epoch=0 + // Expected: 2 days (same result as without relativeTo) + auto utcOpt = ISO8601::parseTemporalTimeZoneIdentifier("UTC"_s); + if (!utcOpt) { + fprintf(stderr, "SKIP [roundIncZDT]: UTC not available\n"); + return; + } + auto utc = *utcOpt; + + // P48H = 2 * 86400000000000 ns from epoch 0 + ISO8601::ExactTime start(Int128(0LL)); + ISO8601::ExactTime end(Int128(172800000000000LL)); // 48h + auto diff = differenceZonedDateTimeForDuration(start, end, utc, TemporalUnit::Day, calendarIDFromString("iso8601"_s)); + TCHECK_TRUE(diff.has_value(), "roundIncZDT: 48h diff ok"); + TCHECK_EQ(static_cast(diff->dateDuration().days()), 2LL, "roundIncZDT: days=2"); + TCHECK_EQ(diff->time(), Int128(0LL), "roundIncZDT: time=0"); +} + // --------------------------------------------------------------------------- // Section 1: direct temporal_rs ports // --------------------------------------------------------------------------- @@ -1570,6 +3233,49 @@ static void runTemporalRSTests() testToZonedDateTimeError(); // temporal_rs: to_zoned_date_time_error testTimeZoneEquals(); // temporal_rs: canonicalize_equals testAddZonedDateTime(); // temporal_rs: basic_zdt_add + testDateWithEmptyError(); // temporal_rs: date_with_empty_error + testBasicDateWith(); // temporal_rs: basic_date_with + + // --- plain_date_time.rs --- + testPlainDateTimeLimits(); // temporal_rs: plain_date_time_limits + testDateTimeAddSubtract(); // temporal_rs: datetime_add_test, datetime_subtract_test, datetime_subtract_hour_overflows, datetime_add + testDiffISODateTime(); // temporal_rs: (diffISODateTime) + testDtSinceConflictingSigns(); // temporal_rs: dt_since_conflicting_signs + testDifferenceTemporalPlainDateTime(); // temporal_rs: dt_until_basic, dt_since_basic, dt_since_conflicting_signs + testDtRoundBasic(); // temporal_rs: dt_round_basic + testRoundISODateTime(); // roundISODateTime: sub-unit base, halfEven, day overflow + testDtUntilBasic(); // temporal_rs: dt_until_basic + testCompareISODateTime(); // temporal_rs: (compareISODateTime) + testDateTimeWithEmptyPartial(); // temporal_rs: datetime_with_empty_partial + testDateTimeRoundOptions(); // temporal_rs: datetime_round_options + testToStringPrecisionDigits(); // temporal_rs: to_string_precision_digits + + // --- duration/tests.rs --- + testDurationSign(); // temporal_rs: (durationSign) + testLargestSubduration(); // temporal_rs: (largestSubduration / default_largest_unit) + testNegateDuration(); // temporal_rs: (negateDuration) + testAbsDuration(); // temporal_rs: (absDuration) + testAdjustDateDurationRecord(); // temporal_rs: (adjustDateDurationRecord) + testTimeDurationFromComponents(); // temporal_rs: (timeDurationFromComponents) + testDurationAddSubtract(); // temporal_rs: basic_add_duration, basic_subtract_duration + testDurationBalancing(); // temporal_rs: balance_subseconds (partial) + testDurationTotalBasic(); // temporal_rs: test_duration_total (pure path) + testDurationTotalWithRelativeTo(); // temporal_rs: balance_days_up_to_both_years_and_months + testRoundingCrossBoundary(); // temporal_rs: rounding_cross_boundary, rounding_cross_boundary_negative, rounding_cross_boundary_time_units + testRoundingToDayOnly(); // temporal_rs: rounding_to_fractional_day_tests + testBubbleSmallestBecomesDay(); // temporal_rs: bubble_smallest_becomes_day + testRoundZeroDuration(); // temporal_rs: round_zero_duration (PlainDate path) + testRoundIncrementRegression(); // temporal_rs: round_increment_regression_test (no relativeTo) + testAddNormTimeDurationOutOfRange(); // temporal_rs: add_normalized_time_duration_out_of_range + testAddLargeDurations(); // temporal_rs: add_large_durations + testRoundingBoundaries(); // temporal_rs: test_rounding_boundaries + testDurationCompareBoundary(); // temporal_rs: test_duration_compare_boundary + testRoundRelativeToZonedDateTime(); // temporal_rs: round_relative_to_zoned_datetime + testNudgeFunctions(); // direct tests for nudgeToCalendarUnit, nudgeToDayOrTime, nudgeToZonedTime, bubbleRelativeDuration + testDurationTotalZDT(); // temporal_rs: test_duration_total (ZDT path) + testNudgePastEnd(); // temporal_rs: nudge_past_end + testRoundZeroDurationZDT(); // temporal_rs: round_zero_duration (ZDT path) + testRoundIncrementRegressionZDT(); // temporal_rs: round_increment_regression_test (ZDT path) } // --------------------------------------------------------------------------- @@ -1582,25 +3288,41 @@ static void runStressTests() testISODateLimits(); // Temporal epoch limit boundary checks testNegativeRounding(); // Negative number rounding across all modes - // Calendar helpers + // Duration helpers + testSplitTimeDuration(); // splitTimeDuration edge cases + testPlainTimeFromSubdayNs(); // sub-day time decomposition + testAdd24HourDaysToTimeDuration(); // 24h day folding + testTemporalDurationFromInternal(); // InternalDuration -> Duration + testToInternalDuration(); // Duration -> InternalDuration + testToDateDurationRecordWithoutTime(); // time field stripping + testTotalSecondsAndSubseconds(); // totalSeconds/totalSubseconds helpers + testTotalTimeDuration(); // fractional unit conversion + testBalanceDuration(); // duration field redistribution + + // Rounding helpers testCalendarDateAdd(); // ISO calendarDateAdd testCalendarDateUntil(); // ISO calendarDateUntil - // Instant + // Instant/TimeZone testMaximumInstantIncrement(); // maximumInstantIncrement per unit // ICU bridges testExactTimeToLocalDateAndTime(); // epoch -> local date+time testGetOffsetNanosecondsForUTC(); // UTC offset = 0 + testInterpretISODateTimeOffset(); // all branches: wall, use, prefer, reject, match-minutes, start-of-day testPossibleEpochNsAtLimits(); // temporal_rs: test_possible_epoch_ns_at_limits testGetTimeZoneTransition(); // temporal_rs: get_time_zone_transition testTimeZoneICUWithIANA(); // IANA timezone with DST (America/New_York) + testGetUTCEpochNanoseconds(); // UTC epoch nanosecond computation + + // Calendar ICU testCalendarIsLunisolar(); // lunisolar calendar detection testCalendarDaysInMonthISO(); // ISO days-in-month testCalendarInLeapYearISO(); // ISO leap year testCalendarISO8601Fields(); // ISO field accessors testCalendarICUNonISO(); // Non-ISO calendars (hebrew, chinese, japanese, persian) testCalendarDateFromFields(); // calendarDateFromFields: era, monthCode, overflow, ROC, Japanese + testCalendarFieldsFunctions(); // yearMonthFromFields, monthDayFromFields, differenceYearMonth, plainYearMonthAdd, etc. } } // namespace TemporalCore diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt index 9748b46b9e5e..744dca0eb4cf 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt @@ -1485,13 +1485,17 @@ set(JavaScriptCore_PRIVATE_FRAMEWORK_HEADERS runtime/WriteBarrierInlines.h runtime/temporal/core/CalendarArithmetic.h + runtime/temporal/core/CalendarFields.h runtime/temporal/core/CalendarICUBridge.h + runtime/temporal/core/DurationArithmetic.h runtime/temporal/core/ISOArithmetic.h runtime/temporal/core/InstantCore.h + runtime/temporal/core/PlainDateTimeCore.h runtime/temporal/core/Rounding.h runtime/temporal/core/TemporalCoreTypes.h runtime/temporal/core/TemporalEnums.h runtime/temporal/core/TimeZoneICUBridge.h + runtime/temporal/core/ZonedDateTimeCore.h tools/Integrity.h tools/IntegrityInlines.h diff --git a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj index f637802fc41c..01ac908849ed 100644 --- a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj +++ b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj @@ -946,6 +946,7 @@ 2A83638618D7D0EE0000EBCC /* EdenGCActivityCallback.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A83638418D7D0EE0000EBCC /* EdenGCActivityCallback.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2A83638A18D7D0FE0000EBCC /* FullGCActivityCallback.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A83638818D7D0FE0000EBCC /* FullGCActivityCallback.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2AA109412F8D5DE600347316 /* UnifiedSource166.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2AA109402F8D5DE600347316 /* UnifiedSource166.cpp */; }; + FF41D81E2FA317C2001D30AA /* UnifiedSource167.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FF41D81F2FA317C2001D30AA /* UnifiedSource167.cpp */; }; 2AAAA31218BD49D100394CC8 /* TypeInfoBlob.h in Headers */ = {isa = PBXBuildFile; fileRef = 2AAAA31018BD49D100394CC8 /* TypeInfoBlob.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2AABCDE718EF294200002096 /* GCLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 2AABCDE618EF294200002096 /* GCLogging.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2AACE63D18CA5A0300ED0191 /* GCActivityCallback.h in Headers */ = {isa = PBXBuildFile; fileRef = 2AACE63B18CA5A0300ED0191 /* GCActivityCallback.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -2370,6 +2371,10 @@ FFE88B9E2EC96CA700C9F5E2 /* ExecutionHandlerTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FFE88B9D2EC96CA700C9F5E2 /* ExecutionHandlerTest.cpp */; }; FFF3373F2BCDF1240070D04B /* JSCBytecodeCacheVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = FFF3373E2BCDF1240070D04B /* JSCBytecodeCacheVersion.h */; settings = {ATTRIBUTES = (Private, ); }; }; FFF4D5A32CE304CD006EA634 /* DeferredWorkTimerInlines.h in Headers */ = {isa = PBXBuildFile; fileRef = FFF4D5A22CE304C2006EA634 /* DeferredWorkTimerInlines.h */; settings = {ATTRIBUTES = (Private, ); }; }; + FFF79F9A2FB2F1B2003FF02A /* PlainDateTimeCore.h in Headers */ = {isa = PBXBuildFile; fileRef = FFF79F962FB2F1B2003FF02A /* PlainDateTimeCore.h */; settings = {ATTRIBUTES = (Private, ); }; }; + FFF79F9B2FB2F1B2003FF02A /* CalendarFields.h in Headers */ = {isa = PBXBuildFile; fileRef = FFF79F922FB2F1B2003FF02A /* CalendarFields.h */; settings = {ATTRIBUTES = (Private, ); }; }; + FFF79F9C2FB2F1B2003FF02A /* ZonedDateTimeCore.h in Headers */ = {isa = PBXBuildFile; fileRef = FFF79F982FB2F1B2003FF02A /* ZonedDateTimeCore.h */; settings = {ATTRIBUTES = (Private, ); }; }; + FFF79F9D2FB2F1B2003FF02A /* DurationArithmetic.h in Headers */ = {isa = PBXBuildFile; fileRef = FFF79F942FB2F1B2003FF02A /* DurationArithmetic.h */; settings = {ATTRIBUTES = (Private, ); }; }; FFFAF8CB2EC97B2B00042238 /* TestScripts.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FFFAF8CA2EC97B2B00042238 /* TestScripts.cpp */; }; FFFAF8CE2EC9A7C000042238 /* ExecutionHandlerTestSupport.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FFFAF8CD2EC9A7C000042238 /* ExecutionHandlerTestSupport.cpp */; }; /* End PBXBuildFile section */ @@ -4231,6 +4236,7 @@ 2A83638718D7D0FE0000EBCC /* FullGCActivityCallback.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FullGCActivityCallback.cpp; sourceTree = ""; }; 2A83638818D7D0FE0000EBCC /* FullGCActivityCallback.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FullGCActivityCallback.h; sourceTree = ""; }; 2AA109402F8D5DE600347316 /* UnifiedSource166.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = UnifiedSource166.cpp; path = "DerivedSources/JavaScriptCore/unified-sources/UnifiedSource166.cpp"; sourceTree = BUILT_PRODUCTS_DIR; }; + FF41D81F2FA317C2001D30AA /* UnifiedSource167.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = UnifiedSource167.cpp; path = "DerivedSources/JavaScriptCore/unified-sources/UnifiedSource167.cpp"; sourceTree = BUILT_PRODUCTS_DIR; }; 2AAAA31018BD49D100394CC8 /* TypeInfoBlob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TypeInfoBlob.h; sourceTree = ""; }; 2AABCDE618EF294200002096 /* GCLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCLogging.h; sourceTree = ""; }; 2AACE63A18CA5A0300ED0191 /* GCActivityCallback.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GCActivityCallback.cpp; sourceTree = ""; }; @@ -6583,6 +6589,14 @@ FFF3373D2BCDF1240070D04B /* JSCBytecodeCacheVersion.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = JSCBytecodeCacheVersion.cpp; sourceTree = ""; }; FFF3373E2BCDF1240070D04B /* JSCBytecodeCacheVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSCBytecodeCacheVersion.h; sourceTree = ""; }; FFF4D5A22CE304C2006EA634 /* DeferredWorkTimerInlines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeferredWorkTimerInlines.h; sourceTree = ""; }; + FFF79F922FB2F1B2003FF02A /* CalendarFields.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CalendarFields.h; sourceTree = ""; }; + FFF79F932FB2F1B2003FF02A /* CalendarFields.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CalendarFields.cpp; sourceTree = ""; }; + FFF79F942FB2F1B2003FF02A /* DurationArithmetic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DurationArithmetic.h; sourceTree = ""; }; + FFF79F952FB2F1B2003FF02A /* DurationArithmetic.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DurationArithmetic.cpp; sourceTree = ""; }; + FFF79F962FB2F1B2003FF02A /* PlainDateTimeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlainDateTimeCore.h; sourceTree = ""; }; + FFF79F972FB2F1B2003FF02A /* PlainDateTimeCore.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PlainDateTimeCore.cpp; sourceTree = ""; }; + FFF79F982FB2F1B2003FF02A /* ZonedDateTimeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ZonedDateTimeCore.h; sourceTree = ""; }; + FFF79F992FB2F1B2003FF02A /* ZonedDateTimeCore.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ZonedDateTimeCore.cpp; sourceTree = ""; }; FFFAF8C92EC97B2B00042238 /* TestScripts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestScripts.h; sourceTree = ""; }; FFFAF8CA2EC97B2B00042238 /* TestScripts.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TestScripts.cpp; sourceTree = ""; }; FFFAF8CC2EC9A7C000042238 /* ExecutionHandlerTestSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExecutionHandlerTestSupport.h; sourceTree = ""; }; @@ -8187,6 +8201,7 @@ 538F15DA368FBBB600D601C4 /* UnifiedSource164.cpp */, 538F15DF368FBBB600D601C4 /* UnifiedSource165.cpp */, 2AA109402F8D5DE600347316 /* UnifiedSource166.cpp */, + FF41D81F2FA317C2001D30AA /* UnifiedSource167.cpp */, ); path = "unified-sources"; sourceTree = ""; @@ -10886,18 +10901,26 @@ children = ( FF62739E2FAD56220098B5A3 /* CalendarArithmetic.cpp */, FF62739D2FAD56220098B5A3 /* CalendarArithmetic.h */, + FFF79F932FB2F1B2003FF02A /* CalendarFields.cpp */, + FFF79F922FB2F1B2003FF02A /* CalendarFields.h */, FF629D9C2FAE9B500098B5A3 /* CalendarICUBridge.cpp */, FF629D9B2FAE9B500098B5A3 /* CalendarICUBridge.h */, + FFF79F952FB2F1B2003FF02A /* DurationArithmetic.cpp */, + FFF79F942FB2F1B2003FF02A /* DurationArithmetic.h */, FF6273A02FAD56220098B5A3 /* InstantCore.cpp */, FF62739F2FAD56220098B5A3 /* InstantCore.h */, FF6273A22FAD56220098B5A3 /* ISOArithmetic.cpp */, FF6273A12FAD56220098B5A3 /* ISOArithmetic.h */, + FFF79F972FB2F1B2003FF02A /* PlainDateTimeCore.cpp */, + FFF79F962FB2F1B2003FF02A /* PlainDateTimeCore.h */, FF6273A42FAD56220098B5A3 /* Rounding.cpp */, FF6273A32FAD56220098B5A3 /* Rounding.h */, FF356CF32FA9832600213929 /* TemporalCoreTypes.h */, FF356CF42FA9832600213929 /* TemporalEnums.h */, FF629D9F2FAE9B500098B5A3 /* TimeZoneICUBridge.cpp */, FF629D9E2FAE9B500098B5A3 /* TimeZoneICUBridge.h */, + FFF79F992FB2F1B2003FF02A /* ZonedDateTimeCore.cpp */, + FFF79F982FB2F1B2003FF02A /* ZonedDateTimeCore.h */, ); path = core; sourceTree = ""; @@ -11326,6 +11349,7 @@ 1409ECC0225E178100BEDD54 /* CacheUpdate.h in Headers */, 0FEC3C601F379F5300F59B6C /* CagedBarrierPtr.h in Headers */, FF6273A82FAD56220098B5A3 /* CalendarArithmetic.h in Headers */, + FFF79F9B2FB2F1B2003FF02A /* CalendarFields.h in Headers */, FF629DA32FAE9B500098B5A3 /* CalendarICUBridge.h in Headers */, BC18C3ED0E16F5CD00B34460 /* CallData.h in Headers */, 0F64B27A1A7957B2006E4E66 /* CallEdge.h in Headers */, @@ -11643,6 +11667,7 @@ E35CA1541DBC3A5C00F83516 /* DOMJITHeapRange.h in Headers */, E350708A1DC49BBF0089BCD6 /* DOMJITSignature.h in Headers */, A70447EE17A0BD7000F5898E /* DumpContext.h in Headers */, + FFF79F9D2FB2F1B2003FF02A /* DurationArithmetic.h in Headers */, 145FF2C8243BB9D600569E71 /* ECMAMode.h in Headers */, 2A83638618D7D0EE0000EBCC /* EdenGCActivityCallback.h in Headers */, 1AB2A6FB9608AD5DF73332D4 /* EmbedderArrayLike.h in Headers */, @@ -12437,6 +12462,7 @@ A5AB49DD1BEC8086007020FB /* PerGlobalObjectWrapperWorld.h in Headers */, 23A356912E9790F40039C82A /* PinballCompletion.h in Headers */, F04DFCBEDED74A4E92B8A9DA /* PinballHandlerContext.h in Headers */, + FFF79F9A2FB2F1B2003FF02A /* PlainDateTimeCore.h in Headers */, 0FE834181A6EF97B00D04847 /* PolymorphicCallStubRoutine.h in Headers */, 521131F71F82BF14007CCEEE /* PolyProtoAccessChain.h in Headers */, 0F070A4B1D543A98006E7232 /* PreciseAllocation.h in Headers */, @@ -12955,6 +12981,7 @@ 86704B8A12DBA33700A9FE7B /* YarrPattern.h in Headers */, 86704B4312DB8A8100A9FE7B /* YarrSyntaxChecker.h in Headers */, 659CDA5B1F6753F200D3E53F /* YarrUnicodeProperties.h in Headers */, + FFF79F9C2FB2F1B2003FF02A /* ZonedDateTimeCore.h in Headers */, E390287D2DD1E7D90083D675 /* Zydis.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -14203,6 +14230,7 @@ 538F15DA268FBBB600D601C4 /* UnifiedSource164.cpp in Sources */, 538F15DF268FBBB600D601C4 /* UnifiedSource165.cpp in Sources */, 2AA109412F8D5DE600347316 /* UnifiedSource166.cpp in Sources */, + FF41D81E2FA317C2001D30AA /* UnifiedSource167.cpp in Sources */, E3C091E929B07D4C00CD6D97 /* WasmBBQJIT.cpp in Sources */, 4615E46B2B5849F4001D4D53 /* WasmBBQJIT64.cpp in Sources */, 642D20D52B476A2E0030545E /* WasmIPIntSlowPaths.cpp in Sources */, diff --git a/Source/JavaScriptCore/Scripts/generate-unified-sources.sh b/Source/JavaScriptCore/Scripts/generate-unified-sources.sh index dca598a88b93..6aa33977bba5 100755 --- a/Source/JavaScriptCore/Scripts/generate-unified-sources.sh +++ b/Source/JavaScriptCore/Scripts/generate-unified-sources.sh @@ -10,7 +10,7 @@ if [ -z "${BUILD_SCRIPTS_DIR}" ]; then BUILD_SCRIPTS_DIR="${WTF_BUILD_SCRIPTS_DIR}" fi -UnifiedSourceCppFileCount=166 +UnifiedSourceCppFileCount=167 UnifiedSourceCFileCount=5 UnifiedSourceMmFileCount=0 UnifiedSourceNonARCMmFileCount=5 diff --git a/Source/JavaScriptCore/Sources.txt b/Source/JavaScriptCore/Sources.txt index 2cd6fa2dab27..f0e0629155ad 100644 --- a/Source/JavaScriptCore/Sources.txt +++ b/Source/JavaScriptCore/Sources.txt @@ -1152,11 +1152,15 @@ runtime/WideningNumberPredictionFuzzerAgent.cpp runtime/WrapForValidIteratorPrototype.cpp runtime/temporal/core/CalendarArithmetic.cpp +runtime/temporal/core/CalendarFields.cpp runtime/temporal/core/CalendarICUBridge.cpp +runtime/temporal/core/DurationArithmetic.cpp runtime/temporal/core/InstantCore.cpp runtime/temporal/core/ISOArithmetic.cpp +runtime/temporal/core/PlainDateTimeCore.cpp runtime/temporal/core/Rounding.cpp runtime/temporal/core/TimeZoneICUBridge.cpp +runtime/temporal/core/ZonedDateTimeCore.cpp tools/CellList.cpp tools/CharacterPropertyDataGenerator.cpp diff --git a/Source/JavaScriptCore/UnifiedSources-output.xcfilelist b/Source/JavaScriptCore/UnifiedSources-output.xcfilelist index e4d4b80a1468..c441c524c98a 100644 --- a/Source/JavaScriptCore/UnifiedSources-output.xcfilelist +++ b/Source/JavaScriptCore/UnifiedSources-output.xcfilelist @@ -76,6 +76,7 @@ $(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSourc $(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSource164.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSource165.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSource166.cpp +$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSource167.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSource17.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSource18.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/unified-sources/UnifiedSource19.cpp diff --git a/Source/JavaScriptCore/runtime/ISO8601.h b/Source/JavaScriptCore/runtime/ISO8601.h index fb0fa892fb9b..88c4b5afa35a 100644 --- a/Source/JavaScriptCore/runtime/ISO8601.h +++ b/Source/JavaScriptCore/runtime/ISO8601.h @@ -276,7 +276,7 @@ class InternalDuration final { Duration dateDuration() const { return m_dateDuration; } - static InternalDuration NODELETE combineDateAndTimeDuration(Duration, Int128); + static InternalDuration NODELETE JS_EXPORT_PRIVATE combineDateAndTimeDuration(Duration, Int128); private: // Time fields are ignored @@ -487,7 +487,7 @@ uint8_t daysInMonth(uint8_t month); String formatTimeZoneOffsetString(int64_t); String temporalTimeToString(PlainTime, std::tuple); String temporalDateToString(PlainDate); -String temporalDateTimeToString(PlainDate, PlainTime, std::tuple); +String JS_EXPORT_PRIVATE temporalDateTimeToString(PlainDate, PlainTime, std::tuple); String temporalYearMonthToString(PlainYearMonth, StringView); String temporalMonthDayToString(PlainMonthDay, StringView); String monthCode(uint32_t); diff --git a/Source/JavaScriptCore/runtime/IntlObject.h b/Source/JavaScriptCore/runtime/IntlObject.h index 07f58e7e4b75..133e1394d368 100644 --- a/Source/JavaScriptCore/runtime/IntlObject.h +++ b/Source/JavaScriptCore/runtime/IntlObject.h @@ -106,8 +106,8 @@ inline const LocaleSet& intlDurationFormatAvailableLocales() { return intlAvaila using CalendarID = unsigned; JS_EXPORT_PRIVATE const Vector& intlAvailableCalendars(); -extern CalendarID iso8601CalendarIDStorage; -CalendarID iso8601CalendarIDSlow(); +extern CalendarID JS_EXPORT_PRIVATE iso8601CalendarIDStorage; +CalendarID JS_EXPORT_PRIVATE iso8601CalendarIDSlow(); inline CalendarID iso8601CalendarID() { unsigned value = iso8601CalendarIDStorage; diff --git a/Source/JavaScriptCore/runtime/TemporalDuration.cpp b/Source/JavaScriptCore/runtime/TemporalDuration.cpp index c4c08c771c60..3a5627567995 100644 --- a/Source/JavaScriptCore/runtime/TemporalDuration.cpp +++ b/Source/JavaScriptCore/runtime/TemporalDuration.cpp @@ -466,7 +466,7 @@ ISO8601::Duration TemporalDuration::add(JSGlobalObject* globalObject, JSValue ot // FIXME: Implement relativeTo parameter after PlainDateTime / ZonedDateTime. auto largestUnit = std::min(largestSubduration(m_duration), largestSubduration(other)); - if (largestUnit <= TemporalUnit::Week) { + if (isCalendarUnit(largestUnit)) { throwRangeError(globalObject, scope, "Cannot add a duration of years, months, or weeks without a relativeTo option"_s); return { }; } @@ -603,7 +603,7 @@ ISO8601::Duration TemporalDuration::subtract(JSGlobalObject* globalObject, JSVal // FIXME: Implement relativeTo parameter after PlainDateTime / ZonedDateTime. auto largestUnit = std::min(largestSubduration(m_duration), largestSubduration(other)); - if (largestUnit <= TemporalUnit::Week) { + if (isCalendarUnit(largestUnit)) { throwRangeError(globalObject, scope, "Cannot subtract a duration of years, months, or weeks without a relativeTo option"_s); return { }; } @@ -906,21 +906,21 @@ void TemporalDuration::roundRelativeDuration(JSGlobalObject* globalObject, ISO86 VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - bool irregularLengthUnit = smallestUnit <= TemporalUnit::Week; + bool irregularLengthUnit = isCalendarUnit(smallestUnit); int32_t sign = duration.sign() < 0 ? -1 : 1; NudgeResult nudgeResult; if (irregularLengthUnit) { Nudged record = nudgeToCalendarUnit(globalObject, sign, duration, destEpochNs, isoDate, increment, smallestUnit, roundingMode); RETURN_IF_EXCEPTION(scope, void()); - nudgeResult = record.m_nudgeResult; + nudgeResult = record.nudgeResult; } else { nudgeResult = nudgeToDayOrTime(globalObject, duration, destEpochNs, largestUnit, increment, smallestUnit, roundingMode); RETURN_IF_EXCEPTION(scope, void()); } - duration = nudgeResult.m_duration; - if (nudgeResult.m_didExpandCalendarUnit && smallestUnit != TemporalUnit::Week) { + duration = nudgeResult.duration; + if (nudgeResult.didExpandCalendarUnit && smallestUnit != TemporalUnit::Week) { auto startUnit = smallestUnit <= TemporalUnit::Day ? smallestUnit : TemporalUnit::Day; - duration = bubbleRelativeDuration(globalObject, sign, duration, nudgeResult.m_nudgedEpochNs, isoDate, largestUnit, startUnit); + duration = bubbleRelativeDuration(globalObject, sign, duration, nudgeResult.nudgedEpochNs, isoDate, largestUnit, startUnit); RETURN_IF_EXCEPTION(scope, void()); } } @@ -1035,12 +1035,12 @@ ISO8601::Duration TemporalDuration::round(JSGlobalObject* globalObject, JSValue return { }; } - if (largestUnit <= TemporalUnit::Week || smallestUnit <= TemporalUnit::Week) [[unlikely]] { + if (isCalendarUnit(largestUnit) || isCalendarUnit(smallestUnit)) [[unlikely]] { throwVMError(globalObject, scope, "FIXME: years, months, or weeks rounding with relativeTo not implemented yet"_s); return { }; } - if (existingLargestUnit <= TemporalUnit::Week || largestUnit <= TemporalUnit::Week) [[unlikely]] { + if (isCalendarUnit(existingLargestUnit) || isCalendarUnit(largestUnit)) [[unlikely]] { throwRangeError(globalObject, scope, "Invalid largest unit for rounding"_s); return { }; } @@ -1084,7 +1084,7 @@ double TemporalDuration::total(JSGlobalObject* globalObject, JSValue optionsValu throwRangeError(globalObject, scope, "Cannot total a duration of years, months, or weeks without a relativeTo option"_s); return { }; } - if (unit <= TemporalUnit::Week) { + if (isCalendarUnit(unit)) { throwVMError(globalObject, scope, "FIXME: years, months, or weeks totalling with relativeTo not implemented yet"_s); return { }; } diff --git a/Source/JavaScriptCore/runtime/TemporalDuration.h b/Source/JavaScriptCore/runtime/TemporalDuration.h index 280751b708a4..445172a66662 100644 --- a/Source/JavaScriptCore/runtime/TemporalDuration.h +++ b/Source/JavaScriptCore/runtime/TemporalDuration.h @@ -26,29 +26,11 @@ #pragma once +#include #include namespace JSC { -class NudgeResult final { - public: - ISO8601::InternalDuration m_duration; - Int128 m_nudgedEpochNs; - bool m_didExpandCalendarUnit; - NudgeResult() { } - NudgeResult(ISO8601::InternalDuration d, Int128 ns, bool expanded) - : m_duration(d), m_nudgedEpochNs(ns), m_didExpandCalendarUnit(expanded) { } -}; - -class Nudged final { - public: - NudgeResult m_nudgeResult; - double m_total; - Nudged() { } - Nudged(NudgeResult n, double t) - : m_nudgeResult(n), m_total(t) { } -}; - class TemporalDuration final : public JSNonFinalObject { public: using Base = JSNonFinalObject; diff --git a/Source/JavaScriptCore/runtime/TemporalPlainDateTime.cpp b/Source/JavaScriptCore/runtime/TemporalPlainDateTime.cpp index 0c7f450c7e55..b12217844637 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainDateTime.cpp +++ b/Source/JavaScriptCore/runtime/TemporalPlainDateTime.cpp @@ -26,6 +26,7 @@ #include "config.h" #include "TemporalPlainDateTime.h" +#include "ISOArithmetic.h" #include "IntlObjectInlines.h" #include "JSCInlines.h" #include "LazyPropertyInlines.h" @@ -184,28 +185,6 @@ int32_t TemporalPlainDateTime::compare(TemporalPlainDateTime* plainDateTime1, Te return TemporalPlainTime::compare(plainDateTime1->plainTime(), plainDateTime2->plainTime()); } -static void incrementDay(ISO8601::Duration& duration) -{ - double year = duration.years(); - double month = duration.months(); - double day = duration.days(); - - double daysInMonth = ISO8601::daysInMonth(year, month); - if (day < daysInMonth) { - duration.setField(TemporalUnit::Day, day + 1); - return; - } - - duration.setField(TemporalUnit::Day, 1); - if (month < 12) { - duration.setField(TemporalUnit::Month, month + 1); - return; - } - - duration.setField(TemporalUnit::Month, 1); - duration.setField(TemporalUnit::Year, year + 1); -} - String TemporalPlainDateTime::toString(JSGlobalObject* globalObject, JSValue optionsValue) const { VM& vm = globalObject->vm(); @@ -232,16 +211,8 @@ String TemporalPlainDateTime::toString(JSGlobalObject* globalObject, JSValue opt RETURN_IF_EXCEPTION(scope, { }); double extraDays = duration.days(); - duration.setYears(static_cast(year())); - duration.setMonths(static_cast(month())); - duration.setDays(static_cast(day())); - if (extraDays) { - ASSERT(extraDays == 1); - incrementDay(duration); - } - - auto plainDate = TemporalPlainDate::toPlainDate(globalObject, duration); - RETURN_IF_EXCEPTION(scope, { }); + ASSERT(!extraDays || extraDays == 1); + auto plainDate = TemporalCore::balanceISODate(year(), month(), day() + static_cast(extraDays)); return ISO8601::temporalDateTimeToString(plainDate, plainTime, data.precision); } @@ -319,7 +290,7 @@ TemporalPlainDateTime* TemporalPlainDateTime::round(JSGlobalObject* globalObject return { }; } - if (smallest.value() <= TemporalUnit::Week) { + if (isCalendarUnit(smallest.value())) { throwRangeError(globalObject, scope, "smallestUnit is a disallowed unit"_s); return { }; } @@ -366,16 +337,8 @@ TemporalPlainDateTime* TemporalPlainDateTime::round(JSGlobalObject* globalObject RETURN_IF_EXCEPTION(scope, { }); double extraDays = duration.days(); - duration.setYears(static_cast(year())); - duration.setMonths(static_cast(month())); - duration.setDays(static_cast(day())); - if (extraDays) { - ASSERT(extraDays == 1); - incrementDay(duration); - } - - auto plainDate = TemporalPlainDate::toPlainDate(globalObject, duration); - RETURN_IF_EXCEPTION(scope, { }); + ASSERT(!extraDays || extraDays == 1); + auto plainDate = TemporalCore::balanceISODate(year(), month(), day() + static_cast(extraDays)); RELEASE_AND_RETURN(scope, TemporalPlainDateTime::tryCreateIfValid(globalObject, globalObject->plainDateTimeStructure(), WTF::move(plainDate), WTF::move(plainTime))); } diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp new file mode 100644 index 000000000000..66c35e72f48f --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp @@ -0,0 +1,734 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "CalendarFields.h" + +#include "CalendarArithmetic.h" +#include "CalendarICUBridge.h" +#include "ISO8601.h" +#include "IntlObject.h" +#include "JSCInlines.h" +#include "TemporalCalendar.h" +#include "TemporalObject.h" +#include +#include +#include + +namespace JSC { +namespace TemporalCore { + +// temporal_rs: fields.rs ROUGH_YEAR_RANGE = -300000..300000 +static constexpr int32_t safeYearMin = -300000; +static constexpr int32_t safeYearMax = 300000; +// ISO reference years for PlainMonthDay — spec: CalendarMonthDayToISOReferenceDate +// 1972 is a leap year (divisible by 4, not a century), 1971 is not. +static constexpr int32_t isoMonthDayReferenceLeapYear = 1972; +static constexpr int32_t isoMonthDayReferenceNonLeapYear = 1971; + +// temporal_rs: fields.rs check_year_in_safe_arithmetical_range +static TemporalResult checkYearRange(const CalendarFieldsIn& fields) +{ + if (fields.year && (*fields.year < safeYearMin || *fields.year > safeYearMax)) + return makeUnexpected(rangeError("Date is not within representable range"_s)); + if (fields.eraYear && (*fields.eraYear < safeYearMin || *fields.eraYear > safeYearMax)) + return makeUnexpected(rangeError("eraYear is not within representable range"_s)); + return { }; +} + +// temporal_rs: types.rs ResolvedIsoFields::try_from_fields (ISO-only resolution) +enum class ISOResolveType : uint8_t { + Date, + YearMonth, + MonthDay +}; + +struct ResolvedISOFields { + int32_t year; + uint8_t month; + uint8_t day; +}; + +// resolveISOFields — internal helper; implements the ISO path of three spec AOs, parameterized by ISOResolveType: +// Date -> ISODateFromFields (#sec-temporal-isodatefromfields) +// YearMonth -> ISOYearMonthFromFields (#sec-temporal-isoyearmonthfromfields) +// MonthDay -> ISOMonthDayFromFields (#sec-temporal-isomonthdayfromfields) +// temporal_rs: types.rs ResolvedIsoFields::try_from_fields +static TemporalResult resolveISOFields(const CalendarFieldsIn& fields, TemporalOverflow overflow, ISOResolveType type) +{ + auto rangeCheck = checkYearRange(fields); + if (!rangeCheck) + return makeUnexpected(rangeCheck.error()); + + // Resolve year: MonthDay uses reference year; others require explicit year field. + int32_t year; + if (type == ISOResolveType::MonthDay) + year = isoMonthDayReferenceLeapYear; + else { + if (!fields.year) + return makeUnexpected(typeError("year property must be present"_s)); + year = *fields.year; + } + + // Resolve day: YearMonth uses day=1; others require explicit day field. + uint8_t day; + if (type == ISOResolveType::YearMonth) + day = 1; + else { + if (!fields.day) + return makeUnexpected(typeError("day property must be present"_s)); + day = *fields.day; + } + + // Resolve month: prefer month over monthCode; validate consistency if both present. + uint32_t month; + if (fields.month && !fields.monthCode) { + month = *fields.month; + } else if (fields.monthCode) { + auto& mc = *fields.monthCode; + if (mc.isLeapMonth) + return makeUnexpected(rangeError("iso8601 calendar does not have leap months"_s)); + if (mc.monthNumber < 1 || mc.monthNumber > 12) + return makeUnexpected(rangeError("month must be 1-12 for iso8601 calendar"_s)); + uint8_t codeMonth = static_cast(mc.monthNumber); + if (fields.month && *fields.month != codeMonth) + return makeUnexpected(rangeError("month does not match monthCode"_s)); + month = codeMonth; + } else if (!fields.month) + return makeUnexpected(typeError("month or monthCode property must be present"_s)); + else + month = *fields.month; + + // Constrain or reject month and day per overflow. + if (month < 1 || month > 12) { + if (overflow == TemporalOverflow::Constrain) + month = clampTo(month, 1, 12); + else + return makeUnexpected(rangeError("month is out of range"_s)); + } + + uint8_t maxDay = ISO8601::daysInMonth(year, month); + if (day < 1 || day > maxDay) { + if (overflow == TemporalOverflow::Constrain) + day = clampTo(day, 1, maxDay); + else + return makeUnexpected(rangeError("day is out of range"_s)); + } + + return ResolvedISOFields { year, static_cast(month), day }; +} + +// CalendarDateFromFields — temporal_rs: Calendar::date_from_fields (src/builtins/core/calendar.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-calendardatefromfields +// Implements steps 1-4; PrepareCalendarFields done by JS-layer caller. +TemporalResult dateFromFields(CalendarID calendarId, const CalendarFieldsIn& fields, TemporalOverflow overflow) +{ + bool isISO = calendarIsISO(calendarId); + + // Steps 1-2: CalendarResolveFields + CalendarDateToISO — fused into resolveISOFields (ISO path). + if (isISO) { + auto resolved = resolveISOFields(fields, overflow, ISOResolveType::Date); + if (!resolved) + return makeUnexpected(resolved.error()); + + auto isoDate = ISO8601::PlainDate(resolved->year, resolved->month, resolved->day); + // Step 3: If ISODateWithinLimits(result) is false, throw RangeError. + if (!ISO8601::isDateTimeWithinLimits(isoDate.year(), isoDate.month(), isoDate.day(), 12, 0, 0, 0, 0, 0)) + return makeUnexpected(rangeError("Date is not within representable range"_s)); + + // Step 4: Return result. + return ResolvedCalendarDate { isoDate, "iso8601"_s }; + } + + // Steps 1-2 (non-ISO): CalendarResolveFields + CalendarDateToISO via ICU bridge. + auto rangeCheck = checkYearRange(fields); + if (!rangeCheck) + return makeUnexpected(rangeCheck.error()); + + // Non-lunisolar calendars have no leap months — reject leap month codes. + if (fields.monthCode && fields.monthCode->isLeapMonth && !calendarIsLunisolar(calendarId)) + return makeUnexpected(rangeError("Leap month codes are not valid for this calendar"_s)); + + bool hasEraYear = fields.era.has_value() && fields.eraYear.has_value(); + if (!fields.year && !hasEraYear) + return makeUnexpected(typeError("year property must be present"_s)); + if (!fields.day) + return makeUnexpected(typeError("day property must be present"_s)); + + // Resolve month from month/monthCode + uint8_t month = 1; + if (fields.month) + month = *fields.month; + else if (fields.monthCode) + month = static_cast(fields.monthCode->monthNumber); + + int32_t year = fields.year.value_or(0); + std::optional era; + if (fields.era) + era = StringView(*fields.era); + + auto result = calendarDateFromFields(calendarId, year, month, *fields.day, era, fields.eraYear, fields.monthCode, overflow); + if (!result) + return makeUnexpected(result.error()); + + // Step 3: If ISODateWithinLimits(result) is false, throw RangeError. + if (!ISO8601::isDateTimeWithinLimits(result->year(), result->month(), result->day(), 12, 0, 0, 0, 0, 0)) + return makeUnexpected(rangeError("Date is not within representable range"_s)); + + // Step 4: Return result. + return ResolvedCalendarDate { *result, calendarIDToString(calendarId).toString() }; +} + +// CalendarYearMonthFromFields — temporal_rs: Calendar::year_month_from_fields (src/builtins/core/calendar.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields +// Implements steps 1-5; PrepareCalendarFields done by JS-layer caller. +TemporalResult yearMonthFromFields(CalendarID calendarId, const CalendarFieldsIn& fields, TemporalOverflow overflow) +{ + bool isISO = calendarIsISO(calendarId); + + // Steps 1-3: set [[Day]]=1 + CalendarResolveFields + CalendarDateToISO — fused into resolveISOFields. + if (isISO) { + auto resolved = resolveISOFields(fields, overflow, ISOResolveType::YearMonth); + if (!resolved) + return makeUnexpected(resolved.error()); + + auto isoDate = ISO8601::PlainDate(resolved->year, resolved->month, resolved->day); + // Step 4: If ISOYearMonthWithinLimits(result) is false, throw RangeError. + if (!ISO8601::isYearMonthWithinLimits(isoDate.year(), isoDate.month())) + return makeUnexpected(rangeError("YearMonth is not within representable range"_s)); + + // Step 5: Return result. + return ResolvedCalendarDate { isoDate, "iso8601"_s }; + } + + // Steps 1-3 (non-ISO): set [[Day]]=1 + CalendarResolveFields + CalendarDateToISO via ICU bridge. + auto rangeCheck = checkYearRange(fields); + if (!rangeCheck) + return makeUnexpected(rangeCheck.error()); + + // Non-lunisolar calendars have no leap months — reject leap month codes. + if (fields.monthCode && fields.monthCode->isLeapMonth && !calendarIsLunisolar(calendarId)) + return makeUnexpected(rangeError("Leap month codes are not valid for this calendar"_s)); + + bool hasEraYear = fields.era.has_value() && fields.eraYear.has_value(); + if (!fields.year && !hasEraYear) + return makeUnexpected(typeError("year property must be present"_s)); + + uint8_t month = 1; + if (fields.month) + month = *fields.month; + else if (fields.monthCode) + month = static_cast(fields.monthCode->monthNumber); + + int32_t year = fields.year.value_or(0); + std::optional era; + if (fields.era) + era = StringView(*fields.era); + + auto result = calendarDateFromFields(calendarId, year, month, 1, era, fields.eraYear, fields.monthCode, overflow); + if (!result) + return makeUnexpected(result.error()); + + // Step 4: If ISOYearMonthWithinLimits(result) is false, throw RangeError. + if (!ISO8601::isYearMonthWithinLimits(result->year(), result->month())) + return makeUnexpected(rangeError("YearMonth is not within representable range"_s)); + + // Step 5: Return result. + return ResolvedCalendarDate { *result, calendarIDToString(calendarId).toString() }; +} + +// CalendarMonthDayFromFields — temporal_rs: Calendar::month_day_from_fields (src/builtins/core/calendar.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields +// Implements steps 1-4; PrepareCalendarFields done by JS-layer caller. +TemporalResult monthDayFromFields(CalendarID calendarId, const CalendarFieldsIn& fields, TemporalOverflow overflow) +{ + bool isISO = calendarIsISO(calendarId); + + // Steps 1-2: CalendarResolveFields + CalendarMonthDayToISOReferenceDate — fused inline (ISO path). + if (isISO) { + // temporal_rs: if year is provided, validate day against that year first, + // then use reference year 1972 for the stored date. + // Per spec: the year is ONLY used for overflow (leap year check), NOT for range validation. + if (fields.year) { + // Substitute a proxy year with the same leap-year property to avoid range errors. + // This matches the spec requirement that year is not range-checked for PlainMonthDay. + CalendarFieldsIn fieldsForOverflow = fields; + int32_t yr = *fields.year; + // Determine leap year status: divisible by 4, except centuries unless also by 400. + bool isLeap = !(yr % 400) || (!(yr % 4) && yr % 100); + fieldsForOverflow.year = isLeap ? isoMonthDayReferenceLeapYear : isoMonthDayReferenceNonLeapYear; + auto resolved = resolveISOFields(fieldsForOverflow, overflow, ISOResolveType::Date); + if (!resolved) + return makeUnexpected(resolved.error()); + // Step 3: Assert: ISODateWithinLimits(result) — always holds for reference year 1972. + ASSERT(ISO8601::isDateTimeWithinLimits(isoMonthDayReferenceLeapYear, resolved->month, resolved->day, 12, 0, 0, 0, 0, 0)); + auto isoDate = ISO8601::PlainDate(isoMonthDayReferenceLeapYear, resolved->month, resolved->day); + // Step 4: Return result. + return ResolvedCalendarDate { isoDate, "iso8601"_s }; + } + auto resolved = resolveISOFields(fields, overflow, ISOResolveType::MonthDay); + if (!resolved) + return makeUnexpected(resolved.error()); + + auto isoDate = ISO8601::PlainDate(isoMonthDayReferenceLeapYear, resolved->month, resolved->day); + // Step 3: Assert: ISODateWithinLimits(result) — always holds for reference year 1972. + ASSERT(ISO8601::isDateTimeWithinLimits(isoMonthDayReferenceLeapYear, resolved->month, resolved->day, 12, 0, 0, 0, 0, 0)); + // Step 4: Return result. + return ResolvedCalendarDate { isoDate, "iso8601"_s }; + } + + // Steps 1-2 (non-ISO): CalendarResolveFields + CalendarMonthDayToISOReferenceDate via ICU bridge. + auto rangeCheck = checkYearRange(fields); + if (!rangeCheck) + return makeUnexpected(rangeCheck.error()); + + // Non-lunisolar calendars have no leap months — reject leap month codes. + if (fields.monthCode && fields.monthCode->isLeapMonth && !calendarIsLunisolar(calendarId)) + return makeUnexpected(rangeError("Leap month codes are not valid for this calendar"_s)); + + if (!fields.day) + return makeUnexpected(typeError("day property must be present"_s)); + + // Non-ISO MonthDay requires monthCode (not just month). + if (!fields.monthCode && !fields.year) + return makeUnexpected(typeError("monthCode is required for non-ISO calendar MonthDay"_s)); + + uint8_t month = 1; + if (fields.month) + month = *fields.month; + else if (fields.monthCode) + month = static_cast(fields.monthCode->monthNumber); + + // Default year: use ecmaReferenceYear (ported from icu4x) for non-ISO MonthDay. + int32_t year; + bool usedRegularMonthFallback = false; + if (fields.year) + year = *fields.year; + else if (fields.monthCode) { + year = ecmaReferenceYear(calendarId, fields.monthCode->monthNumber, fields.monthCode->isLeapMonth, fields.day ? *fields.day : 1); + // icu4x: UseRegularIfConstrain — leap month has no reference year near 1972. + // Constrain: fall back to the non-leap version's reference year AND strip the leap flag. + // Reject: throw RangeError (this leap month configuration doesn't exist). + if (year == ecmaRefYearNotInCalendar) + return makeUnexpected(rangeError("This month code does not exist in this calendar"_s)); + if (year == ecmaRefYearUseRegular) { + if (overflow == TemporalOverflow::Constrain) { + year = ecmaReferenceYear(calendarId, fields.monthCode->monthNumber, false, fields.day ? *fields.day : 1); + usedRegularMonthFallback = true; + } else + return makeUnexpected(rangeError("This leap month does not exist in this calendar near the reference year"_s)); + } + } else + RELEASE_ASSERT_NOT_REACHED(); + + // ecmaReferenceYear returns ISO proleptic years (e.g., 1972). + // Chinese and Dangi are lunisolar calendars sharing the same ICU code path (chnsecal.cpp). + // On older Apple ICU, UCAL_EXTENDED_YEAR uses epoch-based counting instead of ISO years. + // We probe what year ICU assigns to ISO 1972-02-15 (Chinese New Year 1972), then use + // chineseEpochOffset = probe_year - 1972 + // where probe_year = 1972 on modern ICU (offset = 0, no-op) or the epoch-based year on + // older Apple ICU (offset = that year - 1972). + static const int32_t chineseExtYear1972 = chineseCalendarExtendedYearFor1972(); + static const int32_t chineseEpochOffset = chineseExtYear1972 - 1972; + auto calStr = calendarIDToString(calendarId); + bool isChineseOrDangi = (calStr == "chinese"_s || calStr == "dangi"_s); + if (!fields.year && isChineseOrDangi) + year += chineseEpochOffset; + std::optional era; + if (fields.era) + era = StringView(*fields.era); + + // When using the regular-month fallback, use a non-leap monthCode so ICU doesn't + // look for a leap month in the reference year. + std::optional regularMonthCode; + const std::optional* effectiveMonthCode = &fields.monthCode; + if (usedRegularMonthFallback && fields.monthCode) { + regularMonthCode = ParsedMonthCode { fields.monthCode->monthNumber, false }; + effectiveMonthCode = ®ularMonthCode; + } + + auto result = calendarDateFromFields(calendarId, year, month, *fields.day, era, fields.eraYear, *effectiveMonthCode, overflow); + if (!result) + return makeUnexpected(result.error()); + + // For MonthDay with year provided: validate ISO range, then re-resolve with + // reference year to get canonical reference ISO date per spec. + if (fields.year || (fields.era && fields.eraYear)) { + if (!ISO8601::isYearWithinLimits(result->year())) + return makeUnexpected(rangeError("Date is not within representable range"_s)); + + // Re-resolve: get monthCode+day from first resolution, then use ecmaReferenceYear. + auto resolvedFields = isoToCalendarFields(calendarId, *result); + if (resolvedFields && !resolvedFields->monthCode.isEmpty()) { + auto resolvedMonthCode = ISO8601::parseMonthCode(resolvedFields->monthCode); + if (resolvedMonthCode) { + int32_t refYear = ecmaReferenceYear(calendarId, resolvedMonthCode->monthNumber, resolvedMonthCode->isLeapMonth, resolvedFields->day); + if (isChineseOrDangi && chineseEpochOffset) + refYear += chineseEpochOffset; + auto refResult = calendarDateFromFields(calendarId, refYear, resolvedFields->month, resolvedFields->day, std::nullopt, std::nullopt, resolvedMonthCode, TemporalOverflow::Constrain); + if (refResult) + return ResolvedCalendarDate { *refResult, calendarIDToString(calendarId).toString() }; + } + } + } + + // Step 3: Assert: ISODateWithinLimits(result) — reference year is always within limits. + ASSERT(ISO8601::isDateTimeWithinLimits(result->year(), result->month(), result->day(), 12, 0, 0, 0, 0, 0)); + // Step 4: Return result. + return ResolvedCalendarDate { *result, calendarIDToString(calendarId).toString() }; +} + +// plainYearMonthWith — temporal_rs: PlainYearMonth::with (src/builtins/core/plain_year_month.rs) +// https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.with +// Implements inner merge steps; IsPartialTemporalObject and PrepareCalendarFields done by JS-layer caller. +TemporalResult plainYearMonthWith(CalendarID calendarId, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow overflow) +{ + // Step 3: Assert at least one field is user-provided (IsPartialTemporalObject equivalent). + if (!partialFields.year && !partialFields.month && !partialFields.monthCode + && !partialFields.era && !partialFields.eraYear) + return makeUnexpected(typeError("at least one field (year, month, monthCode, era, or eraYear) must be provided"_s)); + + bool isISO = calendarIsISO(calendarId); + + if (isISO) { + // ISO: same merge logic as temporal_rs with_fallback_year_month. + CalendarFieldsIn merged; + merged.year = partialFields.year.has_value() ? partialFields.year : std::optional(currentISODate.year()); + // temporal_rs: impl_with_fallback_method! — month: user's value only, no fallback + merged.month = partialFields.month; + // monthCode: user's overrides; fallback from current only if neither month nor monthCode provided + merged.monthCode = partialFields.monthCode; + if (!partialFields.month.has_value() && !partialFields.monthCode) + merged.monthCode = ISO8601::parseMonthCode(ISO8601::monthCode(currentISODate.month())); + return yearMonthFromFields(calendarId, merged, overflow); + } + + // Non-ISO: get calendar fields from current date, merge with user partial fields. + // temporal_rs: impl_with_fallback_method! macro and impl_field_keys_to_ignore! macro (fields.rs). + auto calFields = isoToCalendarFields(calendarId, currentISODate); + if (!calFields) + return makeUnexpected(calFields.error()); + + // temporal_rs: impl_field_keys_to_ignore! — determine which fallback fields to suppress + bool keysToIgnoreMonth = partialFields.month.has_value() || partialFields.monthCode.has_value(); + bool hasEras = calendarHasEras(calendarId); + bool keysToIgnoreEra = false; + bool keysToIgnoreYear = false; + if (hasEras) { + if (partialFields.year.has_value() || partialFields.eraYear.has_value() || partialFields.era.has_value()) { + keysToIgnoreEra = true; + keysToIgnoreYear = true; + } + } + + CalendarFieldsIn merged; + + // temporal_rs: impl_with_fallback_method! — era/eraYear: fallback from current if !keysToIgnoreEra + merged.era = partialFields.era; + merged.eraYear = partialFields.eraYear; + if (!keysToIgnoreEra) { + if (!merged.era && calFields->era.has_value() && !calFields->era->isEmpty()) + merged.era = *calFields->era; + if (!merged.eraYear && calFields->eraYear.has_value()) + merged.eraYear = *calFields->eraYear; + } + + // temporal_rs: impl_with_fallback_method! — year: fallback from current if !keysToIgnoreYear + merged.year = partialFields.year; + if (!keysToIgnoreYear && !merged.year) + merged.year = std::optional(calFields->year); + + // temporal_rs: impl_with_fallback_method! — month: user's value only, NEVER fallback + merged.month = partialFields.month; + + // temporal_rs: impl_with_fallback_method! — monthCode: fallback ONLY when neither month nor monthCode provided + merged.monthCode = partialFields.monthCode; + if (!partialFields.month.has_value() && !partialFields.monthCode.has_value() && !keysToIgnoreMonth) { + if (!calFields->monthCode.isEmpty()) + merged.monthCode = ISO8601::parseMonthCode(calFields->monthCode); + } + + return yearMonthFromFields(calendarId, merged, overflow); +} + +// differenceYearMonth — temporal_rs: PlainYearMonth::diff (src/builtins/core/plain_year_month.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplainyearmonth +// Implements steps 7-14; steps 1-6 and rounding (steps 15-16) done by JS-layer caller. +TemporalResult differenceYearMonth(CalendarID calendarId, const ISO8601::PlainDate& thisISODate, const ISO8601::PlainDate& otherISODate, TemporalUnit largestUnit) +{ + bool isISO = calendarIsISO(calendarId); + + if (isISO) { + // Steps 7-8: set [[Day]]=1 on both dates; ISODateWithinLimits check. + auto thisDate = ISO8601::PlainDate(thisISODate.year(), thisISODate.month(), 1); + auto otherDate = ISO8601::PlainDate(otherISODate.year(), otherISODate.month(), 1); + if (std::abs(dateToDaysFrom1970(thisDate.year(), static_cast(thisDate.month()) - 1, 1)) > 1e8 + || std::abs(dateToDaysFrom1970(otherDate.year(), static_cast(otherDate.month()) - 1, 1)) > 1e8) + return makeUnexpected(rangeError("date is outside the representable range for Temporal"_s)); + // Steps 10-14: CalendarDateUntil(thisDate, otherDate, largestUnit). + return calendarDateUntil(thisDate, otherDate, largestUnit); + } + + // Non-ISO: resolve both to day=1 via dateFromFields (matching temporal_rs). + // This re-resolves the ISO date through the calendar pipeline with day=1. + // Step 7: ISODateToFields(calendar, thisISODate, year-month); set [[Day]] to 1; CalendarDateFromFields. + auto thisCalFields = isoToCalendarFields(calendarId, thisISODate); + if (!thisCalFields) + return makeUnexpected(thisCalFields.error()); + CalendarFieldsIn thisFields; + thisFields.year = thisCalFields->year; + thisFields.month = thisCalFields->month; + thisFields.day = 1; + if (!thisCalFields->monthCode.isEmpty()) + thisFields.monthCode = ISO8601::parseMonthCode(thisCalFields->monthCode); + auto thisResolved = dateFromFields(calendarId, thisFields, TemporalOverflow::Constrain); + if (!thisResolved) + return makeUnexpected(thisResolved.error()); + + // Steps 8-9: ISODateToFields(calendar, otherISODate, year-month); set [[Day]] to 1; CalendarDateFromFields. + auto otherCalFields = isoToCalendarFields(calendarId, otherISODate); + if (!otherCalFields) + return makeUnexpected(otherCalFields.error()); + CalendarFieldsIn otherFields; + otherFields.year = otherCalFields->year; + otherFields.month = otherCalFields->month; + otherFields.day = 1; + if (!otherCalFields->monthCode.isEmpty()) + otherFields.monthCode = ISO8601::parseMonthCode(otherCalFields->monthCode); + auto otherResolved = dateFromFields(calendarId, otherFields, TemporalOverflow::Constrain); + if (!otherResolved) + return makeUnexpected(otherResolved.error()); + + // Steps 10-14: CalendarDateUntil(thisDate, otherDate, largestUnit). + return calendarDateUntil(calendarId, thisResolved->isoDate, otherResolved->isoDate, largestUnit); +} + +// plainYearMonthAdd — temporal_rs: PlainYearMonth::add_duration (src/builtins/core/plain_year_month.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-adddurationtoyearmonth +// Implements steps 9-15; steps 1-8 done by JS-layer caller. +TemporalResult plainYearMonthAdd(CalendarID calendarId, const ISO8601::PlainDate& currentISODate, const ISO8601::Duration& duration, TemporalOverflow overflow) +{ + bool isISO = calendarIsISO(calendarId); + + if (isISO) { + // Step 1: Get calendar fields from currentISODate (year + monthCode), set day=1. + CalendarFieldsIn fields; + fields.year = currentISODate.year(); + fields.month = currentISODate.month(); + fields.monthCode = ISO8601::parseMonthCode(ISO8601::monthCode(currentISODate.month())); + fields.day = 1; + // Step 2: CalendarDateFromFields -> PlainDate. + auto dateResult = dateFromFields(calendarId, fields, TemporalOverflow::Constrain); + if (!dateResult) + return makeUnexpected(dateResult.error()); + // Step 3: CalendarDateAdd(duration) -> addedDate. + // ISO: bypass ICU bridge and use pure isoDateAdd — handles extreme boundary dates + // correctly (year ±271821) without ICU calendar clamping. + auto addedISO = calendarDateAdd(dateResult->isoDate, duration, overflow); + if (!addedISO) + return makeUnexpected(addedISO.error()); + // Steps 4-5: Get year + monthCode from addedDate; CalendarYearMonthFromFields -> result PYM ISO date. + CalendarFieldsIn addedFields; + addedFields.year = addedISO->year(); + addedFields.monthCode = ISO8601::parseMonthCode(ISO8601::monthCode(addedISO->month())); + return yearMonthFromFields(calendarId, addedFields, overflow); + } + + // Non-ISO: temporal_rs add_duration steps 9-15. + // Step 1: ISODateToFields(calendar, yearMonth.[[ISODate]], year-month) -> year + monthCode, day=1. + auto calFields = isoToCalendarFields(calendarId, currentISODate); + if (!calFields) + return makeUnexpected(calFields.error()); + + CalendarFieldsIn fieldsDay1; + fieldsDay1.year = calFields->year; + fieldsDay1.day = 1; + if (!calFields->monthCode.isEmpty()) + fieldsDay1.monthCode = ISO8601::parseMonthCode(calFields->monthCode); + else + fieldsDay1.month = calFields->month; + if (calFields->era.has_value() && calFields->eraYear.has_value()) { + fieldsDay1.era = *calFields->era; + fieldsDay1.eraYear = *calFields->eraYear; + } + + // Step 2: CalendarDateFromFields -> PlainDate ISO. + auto dateResult = dateFromFields(calendarId, fieldsDay1, TemporalOverflow::Constrain); + if (!dateResult) + return makeUnexpected(dateResult.error()); + + // Step 3: CalendarDateAdd(duration) -> addedDate ISO. + auto addedISO = calendarDateAdd(calendarId, dateResult->isoDate, duration, overflow); + if (!addedISO) + return makeUnexpected(addedISO.error()); + + // Step 4: Get year + monthCode from addedDate. + auto addedCalFields = isoToCalendarFields(calendarId, *addedISO); + if (!addedCalFields) + return makeUnexpected(addedCalFields.error()); + + CalendarFieldsIn addedFields; + addedFields.year = addedCalFields->year; + if (!addedCalFields->monthCode.isEmpty()) + addedFields.monthCode = ISO8601::parseMonthCode(addedCalFields->monthCode); + else + addedFields.month = addedCalFields->month; + if (addedCalFields->era.has_value() && addedCalFields->eraYear.has_value()) { + addedFields.era = *addedCalFields->era; + addedFields.eraYear = *addedCalFields->eraYear; + } + + // Step 5: CalendarYearMonthFromFields -> result PYM ISO date. + return yearMonthFromFields(calendarId, addedFields, overflow); +} + +// plainYearMonthToPlainDate — temporal_rs: PlainYearMonth::to_plain_date (src/builtins/core/plain_year_month.rs) +// https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.toplaindate +// Implements steps 5, 7, 8; steps 1-4, 6, 9 done by JS-layer caller. +TemporalResult plainYearMonthToPlainDate(CalendarID calendarId, const ISO8601::PlainDate& pymISODate, uint8_t day) +{ + bool isISO = calendarIsISO(calendarId); + + if (isISO) { + // Steps 5+7 (ISO): year/month from pymISODate; merge day. + CalendarFieldsIn fields; + fields.year = pymISODate.year(); + fields.month = pymISODate.month(); + fields.day = day; + // Step 8: CalendarDateFromFields(~constrain~). + return dateFromFields(calendarId, fields, TemporalOverflow::Constrain); + } + + // Steps 5+7 (non-ISO): ISODateToFields via ICU bridge; merge day. + auto yearResult = calendarYear(calendarId, pymISODate); + if (!yearResult) + return makeUnexpected(yearResult.error()); + auto monthCodeStr = calendarMonthCode(calendarId, pymISODate); + if (!monthCodeStr) + return makeUnexpected(monthCodeStr.error()); + + CalendarFieldsIn fields; + fields.year = *yearResult; + fields.day = day; + fields.monthCode = ISO8601::parseMonthCode(*monthCodeStr); + // Step 8: CalendarDateFromFields(~constrain~). + return dateFromFields(calendarId, fields, TemporalOverflow::Constrain); +} + +// plainYearMonthFromISODate — no 1:1 temporal_rs function; inlined in PlainYearMonth::from_parsed +// https://tc39.es/proposal-temporal/#sec-temporal-totemporalyearmonth (string parse path) +// Implements steps 10, 12; step 9 (ISOYearMonthWithinLimits) done by JS-layer caller. +TemporalResult plainYearMonthFromISODate(CalendarID calendarId, const ISO8601::PlainDate& fullISODate) +{ + bool isISO = calendarIsISO(calendarId); + + if (isISO) { + // ISO path: year/month from fullISODate; yearMonthFromFields stores with day=1. + CalendarFieldsIn fields; + fields.year = fullISODate.year(); + fields.month = fullISODate.month(); + return yearMonthFromFields(calendarId, fields, TemporalOverflow::Constrain); + } + + // Step 10 (non-ISO): ISODateToFields via ICU bridge — gets year + monthCode. + auto yearResult = calendarYear(calendarId, fullISODate); + if (!yearResult) + return makeUnexpected(yearResult.error()); + auto monthCodeStr = calendarMonthCode(calendarId, fullISODate); + if (!monthCodeStr) + return makeUnexpected(monthCodeStr.error()); + + CalendarFieldsIn fields; + fields.year = *yearResult; + fields.day = 1; + fields.monthCode = ISO8601::parseMonthCode(*monthCodeStr); + // Step 12: CalendarYearMonthFromFields(~constrain~) per spec note. + return yearMonthFromFields(calendarId, fields, TemporalOverflow::Constrain); +} + +// plainMonthDayToPlainDate — temporal_rs: PlainMonthDay::to_plain_date (src/builtins/core/plain_month_day.rs) +// https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.toplaindate +// Implements steps 5, 7, 8; steps 1-4, 6, 9 done by JS-layer caller. +TemporalResult plainMonthDayToPlainDate(CalendarID calendarId, const ISO8601::PlainDate& pmdISODate, int32_t year) +{ + bool isISO = calendarIsISO(calendarId); + + if (isISO) { + // Steps 5+7: ISO ISODateToFields gives month directly; merge year. + CalendarFieldsIn fields; + fields.year = year; + fields.month = pmdISODate.month(); + fields.day = pmdISODate.day(); + // Step 8: CalendarDateFromFields(~constrain~). + return dateFromFields(calendarId, fields, TemporalOverflow::Constrain); + } + + // Steps 5+7 (non-ISO): ISODateToFields via ICU bridge, then merge year. + auto monthCodeStr = calendarMonthCode(calendarId, pmdISODate); + if (!monthCodeStr) + return makeUnexpected(monthCodeStr.error()); + auto dayResult = calendarDay(calendarId, pmdISODate); + if (!dayResult) + return makeUnexpected(dayResult.error()); + + CalendarFieldsIn fields; + fields.year = year; + fields.monthCode = ISO8601::parseMonthCode(*monthCodeStr); + fields.day = static_cast(*dayResult); + // Step 8: CalendarDateFromFields(~constrain~). + return dateFromFields(calendarId, fields, TemporalOverflow::Constrain); +} + +// https://tc39.es/proposal-temporal/#sec-temporal-totemporalmonthday (string parse path) +// Implements steps 10, 12; step 9 (ISODateWithinLimits) done by JS-layer caller. +TemporalResult plainMonthDayFromISODate(CalendarID calendarId, const ISO8601::PlainDate& fullISODate, TemporalOverflow overflow) +{ + bool isISO = calendarIsISO(calendarId); + + if (isISO) { + // ISO path (spec step 7): store directly with reference year 1972. + CalendarFieldsIn fields; + fields.month = fullISODate.month(); + fields.day = fullISODate.day(); + return monthDayFromFields(calendarId, fields, overflow); + } + + // Step 10: ISODateToFields(calendar, fullISODate, ~month-day~) — gets monthCode + day. + auto monthCodeStr = calendarMonthCode(calendarId, fullISODate); + if (!monthCodeStr) + return makeUnexpected(monthCodeStr.error()); + auto dayResult = calendarDay(calendarId, fullISODate); + if (!dayResult) + return makeUnexpected(dayResult.error()); + + CalendarFieldsIn fields; + fields.monthCode = ISO8601::parseMonthCode(*monthCodeStr); + fields.day = static_cast(*dayResult); + // Step 12: caller must pass TemporalOverflow::Constrain per spec note. + return monthDayFromFields(calendarId, fields, overflow); +} + +} // namespace TemporalCore +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h new file mode 100644 index 000000000000..b6fccd636092 --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// JSC Temporal Core — Calendar field resolution (dateFromFields, yearMonthFromFields, monthDayFromFields) +// temporal_rs reference: src/builtins/core/calendar.rs, types.rs, fields.rs + +#include +#include +#include +#include +#include +#include +#include + +namespace JSC { + +struct ParsedMonthCode; + +namespace TemporalCore { + +// Input fields for calendar date resolution. +// Maps to temporal_rs CalendarFields. +struct CalendarFieldsIn { + std::optional year; + std::optional month; + std::optional monthCode; + std::optional day; + std::optional era; + std::optional eraYear; +}; + +// Result of calendar field resolution: ISO date + calendar ID. +struct ResolvedCalendarDate { + ISO8601::PlainDate isoDate; + String calendarId; +}; + +TemporalResult JS_EXPORT_PRIVATE dateFromFields(CalendarID, const CalendarFieldsIn&, TemporalOverflow); + +TemporalResult JS_EXPORT_PRIVATE yearMonthFromFields(CalendarID, const CalendarFieldsIn&, TemporalOverflow); + +TemporalResult JS_EXPORT_PRIVATE monthDayFromFields(CalendarID, const CalendarFieldsIn&, TemporalOverflow); + +TemporalResult JS_EXPORT_PRIVATE plainYearMonthWith(CalendarID, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow); + +TemporalResult JS_EXPORT_PRIVATE differenceYearMonth(CalendarID, const ISO8601::PlainDate& thisISODate, const ISO8601::PlainDate& otherISODate, TemporalUnit largestUnit); + +TemporalResult JS_EXPORT_PRIVATE plainYearMonthAdd(CalendarID, const ISO8601::PlainDate& currentISODate, const ISO8601::Duration&, TemporalOverflow); + +TemporalResult JS_EXPORT_PRIVATE plainYearMonthToPlainDate(CalendarID, const ISO8601::PlainDate& pymISODate, uint8_t day); + +TemporalResult JS_EXPORT_PRIVATE plainYearMonthFromISODate(CalendarID, const ISO8601::PlainDate& fullISODate); + +TemporalResult JS_EXPORT_PRIVATE plainMonthDayToPlainDate(CalendarID, const ISO8601::PlainDate& pmdISODate, int32_t year); + +TemporalResult JS_EXPORT_PRIVATE plainMonthDayFromISODate(CalendarID, const ISO8601::PlainDate& fullISODate, TemporalOverflow); + +} // namespace TemporalCore +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp index f2e6d0c9095b..27cc951be4d3 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp @@ -77,14 +77,15 @@ static std::unique_ptr> openCalendar(CalendarI return cal; } -// chineseCalendarUsesEpochOffset — probes ICU UCAL_EXTENDED_YEAR for Chinese to detect epoch-based vs ISO-proleptic year numbering. -// Result is cached: it depends only on the ICU version, which is fixed for the process lifetime. -bool chineseCalendarUsesEpochOffset() +// chineseCalendarExtendedYearFor1972 — probes ICU UCAL_EXTENDED_YEAR for the Chinese calendar +// at ISO 1972-02-15. Returns 1972 on ISO-proleptic ICU; epoch-based year on older Apple ICU. +// Result is cached: ICU version is fixed for the process lifetime. +int32_t chineseCalendarExtendedYearFor1972() { - static bool cached = []() -> bool { + static int32_t cached = []() -> int32_t { auto cal = openCalendar(chineseCalendarID()); if (!cal) - return false; + return 1972; // fallback: assume ISO-proleptic // Use ucal_setMillis with a precomputed ISO epoch time — NOT ucal_setDateTime which // sets calendar-native fields (not ISO fields) on a non-Gregorian calendar. // ISO 1972-02-15 00:00:00 UTC = 66,960,000,000 ms from Unix epoch (1970-01-01). @@ -93,9 +94,10 @@ bool chineseCalendarUsesEpochOffset() ucal_setMillis(cal.get(), iso1972Feb15EpochMs, &status); int32_t extYear = ucal_get(cal.get(), UCAL_EXTENDED_YEAR, &status); if (U_FAILURE(status)) - return false; - // Epoch-based: extYear = 4608 (Chinese year for 1972 CE); ISO-proleptic: extYear = 1972. - return extYear > 3000; + return 1972; // fallback: assume ISO-proleptic + // Return the raw EXTENDED_YEAR: 1972 on ISO-proleptic ICU, + // epoch-based year (whatever the current ICU uses) on older Apple ICU. + return extYear; }(); return cached; } diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h index 44a2c0094fc7..ca507c38e611 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h @@ -124,7 +124,7 @@ static constexpr int32_t ecmaRefYearNotInCalendar = INT32_MIN + 1; int32_t ecmaReferenceYear(CalendarID, uint8_t monthNumber, bool isLeapMonth, uint8_t day); -bool JS_EXPORT_PRIVATE chineseCalendarUsesEpochOffset(); +int32_t JS_EXPORT_PRIVATE chineseCalendarExtendedYearFor1972(); TemporalResult JS_EXPORT_PRIVATE calendarDateFromFields(CalendarID, int32_t year, uint8_t month, uint8_t day, std::optional era, std::optional eraYear, std::optional, TemporalOverflow); diff --git a/Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.cpp b/Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.cpp new file mode 100644 index 000000000000..b049cb662c83 --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.cpp @@ -0,0 +1,1030 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "DurationArithmetic.h" + +#include "CalendarICUBridge.h" +#include "DateConstructor.h" +#include "FractionToDouble.h" +#include "ISO8601.h" +#include "ISOArithmetic.h" +#include "Rounding.h" +#include "TemporalObject.h" +#include "TimeZoneICUBridge.h" +#include +#include + +namespace JSC { +namespace TemporalCore { + +// durationSign — temporal_rs: Duration::sign() +int durationSign(const ISO8601::Duration& d) +{ +#define JSC_CHECK_SIGN(field) \ + if (d.field() < 0) \ + return -1; \ + if (d.field() > 0) \ + return 1; + JSC_CHECK_SIGN(years) + JSC_CHECK_SIGN(months) + JSC_CHECK_SIGN(weeks) + JSC_CHECK_SIGN(days) + JSC_CHECK_SIGN(hours) + JSC_CHECK_SIGN(minutes) + JSC_CHECK_SIGN(seconds) + JSC_CHECK_SIGN(milliseconds) + JSC_CHECK_SIGN(microseconds) + JSC_CHECK_SIGN(nanoseconds) +#undef JSC_CHECK_SIGN + return 0; +} + +// negateDuration — temporal_rs: Duration::negated() +ISO8601::Duration negateDuration(const ISO8601::Duration& d) { return -d; } + +// absDuration — temporal_rs: Duration::abs() +ISO8601::Duration absDuration(const ISO8601::Duration& d) +{ + return ISO8601::Duration( + std::abs(d.years()), + std::abs(d.months()), + std::abs(d.weeks()), + std::abs(d.days()), + std::abs(d.hours()), + std::abs(d.minutes()), + std::abs(d.seconds()), + std::abs(d.milliseconds()), + absInt128(d.microseconds()), + absInt128(d.nanoseconds())); +} + +// largestSubduration — temporal_rs: Duration::default_largest_unit +TemporalUnit NODELETE largestSubduration(const ISO8601::Duration& d) +{ + uint8_t index = 0; + while (index < numberOfTemporalUnits - 1 && !d[index]) + index++; + return static_cast(index); +} + +// totalSeconds — internal BalanceDuration helper; folds days/hours/minutes into total seconds for redistribution. +int64_t totalSeconds(const ISO8601::Duration& d) +{ + constexpr int64_t hourPerDay = 24; + constexpr int64_t minPerHour = 60; + constexpr int64_t secPerMin = 60; + int64_t hours = hourPerDay * d.days() + d.hours(); + int64_t minutes = minPerHour * hours + d.minutes(); + return secPerMin * minutes + d.seconds(); +} + +// totalSubseconds — internal BalanceDuration helper; returns total sub-second nanoseconds (ms×1e6 + µs×1e3 + ns). +Int128 totalSubseconds(const ISO8601::Duration& d) +{ + constexpr int64_t usPerMs = 1000; + constexpr int64_t nsPerUs = 1000; + Int128 microseconds = Int128(usPerMs) * d.milliseconds() + d.microseconds(); + return Int128(nsPerUs) * microseconds + d.nanoseconds(); +} + +// balanceDuration — temporal_rs: balance is performed inline in Duration::compare. +// Redistributes time fields up to largestUnit. Returns std::nullopt (always). +std::optional balanceDuration(ISO8601::Duration& duration, TemporalUnit largestUnit) +{ + constexpr int64_t nsPerUs = 1000; + constexpr int64_t usPerMs = 1000; + constexpr int64_t msPerSec = 1000; + constexpr int64_t secPerMin = 60; + constexpr int64_t minPerHour = 60; + Int128 nanoseconds = totalSubseconds(duration); + int64_t seconds = totalSeconds(duration); + duration.clear(); + constexpr int64_t secondsPerDay = 86400; + if (largestUnit <= TemporalUnit::Day) { + duration.setDays(seconds / secondsPerDay); + seconds = seconds % secondsPerDay; + } + Int128 microseconds = nanoseconds / nsPerUs; + Int128 milliseconds = microseconds / Int128(usPerMs); + // Fold ms overflow into seconds: ms >= 1000 must carry into seconds. + int64_t secondsFromMs = static_cast(milliseconds / Int128(msPerSec)); + int64_t totalSecs = seconds + secondsFromMs; + int64_t minutes = totalSecs / secPerMin; + if (largestUnit <= TemporalUnit::Hour) { + duration.setNanoseconds(nanoseconds % nsPerUs); + duration.setMicroseconds(microseconds % Int128(usPerMs)); + duration.setMilliseconds(static_cast(milliseconds % Int128(msPerSec))); + duration.setSeconds(totalSecs % secPerMin); + duration.setMinutes(minutes % minPerHour); + duration.setHours(minutes / minPerHour); + } else if (largestUnit == TemporalUnit::Minute) { + duration.setNanoseconds(nanoseconds % nsPerUs); + duration.setMicroseconds(microseconds % Int128(usPerMs)); + duration.setMilliseconds(static_cast(milliseconds % Int128(msPerSec))); + duration.setSeconds(totalSecs % secPerMin); + duration.setMinutes(minutes); + } else if (largestUnit == TemporalUnit::Second) { + duration.setNanoseconds(nanoseconds % nsPerUs); + duration.setMicroseconds(microseconds % Int128(usPerMs)); + duration.setMilliseconds(static_cast(milliseconds % Int128(msPerSec))); + duration.setSeconds(totalSecs); + } else if (largestUnit == TemporalUnit::Millisecond) { + duration.setNanoseconds(nanoseconds % nsPerUs); + duration.setMicroseconds(microseconds % Int128(usPerMs)); + duration.setMilliseconds(static_cast(milliseconds) + seconds * msPerSec); + } else if (largestUnit == TemporalUnit::Microsecond) { + duration.setNanoseconds(nanoseconds % nsPerUs); + duration.setMicroseconds(microseconds + Int128(seconds) * (ISO8601::ExactTime::nsPerSecond / ISO8601::ExactTime::nsPerMicrosecond)); + } else + duration.setNanoseconds(nanoseconds + Int128(seconds) * ISO8601::ExactTime::nsPerSecond); + return std::nullopt; +} + +// timeDurationFromComponents — temporal_rs: TimeDuration::from_components +// Converts duration time fields to a total nanosecond Int128. +Int128 timeDurationFromComponents(double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds) +{ + constexpr int64_t minPerHour = 60; + constexpr int64_t secPerMin = 60; + constexpr int64_t msPerSec = 1000; + constexpr int64_t usPerMs = 1000; + constexpr int64_t nsPerUs = 1000; + CheckedInt128 min = checkedCastDoubleToInt128(minutes) + checkedCastDoubleToInt128(hours) * Int128(minPerHour); + CheckedInt128 sec = checkedCastDoubleToInt128(seconds) + min * Int128(secPerMin); + CheckedInt128 millis = checkedCastDoubleToInt128(milliseconds) + sec * Int128(msPerSec); + CheckedInt128 micros = checkedCastDoubleToInt128(microseconds) + millis * Int128(usPerMs); + CheckedInt128 nanos = checkedCastDoubleToInt128(nanoseconds) + micros * Int128(nsPerUs); + ASSERT(!nanos.hasOverflowed()); + ASSERT(absInt128(nanos) <= ISO8601::InternalDuration::maxTimeDuration); + return nanos; +} + +// splitTimeDuration — internal; splits a timeDuration (ns) into (overflowDays, subdayNs) using floor division. +std::pair splitTimeDuration(Int128 timeDuration) +{ + constexpr Int128 nsPerDay = ISO8601::ExactTime::nsPerDay; + int64_t overflowDays = static_cast(timeDuration / nsPerDay); + Int128 remainder = timeDuration % nsPerDay; + if (remainder < 0) { + remainder += nsPerDay; + overflowDays--; + } + return { overflowDays, remainder }; +} + +// plainTimeFromSubdayNs — internal; decomposes sub-day nanoseconds into a PlainTime. ns must be in [0, nsPerDay). +ISO8601::PlainTime plainTimeFromSubdayNs(Int128 ns) +{ + ASSERT(ns >= 0 && ns < ISO8601::ExactTime::nsPerDay); + int32_t nanosecond = static_cast(ns % 1000); + Int128 remaining = ns / 1000; + int32_t microsecond = static_cast(remaining % 1000); + remaining /= 1000; + int32_t millisecond = static_cast(remaining % 1000); + remaining /= 1000; + int32_t second = static_cast(remaining % 60); + remaining /= 60; + int32_t minute = static_cast(remaining % 60); + int32_t hour = static_cast(remaining / 60); + return ISO8601::PlainTime(hour, minute, second, millisecond, microsecond, nanosecond); +} + +// totalTimeDuration — temporal_rs: TimeDuration::total (src/builtins/core/duration/normalized.rs) +// Returns the total time portion of a duration as a fractional count of the given unit. +double totalTimeDuration(Int128 timeDuration, TemporalUnit unit) +{ + double divisor = static_cast(lengthInNanoseconds(unit)); + ASSERT(isSafeInteger(divisor)); + return fractionToDouble(timeDuration, divisor); +} + +// toInternalDuration — temporal_rs: Duration::to_internal_duration_record (src/builtins/core/duration.rs) +// Builds an InternalDuration from a Duration (time fields only, no day normalization). +ISO8601::InternalDuration toInternalDuration(ISO8601::Duration d) +{ + auto timeDuration = timeDurationFromComponents(d.hours(), d.minutes(), d.seconds(), + d.milliseconds(), static_cast(d.microseconds()), static_cast(d.nanoseconds())); + return ISO8601::InternalDuration::combineDateAndTimeDuration(d, timeDuration); +} + +// temporalDurationFromInternal — temporal_rs: Duration::from_internal (src/builtins/core/duration.rs) +// Converts an InternalDuration back to a Duration given largestUnit. +ISO8601::Duration temporalDurationFromInternal(ISO8601::InternalDuration internalDuration, TemporalUnit largestUnit) +{ + int64_t days = 0; + int64_t hours = 0; + int64_t minutes = 0; + int64_t seconds = 0; + Int128 milliseconds = 0; + Int128 microseconds = 0; + + int32_t sign = internalDuration.timeDurationSign(); + Int128 nanoseconds = absInt128(internalDuration.time()); + + if (largestUnit <= TemporalUnit::Day) { + microseconds = nanoseconds / 1000; + nanoseconds = nanoseconds % 1000; + milliseconds = microseconds / 1000; + microseconds = microseconds % 1000; + seconds = static_cast(milliseconds / 1000); + milliseconds = milliseconds % 1000; + minutes = seconds / 60; + seconds = seconds % 60; + hours = minutes / 60; + minutes = minutes % 60; + days = hours / 24; + hours = hours % 24; + } else if (largestUnit == TemporalUnit::Hour) { + microseconds = nanoseconds / 1000; + nanoseconds = nanoseconds % 1000; + milliseconds = microseconds / 1000; + microseconds = microseconds % 1000; + seconds = static_cast(milliseconds / 1000); + milliseconds = milliseconds % 1000; + minutes = seconds / 60; + seconds = seconds % 60; + hours = minutes / 60; + minutes = minutes % 60; + } else if (largestUnit == TemporalUnit::Minute) { + microseconds = nanoseconds / 1000; + nanoseconds = nanoseconds % 1000; + milliseconds = microseconds / 1000; + microseconds = microseconds % 1000; + seconds = static_cast(milliseconds / 1000); + milliseconds = milliseconds % 1000; + minutes = seconds / 60; + seconds = seconds % 60; + } else if (largestUnit == TemporalUnit::Second) { + microseconds = nanoseconds / 1000; + nanoseconds = nanoseconds % 1000; + milliseconds = microseconds / 1000; + microseconds = microseconds % 1000; + seconds = static_cast(milliseconds / 1000); + milliseconds = milliseconds % 1000; + } else if (largestUnit == TemporalUnit::Millisecond) { + microseconds = nanoseconds / 1000; + nanoseconds = nanoseconds % 1000; + milliseconds = microseconds / 1000; + microseconds = microseconds % 1000; + } else if (largestUnit == TemporalUnit::Microsecond) { + microseconds = nanoseconds / 1000; + nanoseconds = nanoseconds % 1000; + } + + if (hours) + hours *= sign; + if (minutes) + minutes *= sign; + if (seconds) + seconds *= sign; + if (milliseconds) + milliseconds *= sign; + if (microseconds) + microseconds *= sign; + if (nanoseconds) + nanoseconds *= sign; + // Apply ℝ(𝔽(x)) per spec CreateTemporalDuration step 12: round through float64 so that + // Int128 values not exactly representable round past nanosecondsLimit → isValidDuration rejects. + // milliseconds uses doubleToInt64Saturating because Int128 → double can still exceed int64_t + // (e.g. largestUnit=millisecond, out-of-range input: ms ≈ 9e21 > INT64_MAX ≈ 9.2e18). + return ISO8601::Duration { internalDuration.dateDuration().years(), + internalDuration.dateDuration().months(), + internalDuration.dateDuration().weeks(), + static_cast(internalDuration.dateDuration().days() + days * sign), + static_cast(hours), static_cast(minutes), static_cast(seconds), + ISO8601::Duration::doubleToInt64Saturating(static_cast(milliseconds)), + Int128(static_cast(microseconds)), + Int128(static_cast(nanoseconds)) }; +} + +// add24HourDaysToTimeDuration — temporal_rs: TimeDuration::add_days (src/builtins/core/duration/normalized.rs) +// Adds 24-hour days to a timeDuration (ns). Returns error if result exceeds maxTimeDuration. +TemporalResult add24HourDaysToTimeDuration(Int128 d, double days) +{ + CheckedInt128 daysInNanoseconds = checkedCastDoubleToInt128(days) * ISO8601::ExactTime::nsPerDay; + CheckedInt128 result = d + daysInNanoseconds; + ASSERT(!result.hasOverflowed()); + if (absInt128(result) > ISO8601::InternalDuration::maxTimeDuration) + return makeUnexpected(TemporalError { TemporalErrorKind::RangeError, "Total time in duration is out of range"_s }); + return Int128(result); +} + +// toInternalDurationRecordWith24HourDays — temporal_rs: InternalDurationRecord::from_duration_with_24_hour_days (src/builtins/core/duration/normalized.rs) +// Folds duration.days into timeDuration and returns InternalDuration with date-only datePart. +TemporalResult toInternalDurationRecordWith24HourDays(ISO8601::Duration d) +{ + Int128 timeDuration = timeDurationFromComponents(d.hours(), d.minutes(), d.seconds(), + d.milliseconds(), static_cast(d.microseconds()), static_cast(d.nanoseconds())); + auto result = add24HourDaysToTimeDuration(timeDuration, d.days()); + if (!result) + return makeUnexpected(result.error()); + ISO8601::Duration dateDuration = ISO8601::Duration { + d.years(), d.months(), + d.weeks(), 0, 0, 0, 0, 0, Int128(0), Int128(0) + }; + return ISO8601::InternalDuration::combineDateAndTimeDuration(dateDuration, *result); +} + +// toDateDurationRecordWithoutTime — temporal_rs: InternalDurationRecord::to_date_duration_record_without_time (src/builtins/core/duration/normalized.rs) +// Strips time fields and folds days into the date part. Returns a date-only Duration. +TemporalResult toDateDurationRecordWithoutTime(ISO8601::Duration duration) +{ + auto internalDuration = toInternalDurationRecordWith24HourDays(duration); + if (!internalDuration) + return makeUnexpected(internalDuration.error()); + auto days = internalDuration->time() / ISO8601::ExactTime::nsPerDay; + return ISO8601::Duration { + internalDuration->dateDuration().years(), + internalDuration->dateDuration().months(), + internalDuration->dateDuration().weeks(), + static_cast(days), 0, 0, 0, 0, Int128(0), Int128(0) + }; +} + +// getUTCEpochNanoseconds — temporal_rs: IsoDateTime::as_nanoseconds (UTC path) +// Converts (PlainDate, PlainTime) to epoch nanoseconds as if the datetime were UTC. +Int128 getUTCEpochNanoseconds(ISO8601::PlainDate date, ISO8601::PlainTime time) +{ + auto dayMs = makeDay(date.year(), date.month() - 1, date.day()); + auto timeMs = makeTime(time.hour(), time.minute(), time.second(), time.millisecond()); + auto ms = makeDate(dayMs, timeMs); + ASSERT(isInteger(ms)); + return Int128(ms) * ISO8601::ExactTime::nsPerMillisecond + + Int128(time.microsecond()) * ISO8601::ExactTime::nsPerMicrosecond + + Int128(time.nanosecond()); +} + +constexpr int32_t unitIndexInTable(TemporalUnit unit) +{ + switch (unit) { + case TemporalUnit::Year: + return 0; + case TemporalUnit::Month: + return 1; + case TemporalUnit::Week: + return 2; + case TemporalUnit::Day: + return 3; + case TemporalUnit::Hour: + return 4; + case TemporalUnit::Minute: + return 5; + case TemporalUnit::Second: + return 6; + case TemporalUnit::Millisecond: + return 7; + case TemporalUnit::Microsecond: + return 8; + case TemporalUnit::Nanosecond: + return 9; + default: + RELEASE_ASSERT_NOT_REACHED(); + } +} + +constexpr TemporalUnit unitInTable(int32_t i) +{ + switch (i) { + case 0: + return TemporalUnit::Year; + case 1: + return TemporalUnit::Month; + case 2: + return TemporalUnit::Week; + case 3: + return TemporalUnit::Day; + case 4: + return TemporalUnit::Hour; + case 5: + return TemporalUnit::Minute; + case 6: + return TemporalUnit::Second; + case 7: + return TemporalUnit::Millisecond; + case 8: + return TemporalUnit::Microsecond; + case 9: + return TemporalUnit::Nanosecond; + default: + RELEASE_ASSERT_NOT_REACHED(); + } +} + +// epochNanosecondsForDateAndTime — internal; computes epoch nanoseconds for (date, time): UTC if timeZone==nullptr, TZ-aware otherwise. +static TemporalResult epochNanosecondsForDateAndTime( + const ISO8601::PlainDate& date, const ISO8601::PlainTime& time, + const TimeZone* timeZone) +{ + if (!timeZone) + return TemporalCore::getUTCEpochNanoseconds(date, time); + auto result = TemporalCore::getEpochNanosecondsFor(*timeZone, date, time, TemporalDisambiguation::Compatible); + if (!result) + return makeUnexpected(result.error()); + return result->epochNanoseconds(); +} + +// adjustDateDurationRecord — internal helper; builds a new date duration with overridden days/weeks/months fields. +// https://tc39.es/proposal-temporal/#sec-temporal-adjustdatedurationrecord +TemporalResult adjustDateDurationRecord(const ISO8601::Duration& dateDuration, double days, std::optional weeks, std::optional months) +{ + // Step 1: If weeks is not present, set weeks to dateDuration.[[Weeks]]. + // Step 2: If months is not present, set months to dateDuration.[[Months]]. + // (Both handled implicitly by the std::optional parameters.) + auto result = ISO8601::Duration { + dateDuration.years(), + months ? static_cast(months.value()) : dateDuration.months(), + weeks ? static_cast(weeks.value()) : dateDuration.weeks(), + // Step 3: Return ? CreateDateDurationRecord(dateDuration.[[Years]], months, weeks, days). + static_cast(days), 0, 0, 0, 0, Int128(0), Int128(0) + }; + if (!ISO8601::isValidDuration(result)) + return makeUnexpected(rangeError("Temporal.Duration properties must be valid and of consistent sign"_s)); + return result; +} + +// NudgeWindow — internal; holds the floor/ceiling epoch-ns bounds and durations for a single calendar nudge step. +struct NudgeWindow { + double r1; + double r2; + Int128 startEpochNs; + Int128 endEpochNs; + ISO8601::Duration startDuration; + ISO8601::Duration endDuration; +}; + +// computeNudgeWindow — temporal_rs: InternalDurationRecord::compute_nudge_window (src/builtins/core/duration/normalized.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-computenudgewindow +static TemporalResult> computeNudgeWindow( + int32_t sign, + const ISO8601::InternalDuration& duration, Int128 originEpochNs, + ISO8601::PlainDate isoDate, ISO8601::PlainTime isoTime, + double increment, TemporalUnit unit, bool additionalShift, + const TimeZone* timeZone = nullptr, CalendarID calendarId = iso8601CalendarID()) +{ + double r1 = 0, r2 = 0; + ISO8601::Duration startDuration, endDuration; + // Steps 1-4: compute r1, r2, startDuration, endDuration based on unit. + switch (unit) { + case TemporalUnit::Year: { + // Step 1.a: years = RoundNumberToIncrement(duration.[[Date]].[[Years]], increment, trunc). + Int128 years = roundNumberToIncrementInt128((Int128)duration.dateDuration().years(), (Int128)increment, RoundingMode::Trunc); + // Step 1.b-c: r1 = years; or if additionalShift, r1 = years + increment × sign. + r1 = additionalShift ? (double)years + increment * sign : (double)years; + // Step 1.d: r2 = r1 + increment × sign. + r2 = r1 + increment * sign; + // Step 1.e: startDuration = CreateDateDurationRecord(r1, 0, 0, 0). + startDuration = ISO8601::Duration { static_cast(r1), 0, 0, 0, 0, 0, 0, 0, Int128(0), Int128(0) }; + // Step 1.f: endDuration = CreateDateDurationRecord(r2, 0, 0, 0). + endDuration = ISO8601::Duration { static_cast(r2), 0, 0, 0, 0, 0, 0, 0, Int128(0), Int128(0) }; + break; + } + case TemporalUnit::Month: { + // Step 2.a: months = RoundNumberToIncrement(duration.[[Date]].[[Months]], increment, trunc). + Int128 months = roundNumberToIncrementInt128((Int128)duration.dateDuration().months(), (Int128)increment, RoundingMode::Trunc); + // Step 2.b-c: r1 = months; or if additionalShift, r1 = months + increment × sign. + r1 = additionalShift ? (double)months + increment * sign : (double)months; + // Step 2.d: r2 = r1 + increment × sign. + r2 = r1 + increment * sign; + // Step 2.e: startDuration = AdjustDateDurationRecord(duration.[[Date]], 0, 0, r1). + auto sd = adjustDateDurationRecord(duration.dateDuration(), 0, 0, r1); + if (!sd) + return makeUnexpected(sd.error()); + startDuration = *sd; + // Step 2.f: endDuration = AdjustDateDurationRecord(duration.[[Date]], 0, 0, r2). + auto ed = adjustDateDurationRecord(duration.dateDuration(), 0, 0, r2); + if (!ed) + return makeUnexpected(ed.error()); + endDuration = *ed; + break; + } + case TemporalUnit::Week: { + // Step 3.a: yearsMonths = AdjustDateDurationRecord(duration.[[Date]], 0, 0). + auto yearsMonths = adjustDateDurationRecord(duration.dateDuration(), 0, 0, std::nullopt); + if (!yearsMonths) + return makeUnexpected(yearsMonths.error()); + // Step 3.b: weeksStart = CalendarDateAdd(calendar, isoDateTime.[[ISODate]], yearsMonths, constrain). + auto weeksStartResult = TemporalCore::calendarDateAdd(calendarId, isoDate, *yearsMonths, TemporalOverflow::Constrain); + if (!weeksStartResult) + return makeUnexpected(weeksStartResult.error()); + auto weeksStart = *weeksStartResult; + // Step 3.c: weeksEnd = AddDaysToISODate(weeksStart, duration.[[Date]].[[Days]]). + auto weeksEnd = TemporalCore::balanceISODate(weeksStart.year(), static_cast(weeksStart.month()), static_cast(weeksStart.day()) + duration.dateDuration().days()); + // Step 3.d: untilResult = CalendarDateUntil(calendar, weeksStart, weeksEnd, week). + auto untilResult = TemporalCore::calendarDateUntil(calendarId, weeksStart, weeksEnd, TemporalUnit::Week); + if (!untilResult) + return makeUnexpected(untilResult.error()); + // Step 3.e: weeks = RoundNumberToIncrement(duration.[[Date]].[[Weeks]] + untilResult.[[Weeks]], increment, trunc). + Int128 weeks = roundNumberToIncrementInt128((Int128)(duration.dateDuration().weeks() + untilResult->weeks()), (Int128)increment, RoundingMode::Trunc); + // Step 3.f: r1 = weeks. Step 3.g: r2 = weeks + increment × sign. + r1 = (double)weeks; + r2 = (double)weeks + increment * sign; + // Step 3.h: startDuration = AdjustDateDurationRecord(duration.[[Date]], 0, r1). + auto sd = adjustDateDurationRecord(duration.dateDuration(), 0, r1, std::nullopt); + if (!sd) + return makeUnexpected(sd.error()); + startDuration = *sd; + // Step 3.i: endDuration = AdjustDateDurationRecord(duration.[[Date]], 0, r2). + auto ed = adjustDateDurationRecord(duration.dateDuration(), 0, r2, std::nullopt); + if (!ed) + return makeUnexpected(ed.error()); + endDuration = *ed; + break; + } + default: { + // Step 4.a: Assert unit is day. + ASSERT(unit == TemporalUnit::Day); + // Step 4.b: days = RoundNumberToIncrement(duration.[[Date]].[[Days]], increment, trunc). + Int128 days = roundNumberToIncrementInt128((Int128)duration.dateDuration().days(), (Int128)increment, RoundingMode::Trunc); + // Step 4.c: r1 = days. Step 4.d: r2 = days + increment × sign. + r1 = (double)days; + r2 = (double)days + increment * sign; + // Step 4.e: startDuration = AdjustDateDurationRecord(duration.[[Date]], r1). + auto sd = adjustDateDurationRecord(duration.dateDuration(), r1, std::nullopt, std::nullopt); + if (!sd) + return makeUnexpected(sd.error()); + startDuration = *sd; + // Step 4.f: endDuration = AdjustDateDurationRecord(duration.[[Date]], r2). + auto ed = adjustDateDurationRecord(duration.dateDuration(), r2, std::nullopt, std::nullopt); + if (!ed) + return makeUnexpected(ed.error()); + endDuration = *ed; + break; + } + } + + // Step 5: Assert if sign=1, r1≥0 and r1= 0 && r1 < r2)); + // Step 6: Assert if sign=-1, r1≤0 and r1>r2. + ASSERT(sign != -1 || (r1 <= 0 && r1 > r2)); + + // Step 7: If r1=0, startEpochNs = originEpochNs. + // Step 8: Else, start = CalendarDateAdd (8.a); then steps 8.b (CombineISODateAndTimeRecord) + + // 8.c/8.d (GetUTCEpochNanoseconds or GetEpochNanosecondsFor) fused into epochNanosecondsForDateAndTime. + Int128 startEpochNs; + if (!r1) + startEpochNs = originEpochNs; + else { + auto startResult = TemporalCore::calendarDateAdd(calendarId, isoDate, startDuration, TemporalOverflow::Constrain); + if (!startResult) + return makeUnexpected(startResult.error()); + auto start = *startResult; + double startDayCount = dateToDaysFrom1970(start.year(), static_cast(start.month()) - 1, static_cast(start.day())); + if (std::abs(startDayCount) > 1e8) + return makeUnexpected(rangeError("date is outside the representable range"_s)); + auto startNsResult = epochNanosecondsForDateAndTime(start, isoTime, timeZone); + if (!startNsResult) + return makeUnexpected(startNsResult.error()); + startEpochNs = *startNsResult; + } + // Step 9: end = CalendarDateAdd(calendar, isoDateTime.[[ISODate]], endDuration, constrain). + auto endResult = TemporalCore::calendarDateAdd(calendarId, isoDate, endDuration, TemporalOverflow::Constrain); + if (!endResult) + return makeUnexpected(endResult.error()); + auto end = *endResult; + double endDayCount = dateToDaysFrom1970(end.year(), static_cast(end.month()) - 1, static_cast(end.day())); + if (std::abs(endDayCount) > 1e8) + return makeUnexpected(rangeError("date is outside the representable range"_s)); + // Steps 10-12: CombineISODateAndTimeRecord (step 10) + GetUTCEpochNanoseconds/GetEpochNanosecondsFor + // (steps 11/12) fused into epochNanosecondsForDateAndTime — avoids an intermediate endDateTime record. + auto endNsResult = epochNanosecondsForDateAndTime(end, isoTime, timeZone); + if (!endNsResult) + return makeUnexpected(endNsResult.error()); + Int128 endEpochNs = *endNsResult; + // Steps 13-14: startDuration/endDuration = CombineDateAndTimeDuration(dateDuration, 0). + // Deferred to caller (nudgeToCalendarUnit) for efficiency — only the selected path needs the combine. + // Step 15: Return the Record. + return std::optional(NudgeWindow { r1, r2, startEpochNs, endEpochNs, startDuration, endDuration }); +} + +// nudgeToCalendarUnit — temporal_rs: InternalDurationRecord::nudge_calendar_unit (internal step of round_relative_duration) +// https://tc39.es/proposal-temporal/#sec-temporal-nudgetocalendarunit +TemporalResult nudgeToCalendarUnit(int32_t sign, + const ISO8601::InternalDuration& duration, Int128 originEpochNs, Int128 destEpochNs, + ISO8601::PlainDate isoDate, ISO8601::PlainTime isoTime, double increment, + TemporalUnit unit, RoundingMode roundingMode, const TimeZone* timeZone, CalendarID calendarId) +{ + // Step 1: Let didExpandCalendarUnit be false. + // Step 2: Let nudgeWindow be ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, false). + auto nudgeWindowResult = computeNudgeWindow(sign, duration, originEpochNs, isoDate, isoTime, increment, unit, false, timeZone, calendarId); + if (!nudgeWindowResult) + return makeUnexpected(nudgeWindowResult.error()); + ASSERT(nudgeWindowResult->has_value()); + auto nudgeWindow = **nudgeWindowResult; + + // Step 3-4: Let startEpochNs/endEpochNs be nudgeWindow.[[StartEpochNs]]/[[EndEpochNs]]. + bool didExpandCalendarUnit = false; + // Step 5: If sign = 1, then / Step 6: Else, — check destEpochNs is in bounds; if not, retry with additionalShift=true. + bool inBounds = (sign == 1) + ? (nudgeWindow.startEpochNs <= destEpochNs && destEpochNs <= nudgeWindow.endEpochNs) + : (nudgeWindow.endEpochNs <= destEpochNs && destEpochNs <= nudgeWindow.startEpochNs); + if (!inBounds) { + // Step 5.a / 6.a: Set nudgeWindow to ? ComputeNudgeWindow(..., true). + auto retried = computeNudgeWindow(sign, duration, originEpochNs, isoDate, isoTime, increment, unit, true, timeZone, calendarId); + if (!retried) + return makeUnexpected(retried.error()); + ASSERT(retried->has_value()); + nudgeWindow = **retried; + // Step 5.a.ii / 6.a.ii: Assert bounds hold after retry. Set didExpandCalendarUnit to true. + ASSERT(sign != 1 || (nudgeWindow.startEpochNs <= destEpochNs && destEpochNs <= nudgeWindow.endEpochNs)); + ASSERT(sign != -1 || (nudgeWindow.endEpochNs <= destEpochNs && destEpochNs <= nudgeWindow.startEpochNs)); + didExpandCalendarUnit = true; + } + + // Steps 7-12: Extract r1, r2, startEpochNs, endEpochNs, startDuration, endDuration from nudgeWindow. + auto& startDuration = nudgeWindow.startDuration; + auto& endDuration = nudgeWindow.endDuration; + // Step 13: Assert: startEpochNs ≠ endEpochNs. + ASSERT(nudgeWindow.startEpochNs != nudgeWindow.endEpochNs); + // Step 14: Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs). + Int128 progressNumerator = destEpochNs - nudgeWindow.startEpochNs; + Int128 progressDenominator = nudgeWindow.endEpochNs - nudgeWindow.startEpochNs; + // Step 15: Let total be r1 + progress × increment × sign. + // (NOTE: computed via integer arithmetic before the final float division per spec note.) + Int128 totalNumerator = Int128(static_cast(nudgeWindow.r1)) * progressDenominator + progressNumerator * Int128(static_cast(increment)) * Int128(sign); + double total = fractionToDouble(totalNumerator, static_cast(absInt128(progressDenominator))) * (progressDenominator < 0 ? -1.0 : 1.0); + Int128 progress = progressNumerator / progressDenominator; + // Step 16: Assert: 0 ≤ progress ≤ 1. + ASSERT(0 <= progress && progress <= 1); + // Step 17: Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). + UnsignedRoundingMode unsignedRoundingMode = getUnsignedRoundingMode(roundingMode, sign < 0); + // Step 18-19: If progress = 1, roundedUnit = abs(r2); else apply unsigned rounding. + double roundedUnit = std::abs(nudgeWindow.r2); + if (progress != 1) { + ASSERT(std::abs(nudgeWindow.r1) <= std::abs(total) && std::abs(total) < std::abs(nudgeWindow.r2)); + roundedUnit = applyUnsignedRoundingMode(std::abs(total), std::abs(nudgeWindow.r1), std::abs(nudgeWindow.r2), unsignedRoundingMode); + } + // Step 20: If roundedUnit = abs(r2), set didExpandCalendarUnit to true and use endDuration/endEpochNs; else use start. + didExpandCalendarUnit |= (roundedUnit == std::abs(nudgeWindow.r2)); + ISO8601::Duration resultDuration = (roundedUnit == std::abs(nudgeWindow.r2)) ? endDuration : startDuration; + Int128 nudgedEpochNs = (roundedUnit == std::abs(nudgeWindow.r2)) ? nudgeWindow.endEpochNs : nudgeWindow.startEpochNs; + // Step 21: Let nudgeResult be Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandCalendarUnit }. + // (computeNudgeWindow steps 13-14 deferred here: CombineDateAndTimeDuration applied only to the selected path.) + auto resultDurationInternal = ISO8601::InternalDuration::combineDateAndTimeDuration(resultDuration, 0); + // Step 22: Return the Record { [[NudgeResult]]: nudgeResult, [[Total]]: total }. + return Nudged(NudgeResult(resultDurationInternal, nudgedEpochNs, didExpandCalendarUnit), total); +} + +// nudgeToZonedTime — temporal_rs: InternalDurationRecord::nudge_to_zoned_time (src/builtins/core/duration/normalized.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-nudgetozonedtime +TemporalResult nudgeToZonedTime(int32_t sign, + const ISO8601::InternalDuration& duration, ISO8601::PlainDate isoDate, + ISO8601::PlainTime isoTime, const TimeZone& timeZone, double increment, + TemporalUnit unit, RoundingMode roundingMode, CalendarID calendarId) +{ + // Step 1: Let start be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], duration.[[Date]], ~constrain~). + auto startResult = TemporalCore::calendarDateAdd(calendarId, isoDate, duration.dateDuration(), TemporalOverflow::Constrain); + if (!startResult) + return makeUnexpected(startResult.error()); + auto start = *startResult; + // Step 2: startDateTime = CombineISODateAndTimeRecord(start, isoDateTime.[[Time]]). + // Step 3: endDate = AddDaysToISODate(start, sign). + // Step 4: endDateTime = CombineISODateAndTimeRecord(endDate, isoDateTime.[[Time]]). + // (Steps 2/4 fused into epochNanosecondsForDateAndTime — no intermediate records.) + auto endDate = TemporalCore::balanceISODate(start.year(), static_cast(start.month()), static_cast(start.day()) + sign); + // Step 5: startEpochNs = GetEpochNanosecondsFor(timeZone, startDateTime, compatible). (steps 2+5 fused) + auto startNsResult = epochNanosecondsForDateAndTime(start, isoTime, &timeZone); + if (!startNsResult) + return makeUnexpected(startNsResult.error()); + Int128 startEpochNs = *startNsResult; + // Step 6: endEpochNs = GetEpochNanosecondsFor(timeZone, endDateTime, compatible). (steps 4+6 fused) + auto endNsResult = epochNanosecondsForDateAndTime(endDate, isoTime, &timeZone); + if (!endNsResult) + return makeUnexpected(endNsResult.error()); + Int128 endEpochNs = *endNsResult; + // Step 7: daySpan = TimeDurationFromEpochNanosecondsDifference(endEpochNs, startEpochNs). + Int128 daySpan = endEpochNs - startEpochNs; + // Step 8: Assert: TimeDurationSign(daySpan) = sign. + ASSERT((daySpan < 0 ? -1 : daySpan > 0 ? 1 : 0) == sign); + // Step 9: Let unitLength be the nanoseconds length of unit. + Int128 unitLength = lengthInNanoseconds(unit); + // Step 10: Let roundedTimeDuration be ? RoundTimeDurationToIncrement(duration.[[Time]], increment × unitLength, roundingMode). + Int128 roundedTimeDuration = roundNumberToIncrementInt128(duration.time(), unitLength * (Int128)std::trunc(increment), roundingMode); + // Step 11: Let beyondDaySpan be ! AddTimeDuration(roundedTimeDuration, -daySpan). + Int128 beyondDaySpan = roundedTimeDuration - daySpan; + int32_t beyondSign = beyondDaySpan < 0 ? -1 : beyondDaySpan > 0 ? 1 : 0; + // Step 12: If TimeDurationSign(beyondDaySpan) ≠ -sign (didRoundBeyondDay already set above), then + bool didRoundBeyondDay = (beyondSign != -sign); + int32_t dayDelta = 0; + Int128 nudgedEpochNs; + if (didRoundBeyondDay) { + // Step 12.b: dayDelta = sign. + dayDelta = sign; + // Step 12.c: Set roundedTimeDuration to ? RoundTimeDurationToIncrement(beyondDaySpan, ...). + roundedTimeDuration = roundNumberToIncrementInt128(beyondDaySpan, unitLength * (Int128)std::trunc(increment), roundingMode); + // Step 12.d: nudgedEpochNs = AddTimeDurationToEpochNanoseconds(roundedTimeDuration, endEpochNs). + nudgedEpochNs = roundedTimeDuration + endEpochNs; + } else { + // Step 13.b: dayDelta = 0. (already initialized) + // Step 13.c: nudgedEpochNs = AddTimeDurationToEpochNanoseconds(roundedTimeDuration, startEpochNs). + nudgedEpochNs = roundedTimeDuration + startEpochNs; + } + // Step 14: Let dateDuration be ! AdjustDateDurationRecord(duration.[[Date]], duration.[[Date]].[[Days]] + dayDelta). + auto dateDurationResult = adjustDateDurationRecord(duration.dateDuration(), duration.dateDuration().days() + dayDelta, std::nullopt, std::nullopt); + if (!dateDurationResult) + return makeUnexpected(dateDurationResult.error()); + // Step 15: Let resultDuration be CombineDateAndTimeDuration(dateDuration, roundedTimeDuration). + auto resultDuration = ISO8601::InternalDuration::combineDateAndTimeDuration(*dateDurationResult, roundedTimeDuration); + // Step 16: Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didRoundBeyondDay }. + return NudgeResult(resultDuration, nudgedEpochNs, didRoundBeyondDay); +} + +// nudgeToDayOrTime — temporal_rs: InternalDurationRecord::nudge_to_day_or_time (src/builtins/core/duration/normalized.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-nudgetodayortime +TemporalResult nudgeToDayOrTime(ISO8601::InternalDuration duration, + Int128 destEpochNs, TemporalUnit largestUnit, double increment, + TemporalUnit smallestUnit, RoundingMode roundingMode) +{ + // Step 1: Let timeDuration be ! Add24HourDaysToTimeDuration(duration.[[Time]], duration.[[Date]].[[Days]]). + auto timeDurationResult = add24HourDaysToTimeDuration(duration.time(), duration.dateDuration().days()); + if (!timeDurationResult) + return makeUnexpected(timeDurationResult.error()); + Int128 timeDuration = *timeDurationResult; + // Step 2: Let unitLength be the nanoseconds length of smallestUnit. + Int128 unitLength = lengthInNanoseconds(smallestUnit); + // Step 3: Let roundedTime be ? RoundTimeDurationToIncrement(timeDuration, unitLength × increment, roundingMode). + Int128 roundedTime = roundNumberToIncrementInt128(timeDuration, unitLength * (Int128)std::trunc(increment), roundingMode); + // Step 4: Let diffTime be ! AddTimeDuration(roundedTime, -timeDuration). + Int128 diffTime = roundedTime - timeDuration; + // Step 5: wholeDays = truncate(TotalTimeDuration(timeDuration, day)). + // (totalTimeDuration returns int64_t-truncated double, so truncate is implicit.) + double wholeDays = totalTimeDuration(timeDuration, TemporalUnit::Day); + // Step 6: roundedWholeDays = truncate(TotalTimeDuration(roundedTime, day)). (same) + double roundedWholeDays = totalTimeDuration(roundedTime, TemporalUnit::Day); + // Step 7: Let dayDelta be roundedWholeDays - wholeDays. + auto dayDelta = roundedWholeDays - wholeDays; + // Step 8: If dayDelta < 0, let dayDeltaSign be -1; else if dayDelta > 0, let dayDeltaSign be 1; else 0. + auto dayDeltaSign = dayDelta < 0 ? -1 : dayDelta > 0 ? 1 : 0; + // Step 9: If dayDeltaSign = TimeDurationSign(timeDuration), let didExpandDays be true; else false. + bool didExpandDays = dayDeltaSign == (timeDuration < 0 ? -1 : timeDuration > 0 ? 1 : 0); + // Step 10: Let nudgedEpochNs be AddTimeDurationToEpochNanoseconds(diffTime, destEpochNs). + auto nudgedEpochNs = diffTime + destEpochNs; + // Step 11: Let days be 0. + // Step 12: Let remainder be roundedTime. + auto days = 0; + auto remainder = roundedTime; + // Step 13: If TemporalUnitCategory(largestUnit) is ~date~, then + if (largestUnit <= TemporalUnit::Day) { + // Step 13.a: Set days to roundedWholeDays. + days = roundedWholeDays; + // Step 13.b: remainder = roundedTime - roundedWholeDays×hoursPerDay (days==roundedWholeDays here). + remainder = roundedTime + timeDurationFromComponents(-days * WTF::hoursPerDay, 0, 0, 0, 0, 0); + } + // Step 14: Let dateDuration be ! AdjustDateDurationRecord(duration.[[Date]], days). + auto dateDurationResult = adjustDateDurationRecord(duration.dateDuration(), days, std::nullopt, std::nullopt); + if (!dateDurationResult) + return makeUnexpected(dateDurationResult.error()); + // Step 15: Let resultDuration be CombineDateAndTimeDuration(dateDuration, remainder). + auto resultDuration = ISO8601::InternalDuration::combineDateAndTimeDuration(*dateDurationResult, remainder); + // Step 16: Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandDays }. + return NudgeResult(resultDuration, nudgedEpochNs, didExpandDays); +} + +// bubbleRelativeDuration — temporal_rs: InternalDurationRecord::bubble_relative_duration (src/builtins/core/duration/normalized.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-bubblerelativeduration +TemporalResult bubbleRelativeDuration( + int32_t sign, ISO8601::InternalDuration duration, Int128 nudgedEpochNs, + ISO8601::PlainDate isoDate, ISO8601::PlainTime isoTime, + TemporalUnit largestUnit, TemporalUnit smallestUnit, + const TimeZone* timeZone, CalendarID calendarId) +{ + // Step 1: If smallestUnit is largestUnit, return duration. + if (smallestUnit == largestUnit) + return duration; + // Step 2: Let largestUnitIndex be the ordinal index of largestUnit in the units table. + auto largestUnitIndex = unitIndexInTable(largestUnit); + // Step 3: Let smallestUnitIndex be the ordinal index of smallestUnit in the units table. + auto smallestUnitIndex = unitIndexInTable(smallestUnit); + // Step 4: Let unitIndex be smallestUnitIndex - 1. + auto unitIndex = smallestUnitIndex - 1; + // Step 5: Let done be false. + bool done = false; + ISO8601::Duration endDuration; + // Step 6: Repeat, while unitIndex ≥ largestUnitIndex and done is false, + while (unitIndex >= largestUnitIndex && !done) { + // Step 6.a: Let unit be the unit at ordinal unitIndex in the units table. + auto unit = unitInTable(unitIndex); + // Step 6.b: If unit is not ~week~, or largestUnit is ~week~, then + if (unit != TemporalUnit::Week || largestUnit == TemporalUnit::Week) { + // Step 6.b.i: If unit is ~year~, let endDuration be ? CreateDateDurationRecord(years + sign, 0, 0, 0). + if (unit == TemporalUnit::Year) { + endDuration = ISO8601::Duration { duration.dateDuration().years() + sign, 0, 0, 0, 0, 0, 0, 0, Int128(0), Int128(0) }; + } else if (unit == TemporalUnit::Month) { + // Step 6.b.ii: Else if unit is ~month~, let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, months + sign). + auto r = adjustDateDurationRecord(duration.dateDuration(), 0, 0, duration.dateDuration().months() + sign); + if (!r) + return makeUnexpected(r.error()); + endDuration = *r; + } else { + // Step 6.b.iii: Else (unit is ~week~), let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, weeks + sign). + auto r = adjustDateDurationRecord(duration.dateDuration(), 0, duration.dateDuration().weeks() + sign, std::nullopt); + if (!r) + return makeUnexpected(r.error()); + endDuration = *r; + } + // Step 6.b.iv: Let end be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], endDuration, ~constrain~). + auto endResult = TemporalCore::calendarDateAdd(calendarId, isoDate, endDuration, TemporalOverflow::Constrain); + if (!endResult) + return makeUnexpected(endResult.error()); + // Step 6.b.v: endDateTime = CombineISODateAndTimeRecord(end, isoDateTime.[[Time]]). + // Step 6.b.vi: endEpochNs = GetUTCEpochNanoseconds/GetEpochNanosecondsFor. (v+vi fused) + auto endNsResult = epochNanosecondsForDateAndTime(*endResult, isoTime, timeZone); + if (!endNsResult) + return makeUnexpected(endNsResult.error()); + // Step 6.b.vii: Let beyondEnd be nudgedEpochNs - endEpochNs. + auto beyondEnd = nudgedEpochNs - *endNsResult; + // Step 6.b.viii: If beyondEnd < 0, beyondEndSign = -1; > 0, = 1; else 0. + auto beyondEndSign = beyondEnd < 0 ? -1 : beyondEnd > 0 ? 1 : 0; + // Step 6.b.ix: If beyondEndSign ≠ -sign, set duration to CombineDateAndTimeDuration(endDuration, 0). + if (beyondEndSign != -sign) + duration = ISO8601::InternalDuration::combineDateAndTimeDuration(endDuration, 0); + // Step 6.b.x: Else, set done to true. + else + done = true; + } + // Step 6.c: Set unitIndex to unitIndex - 1. + unitIndex--; + } + // Step 7: Return duration. + return duration; +} + +// roundRelativeDuration — temporal_rs: InternalDurationRecord::round_relative_duration (src/builtins/core/duration/normalized.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-roundrelativeduration +TemporalResult roundRelativeDuration(ISO8601::InternalDuration& duration, + Int128 originEpochNs, Int128 destEpochNs, ISO8601::PlainDate isoDate, ISO8601::PlainTime isoTime, + TemporalUnit largestUnit, double increment, TemporalUnit smallestUnit, RoundingMode roundingMode, + const TimeZone* timeZone, CalendarID calendarId) +{ + // Step 1: Let irregularLengthUnit be false. + // Step 2: If IsCalendarUnit(smallestUnit) is true, set irregularLengthUnit to true. + // Step 3: If timeZone is not ~unset~ and smallestUnit is ~day~, set irregularLengthUnit to true. + bool irregularLengthUnit = isCalendarUnit(smallestUnit); + if (timeZone && smallestUnit == TemporalUnit::Day) + irregularLengthUnit = true; + // Step 4: If InternalDurationSign(duration) < 0, let sign be -1; else let sign be 1. + int32_t sign = (duration.sign() < 0) ? -1 : 1; + + NudgeResult nudgeResult; + // Step 5: If irregularLengthUnit is true, then + if (irregularLengthUnit) { + // Step 5.a: Let record be ? NudgeToCalendarUnit(...). + auto record = nudgeToCalendarUnit(sign, duration, originEpochNs, destEpochNs, isoDate, isoTime, increment, smallestUnit, roundingMode, timeZone, calendarId); + if (!record) + return makeUnexpected(record.error()); + // Step 5.b: Let nudgeResult be record.[[NudgeResult]]. + nudgeResult = record->nudgeResult; + // Step 5.c: Let total be record.[[Total]]. (total not needed by round/until/since callers; dropped) + } else if (timeZone) { + // Step 6.a: Let nudgeResult be ? NudgeToZonedTime(...). + auto result = nudgeToZonedTime(sign, duration, isoDate, isoTime, *timeZone, increment, smallestUnit, roundingMode, calendarId); + if (!result) + return makeUnexpected(result.error()); + nudgeResult = *result; + } else { + // Step 7.a: Let nudgeResult be ? NudgeToDayOrTime(...). + auto result = nudgeToDayOrTime(duration, destEpochNs, largestUnit, increment, smallestUnit, roundingMode); + if (!result) + return makeUnexpected(result.error()); + nudgeResult = *result; + } + // Step 8: Set duration to nudgeResult.[[Duration]]. + duration = nudgeResult.duration; + // Step 9: If nudgeResult.[[DidExpandCalendarUnit]] is true and smallestUnit is not ~week~, then + if (nudgeResult.didExpandCalendarUnit && smallestUnit != TemporalUnit::Week) { + // Step 9.a: Let startUnit be LargerOfTwoTemporalUnits(smallestUnit, ~day~). + auto startUnit = smallestUnit <= TemporalUnit::Day ? smallestUnit : TemporalUnit::Day; + // Step 9.b: Set duration to ? BubbleRelativeDuration(sign, duration, nudgeResult.[[NudgedEpochNs]], isoDateTime, timeZone, calendar, largestUnit, startUnit). + auto bubbled = bubbleRelativeDuration(sign, duration, nudgeResult.nudgedEpochNs, isoDate, isoTime, largestUnit, startUnit, timeZone, calendarId); + if (!bubbled) + return makeUnexpected(bubbled.error()); + duration = *bubbled; + } + // Step 10: Return duration. + return { }; +} + +// differenceZonedDateTimeForDuration — temporal_rs: ZonedDateTime::diff_zoned_datetime (src/builtins/core/zoned_date_time.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-differencezoneddatetime +TemporalResult differenceZonedDateTimeForDuration( + ISO8601::ExactTime startExact, ISO8601::ExactTime endExact, + const TimeZone& timeZone, TemporalUnit largestUnit, CalendarID calendarId) +{ + Int128 nsA = startExact.epochNanoseconds(); + Int128 nsB = endExact.epochNanoseconds(); + // Step 1: If ns1 = ns2, return CombineDateAndTimeDuration(ZeroDateDuration(), 0). + if (nsA == nsB) + return ISO8601::InternalDuration::combineDateAndTimeDuration(ISO8601::Duration(), Int128(0)); + // Step 2: Let startDateTime be GetISODateTimeFor(timeZone, ns1). + // Step 3: Let endDateTime be GetISODateTimeFor(timeZone, ns2). + // (Each GetISODateTimeFor is split: getOffsetNanosecondsFor computes the offset, + // exactTimeToLocalDateAndTime completes the date/time extraction.) + auto offset1Result = getOffsetNanosecondsFor(timeZone, startExact); + if (!offset1Result) + return makeUnexpected(offset1Result.error()); + auto offset2Result = getOffsetNanosecondsFor(timeZone, endExact); + if (!offset2Result) + return makeUnexpected(offset2Result.error()); + + ISO8601::PlainDate startDate, endDate; + ISO8601::PlainTime startTime, endTime; + exactTimeToLocalDateAndTime(startExact, *offset1Result, startDate, startTime); + exactTimeToLocalDateAndTime(endExact, *offset2Result, endDate, endTime); + + // Step 4: If CompareISODate(startDateTime.[[ISODate]], endDateTime.[[ISODate]]) = 0, return CombineDateAndTimeDuration(ZeroDateDuration(), ns2 - ns1). + if (!isoDateCompare(startDate, endDate)) + return ISO8601::InternalDuration::combineDateAndTimeDuration(ISO8601::Duration(), nsB - nsA); + + // Step 5: If ns2 - ns1 < 0, let sign be 1; else let sign be -1. + int32_t diffSign = (nsB - nsA < Int128 { 0 }) ? 1 : -1; + // Step 6: If sign = -1, let maxDayCorrection be 2; else let maxDayCorrection be 1. + int32_t maxDayCorrection = (diffSign == -1) ? 2 : 1; + // Step 7: Let dayCorrection be 0. + int32_t dayCorrection = 0; + // Step 8: Let timeDuration be DifferenceTime(startDateTime.[[Time]], endDateTime.[[Time]]). + Int128 timeDiff = timeDurationFromComponents( + static_cast(endTime.hour()) - static_cast(startTime.hour()), + static_cast(endTime.minute()) - static_cast(startTime.minute()), + static_cast(endTime.second()) - static_cast(startTime.second()), + static_cast(endTime.millisecond()) - static_cast(startTime.millisecond()), + static_cast(endTime.microsecond()) - static_cast(startTime.microsecond()), + static_cast(endTime.nanosecond()) - static_cast(startTime.nanosecond())); + int32_t timeSign = (timeDiff < 0) ? -1 : (timeDiff > 0) ? 1 : 0; + // Step 9: If timeSign = sign, set dayCorrection to dayCorrection + 1. + if (timeSign == diffSign) + dayCorrection++; + + // Step 10: Let success be false. + ISO8601::PlainDate intermediateDate = endDate; + Int128 adjustedTimeDiff = timeDiff; + bool success = false; + // Step 11: Repeat, while dayCorrection ≤ maxDayCorrection and success is false, + while (dayCorrection <= maxDayCorrection && !success) { + // Step 11.a: Let intermediateDate be AddDaysToISODate(endDateTime.[[ISODate]], dayCorrection × sign). + intermediateDate = balanceISODate(endDate.year(), static_cast(endDate.month()), static_cast(endDate.day()) + dayCorrection * diffSign); + // Step 11.b: intermediateDateTime = CombineISODateAndTimeRecord(intermediateDate, startDateTime.[[Time]]). + // Step 11.c: intermediateNs = GetEpochNanosecondsFor(timeZone, intermediateDateTime, ~compatible~). (11.b+11.c fused) + auto intermediateNsResult = getEpochNanosecondsFor(timeZone, intermediateDate, startTime, TemporalDisambiguation::Compatible); + if (!intermediateNsResult) + return makeUnexpected(intermediateNsResult.error()); + // Step 11.d: Set timeDuration to TimeDurationFromEpochNanosecondsDifference(ns2, intermediateNs). + adjustedTimeDiff = nsB - intermediateNsResult->epochNanoseconds(); + // Step 11.e: Let timeSign be TimeDurationSign(timeDuration). + int32_t adjTimeSign = (adjustedTimeDiff < 0) ? -1 : (adjustedTimeDiff > 0) ? 1 : 0; + // Step 11.f: If sign ≠ timeSign, set success to true. + if (diffSign != adjTimeSign) + success = true; + // Step 11.g: Set dayCorrection to dayCorrection + 1. + dayCorrection++; + } + // Step 12: Assert: success is true. + ASSERT(success); + + // Step 13: Let dateLargestUnit be LargerOfTwoTemporalUnits(largestUnit, ~day~). + TemporalUnit dateLargestUnit = (largestUnit > TemporalUnit::Day) ? TemporalUnit::Day : largestUnit; + // Step 14: Let dateDifference be CalendarDateUntil(calendar, startDateTime.[[ISODate]], intermediateDate, dateLargestUnit). + // (Note: intermediateDateTime.[[ISODate]] = intermediateDate computed above.) + auto dateDiffResult = TemporalCore::calendarDateUntil(calendarId, startDate, intermediateDate, dateLargestUnit); + if (!dateDiffResult) + return makeUnexpected(dateDiffResult.error()); + ISO8601::Duration dateDiff = *dateDiffResult; + + // Step 15: Return CombineDateAndTimeDuration(dateDifference, timeDuration). + // (Implementation folds days into time when largestUnit > day.) + constexpr Int128 nsPerDay = ISO8601::ExactTime::nsPerDay; + double remainingDays = 0; + Int128 timeDuration = adjustedTimeDiff; + if (largestUnit != dateLargestUnit) + timeDuration = adjustedTimeDiff + Int128(dateDiff.days()) * nsPerDay; + else + remainingDays = static_cast(dateDiff.days()); + + ISO8601::Duration datePart(dateDiff.years(), dateDiff.months(), + dateDiff.weeks(), static_cast(remainingDays), 0, 0, 0, 0, Int128(0), Int128(0)); + return ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, timeDuration); +} + +} // namespace TemporalCore +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.h b/Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.h new file mode 100644 index 000000000000..97ff27a0b362 --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// JSC Temporal Core — Duration arithmetic algorithms +// temporal_rs reference: src/builtins/core/duration.rs +// Last synced: v0.2.3 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JSC { + +// NudgeResult — output record from a single nudge step: rounded duration, epoch ns, and calendar-expand flag. +// temporal_rs: NudgeResultRecord (src/builtins/core/duration.rs) +struct NudgeResult { + ISO8601::InternalDuration duration; + Int128 nudgedEpochNs; + bool didExpandCalendarUnit; + NudgeResult() { } + NudgeResult(ISO8601::InternalDuration d, Int128 ns, bool expanded) + : duration(d) + , nudgedEpochNs(ns) + , didExpandCalendarUnit(expanded) + { + } +}; + +// Nudged — combines a NudgeResult with a fractional total used by RoundRelativeDuration. +// temporal_rs: NudgedRecord (src/builtins/core/duration.rs) +struct Nudged { + NudgeResult nudgeResult; + double total; + Nudged() { } + Nudged(NudgeResult n, double t) + : nudgeResult(n) + , total(t) + { + } +}; + +} // namespace JSC + +namespace JSC { +namespace TemporalCore { + +int JS_EXPORT_PRIVATE durationSign(const ISO8601::Duration&); + +ISO8601::Duration JS_EXPORT_PRIVATE negateDuration(const ISO8601::Duration&); + +ISO8601::Duration JS_EXPORT_PRIVATE absDuration(const ISO8601::Duration&); + +TemporalUnit NODELETE JS_EXPORT_PRIVATE largestSubduration(const ISO8601::Duration&); + +int64_t JS_EXPORT_PRIVATE totalSeconds(const ISO8601::Duration&); + +Int128 JS_EXPORT_PRIVATE totalSubseconds(const ISO8601::Duration&); + +std::optional JS_EXPORT_PRIVATE balanceDuration(ISO8601::Duration&, TemporalUnit largestUnit); + +Int128 JS_EXPORT_PRIVATE timeDurationFromComponents(double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); + +std::pair JS_EXPORT_PRIVATE splitTimeDuration(Int128 timeDuration); + +ISO8601::PlainTime JS_EXPORT_PRIVATE plainTimeFromSubdayNs(Int128 ns); + +double JS_EXPORT_PRIVATE totalTimeDuration(Int128, TemporalUnit); + +ISO8601::Duration JS_EXPORT_PRIVATE temporalDurationFromInternal(ISO8601::InternalDuration, TemporalUnit largestUnit); + +ISO8601::InternalDuration JS_EXPORT_PRIVATE toInternalDuration(ISO8601::Duration); + +TemporalResult JS_EXPORT_PRIVATE add24HourDaysToTimeDuration(Int128 timeDuration, double days); + +TemporalResult JS_EXPORT_PRIVATE toInternalDurationRecordWith24HourDays(ISO8601::Duration); + +TemporalResult JS_EXPORT_PRIVATE toDateDurationRecordWithoutTime(ISO8601::Duration); + +Int128 JS_EXPORT_PRIVATE getUTCEpochNanoseconds(ISO8601::PlainDate, ISO8601::PlainTime); + +constexpr int32_t unitIndexInTable(TemporalUnit); + +constexpr TemporalUnit unitInTable(int32_t); + +TemporalResult JS_EXPORT_PRIVATE adjustDateDurationRecord(const ISO8601::Duration& dateDuration, double days, std::optional weeks, std::optional months); + +TemporalResult JS_EXPORT_PRIVATE nudgeToCalendarUnit(int32_t sign, + const ISO8601::InternalDuration&, Int128 originEpochNs, Int128 destEpochNs, + ISO8601::PlainDate, ISO8601::PlainTime, double increment, TemporalUnit, + RoundingMode, const TimeZone* = nullptr, CalendarID = iso8601CalendarID()); + +TemporalResult JS_EXPORT_PRIVATE nudgeToZonedTime(int32_t sign, + const ISO8601::InternalDuration&, ISO8601::PlainDate, ISO8601::PlainTime, + const TimeZone&, double increment, TemporalUnit, RoundingMode, + CalendarID = iso8601CalendarID()); + +TemporalResult JS_EXPORT_PRIVATE nudgeToDayOrTime(ISO8601::InternalDuration, + Int128 destEpochNs, TemporalUnit largestUnit, double increment, + TemporalUnit smallestUnit, RoundingMode); + +TemporalResult JS_EXPORT_PRIVATE bubbleRelativeDuration(int32_t sign, + ISO8601::InternalDuration, Int128 nudgedEpochNs, ISO8601::PlainDate, ISO8601::PlainTime, + TemporalUnit largestUnit, TemporalUnit smallestUnit, + const TimeZone*, CalendarID); + +TemporalResult JS_EXPORT_PRIVATE roundRelativeDuration(ISO8601::InternalDuration&, + Int128 originEpochNs, Int128 destEpochNs, ISO8601::PlainDate, ISO8601::PlainTime, + TemporalUnit largestUnit, double increment, TemporalUnit smallestUnit, + RoundingMode, const TimeZone*, CalendarID); + +TemporalResult JS_EXPORT_PRIVATE differenceZonedDateTimeForDuration( + ISO8601::ExactTime startExact, ISO8601::ExactTime endExact, + const TimeZone&, TemporalUnit largestUnit, CalendarID = iso8601CalendarID()); + +} // namespace TemporalCore +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.cpp b/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.cpp index f7444462e162..812c4b0ed3f7 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.cpp +++ b/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.cpp @@ -27,6 +27,7 @@ #include "ISOArithmetic.h" #include "DateConstructor.h" +#include "Rounding.h" #include #include @@ -316,5 +317,81 @@ ISO8601::InternalDuration diffISODateTime(const ISO8601::PlainDate& d1, const IS return ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, timeDuration); } +// RoundTime steps 1–6 (quantity only) — temporal_rs: IsoTime::round (src/iso.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-roundtime +// Returns {quantity, baseOffset} in ns; caller does RoundNumberToIncrement + BalanceTime in Int128 +// to avoid double precision loss. Day unit omitted — caller handles overflow via nsPerDay bounds. +static std::pair roundTime(const ISO8601::PlainTime& t, TemporalUnit unit) +{ + using ET = ISO8601::ExactTime; + const Int128 lH = Int128(t.hour()); + const Int128 lMi = Int128(t.minute()); + const Int128 lS = Int128(t.second()); + const Int128 lMs = Int128(t.millisecond()); + const Int128 lUs = Int128(t.microsecond()); + const Int128 lNs = Int128(t.nanosecond()); + switch (unit) { + case TemporalUnit::Hour: + // Step 1: quantity = full time from midnight in ns. + return { lH * ET::nsPerHour + lMi * ET::nsPerMinute + lS * ET::nsPerSecond + lMs * ET::nsPerMillisecond + lUs * ET::nsPerMicrosecond + lNs, 0 }; + case TemporalUnit::Minute: + // Step 2: quantity = minute-relative; baseOffset = hours. + return { lMi * ET::nsPerMinute + lS * ET::nsPerSecond + lMs * ET::nsPerMillisecond + lUs * ET::nsPerMicrosecond + lNs, lH * ET::nsPerHour }; + case TemporalUnit::Second: + // Step 3: quantity = second-relative; baseOffset = hours+minutes. + return { lS * ET::nsPerSecond + lMs * ET::nsPerMillisecond + lUs * ET::nsPerMicrosecond + lNs, lH * ET::nsPerHour + lMi * ET::nsPerMinute }; + case TemporalUnit::Millisecond: + // Step 4: quantity = ms-relative; baseOffset = hours+minutes+seconds. + return { lMs * ET::nsPerMillisecond + lUs * ET::nsPerMicrosecond + lNs, lH * ET::nsPerHour + lMi * ET::nsPerMinute + lS * ET::nsPerSecond }; + case TemporalUnit::Microsecond: + // Step 5: quantity = us-relative; baseOffset = hours+minutes+seconds+ms. + return { lUs * ET::nsPerMicrosecond + lNs, lH * ET::nsPerHour + lMi * ET::nsPerMinute + lS * ET::nsPerSecond + lMs * ET::nsPerMillisecond }; + default: // Nanosecond + // Step 6: Assert unit is nanosecond. quantity = ns; baseOffset = everything else. + return { lNs, lH * ET::nsPerHour + lMi * ET::nsPerMinute + lS * ET::nsPerSecond + lMs * ET::nsPerMillisecond + lUs * ET::nsPerMicrosecond }; + } +} + +// RoundISODateTime — temporal_rs: IsoDateTime::round (src/iso.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-roundisodatetime +RoundedISODateTime roundISODateTime(ISO8601::PlainDate date, ISO8601::PlainTime time, Int128 incrementNs, TemporalUnit unit, RoundingMode mode) +{ + using ET = ISO8601::ExactTime; + + // Step 1: Assert ISODateTimeWithinLimits(isoDateTime). + ASSERT(ISO8601::isDateTimeWithinLimits(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second(), time.millisecond(), time.microsecond(), time.nanosecond())); + + // Step 2: roundedTime = RoundTime(time, increment, unit, roundingMode). + auto [quantity, baseOffset] = roundTime(time, unit); + Int128 roundedLocalNs = baseOffset + roundNumberToIncrementInt128(quantity, incrementNs, mode); + + // Step 3: balanceResult = AddDaysToISODate(isoDate, roundedTime.[[Days]]). + if (roundedLocalNs < 0) { + roundedLocalNs += ET::nsPerDay; + int32_t days = WTF::daysFromYearMonth(date.year(), date.month() - 1) + (date.day() - 1) - 1; + auto [y, m, d] = WTF::yearMonthDayFromDays(days); + date = ISO8601::PlainDate(y, static_cast(m + 1), static_cast(d)); + } else if (roundedLocalNs >= ET::nsPerDay) { + roundedLocalNs -= ET::nsPerDay; + int32_t days = WTF::daysFromYearMonth(date.year(), date.month() - 1) + (date.day() - 1) + 1; + auto [y, m, d] = WTF::yearMonthDayFromDays(days); + date = ISO8601::PlainDate(y, static_cast(m + 1), static_cast(d)); + } + + // Step 4: CombineISODateAndTimeRecord(balanceResult, roundedTime). + Int128 rem = roundedLocalNs; + unsigned h = static_cast(rem / ET::nsPerHour); + rem %= ET::nsPerHour; + unsigned mi = static_cast(rem / ET::nsPerMinute); + rem %= ET::nsPerMinute; + unsigned s = static_cast(rem / ET::nsPerSecond); + rem %= ET::nsPerSecond; + unsigned ms = static_cast(rem / ET::nsPerMillisecond); + rem %= ET::nsPerMillisecond; + unsigned us = static_cast(rem / ET::nsPerMicrosecond); + unsigned ns = static_cast(rem % ET::nsPerMicrosecond); + return { date, ISO8601::PlainTime(h, mi, s, ms, us, ns) }; +} + } // namespace TemporalCore } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.h b/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.h index d44017143f6a..fba25f7f9011 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.h +++ b/Source/JavaScriptCore/runtime/temporal/core/ISOArithmetic.h @@ -53,5 +53,11 @@ ISO8601::Duration JS_EXPORT_PRIVATE diffISODate(const ISO8601::PlainDate& one, c ISO8601::InternalDuration JS_EXPORT_PRIVATE diffISODateTime(const ISO8601::PlainDate& d1, const ISO8601::PlainTime& t1, const ISO8601::PlainDate& d2, const ISO8601::PlainTime& t2, TemporalUnit largestUnit); +struct RoundedISODateTime { + ISO8601::PlainDate date; + ISO8601::PlainTime time; +}; +RoundedISODateTime JS_EXPORT_PRIVATE roundISODateTime(ISO8601::PlainDate, ISO8601::PlainTime, Int128 incrementNs, TemporalUnit, RoundingMode); + } // namespace TemporalCore } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.cpp b/Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.cpp new file mode 100644 index 000000000000..f81fc3da2e3b --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "PlainDateTimeCore.h" + +#include "CalendarICUBridge.h" +#include "DurationArithmetic.h" +#include "ISO8601.h" +#include "ISOArithmetic.h" +#include "TemporalObject.h" + +namespace JSC { +namespace TemporalCore { + +// CompareISODateTime — temporal_rs: PlainDateTime::compare_iso (pure) +// https://tc39.es/proposal-temporal/#sec-temporal-compareisodatetime +int32_t compareISODateTime(ISO8601::PlainDate d1, ISO8601::PlainTime t1, ISO8601::PlainDate d2, ISO8601::PlainTime t2) +{ + // Step 1: Let dateResult be CompareISODate(isoDateTime1.[[ISODate]], isoDateTime2.[[ISODate]]). + // Step 2: If dateResult ≠ 0, return dateResult. + if (auto r = isoDateCompare(d1, d2)) + return r; + // Step 3: Return CompareTimeRecord(isoDateTime1.[[Time]], isoDateTime2.[[Time]]). + return isoTimeCompare(t1, t2); +} + +// DifferencePlainDateTimeWithRounding — temporal_rs: PlainDateTime::diff inner path +// https://tc39.es/proposal-temporal/#sec-temporal-differenceplaindatetimewithrounding +static TemporalResult differencePlainDateTimeWithRounding( + ISO8601::PlainDate thisDate, ISO8601::PlainTime thisTime, + ISO8601::PlainDate otherDate, ISO8601::PlainTime otherTime, + CalendarID calendarId, + TemporalUnit largestUnit, TemporalUnit smallestUnit, + RoundingMode roundingMode, double increment) +{ + // Step 1: If CompareISODateTime = 0, return zero. (caller already checked, but guard here too) + // Step 2: ISODateTimeWithinLimits check. + if (!ISO8601::isDateTimeWithinLimits(thisDate.year(), thisDate.month(), thisDate.day(), thisTime.hour(), thisTime.minute(), thisTime.second(), thisTime.millisecond(), thisTime.microsecond(), thisTime.nanosecond()) + || !ISO8601::isDateTimeWithinLimits(otherDate.year(), otherDate.month(), otherDate.day(), otherTime.hour(), otherTime.minute(), otherTime.second(), otherTime.millisecond(), otherTime.microsecond(), otherTime.nanosecond())) + return makeUnexpected(rangeError("date-time is outside the representable range for Temporal"_s)); + + // Step 3: diff = DifferenceISODateTime(isoDateTime1, isoDateTime2, calendar, largestUnit). + ISO8601::InternalDuration diff; + if (calendarId != iso8601CalendarID()) { + Int128 timeDiff = timeDurationFromComponents( + static_cast(otherTime.hour()) - static_cast(thisTime.hour()), + static_cast(otherTime.minute()) - static_cast(thisTime.minute()), + static_cast(otherTime.second()) - static_cast(thisTime.second()), + static_cast(otherTime.millisecond()) - static_cast(thisTime.millisecond()), + static_cast(otherTime.microsecond()) - static_cast(thisTime.microsecond()), + static_cast(otherTime.nanosecond()) - static_cast(thisTime.nanosecond())); + int32_t timeSign = timeDiff < 0 ? -1 : timeDiff > 0 ? 1 : 0; + int32_t dateSign = isoDateCompare(thisDate, otherDate); + ISO8601::PlainDate adjustedD2 = otherDate; + if (dateSign && timeSign && dateSign == timeSign) { + adjustedD2 = balanceISODate(adjustedD2.year(), static_cast(adjustedD2.month()), + static_cast(adjustedD2.day()) + dateSign); + timeDiff -= Int128(dateSign) * ISO8601::ExactTime::nsPerDay; + } + TemporalUnit dateLargestUnit = (largestUnit > TemporalUnit::Day) ? TemporalUnit::Day : largestUnit; + auto dateDiffResult = TemporalCore::calendarDateUntil(calendarId, thisDate, adjustedD2, dateLargestUnit); + if (!dateDiffResult) + return makeUnexpected(dateDiffResult.error()); + auto& dateDiff = *dateDiffResult; + if (largestUnit > TemporalUnit::Day) + timeDiff += Int128(static_cast(dateDiff.days())) * ISO8601::ExactTime::nsPerDay; + diff = ISO8601::InternalDuration::combineDateAndTimeDuration( + ISO8601::Duration(dateDiff.years(), dateDiff.months(), dateDiff.weeks(), largestUnit > TemporalUnit::Day ? 0 : dateDiff.days(), 0, 0, 0, 0, Int128(0), Int128(0)), timeDiff); + } else + diff = diffISODateTime(thisDate, thisTime, otherDate, otherTime, largestUnit); + + // Step 4: If smallestUnit=nanosecond and increment=1, return diff. + if (smallestUnit == TemporalUnit::Nanosecond && increment == 1) + return diff; + + // Step 5: originEpochNs = GetUTCEpochNanoseconds(isoDateTime1). + Int128 originEpochNs = getUTCEpochNanoseconds(thisDate, thisTime); + // Step 6: destEpochNs = GetUTCEpochNanoseconds(isoDateTime2). + Int128 destEpochNs = getUTCEpochNanoseconds(otherDate, otherTime); + // Step 7: Return ? RoundRelativeDuration(diff, originEpochNs, destEpochNs, isoDateTime1, unset, ...). + auto roundResult = roundRelativeDuration(diff, originEpochNs, destEpochNs, + thisDate, thisTime, largestUnit, increment, smallestUnit, roundingMode, nullptr, calendarId); + if (!roundResult) + return makeUnexpected(roundResult.error()); + return diff; +} + +// DifferenceTemporalPlainDateTime — temporal_rs: PlainDateTime::diff (src/builtins/core/plain_date_time.rs; until/since call this) +// https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplaindatetime +// Steps 1-4 (ToTemporalDateTime, CalendarEquals, GetOptionsObject, GetDifferenceSettings) +// are handled by the JS layer before calling this function. +TemporalResult differenceTemporalPlainDateTime( + DifferenceOperation op, + ISO8601::PlainDate thisDate, ISO8601::PlainTime thisTime, + ISO8601::PlainDate otherDate, ISO8601::PlainTime otherTime, + CalendarID calendarId, + TemporalUnit smallestUnit, TemporalUnit largestUnit, + RoundingMode roundingMode, double increment) +{ + ASSERT(largestUnit <= smallestUnit); + ASSERT(increment >= 1); + // Step 5: If CompareISODateTime = 0, return zero duration. + if (thisDate == otherDate && thisTime == otherTime) + return ISO8601::Duration(); + + // Step 6: internalDuration = DifferencePlainDateTimeWithRounding(...). + auto internalDuration = differencePlainDateTimeWithRounding(thisDate, thisTime, otherDate, otherTime, calendarId, largestUnit, smallestUnit, roundingMode, increment); + if (!internalDuration) + return makeUnexpected(internalDuration.error()); + + // Step 7: result = TemporalDurationFromInternal(internalDuration, largestUnit). + auto result = temporalDurationFromInternal(*internalDuration, largestUnit); + // Step 8: If operation is since, set result to CreateNegatedTemporalDuration(result). + if (op == DifferenceOperation::Since) + result = -result; + // Step 9: Return result. + return result; +} + +} // namespace TemporalCore +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.h b/Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.h new file mode 100644 index 000000000000..7004aa31c025 --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/PlainDateTimeCore.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// JSC Temporal Core — PlainDateTime algorithms +// temporal_rs reference: src/builtins/core/plain_date_time.rs +// Last synced: v0.2.3 + +#include +#include +#include +#include +#include + +namespace JSC { +namespace TemporalCore { + +int32_t NODELETE JS_EXPORT_PRIVATE compareISODateTime(ISO8601::PlainDate, ISO8601::PlainTime, ISO8601::PlainDate, ISO8601::PlainTime); + +TemporalResult JS_EXPORT_PRIVATE differenceTemporalPlainDateTime( + DifferenceOperation, + ISO8601::PlainDate thisDate, ISO8601::PlainTime thisTime, + ISO8601::PlainDate otherDate, ISO8601::PlainTime otherTime, + CalendarID, + TemporalUnit smallestUnit, TemporalUnit largestUnit, + RoundingMode, double increment); + +} // namespace TemporalCore +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/TemporalEnums.h b/Source/JavaScriptCore/runtime/temporal/core/TemporalEnums.h index 2e17d186d6c1..ff58c4f2365b 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/TemporalEnums.h +++ b/Source/JavaScriptCore/runtime/temporal/core/TemporalEnums.h @@ -60,153 +60,6 @@ enum class OffsetBehaviour : uint8_t { Option, // +HH:MM present — use inlineOffsetNs }; -// https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier -// temporal_rs: AnyCalendarKind (icu_calendar crate, imported by builtins/core/calendar.rs) -enum class CalendarKind : uint8_t { - Iso8601 = 0, // must be 0 so zero-initialised objects default to iso8601 - Buddhist, - Chinese, - Coptic, - Dangi, - Ethiopic, - EthioAA, - Gregory, - Hebrew, - Indian, - Islamic, - IslamicCivil, - IslamicRGSA, - IslamicTBLA, - IslamicUmmAlQura, - Japanese, - Persian, - Roc, - Bangla, - Gujarati, - Kannada, - Marathi, - Odia, - Tamil, - Telugu, - Vikram, -}; - -inline ASCIILiteral toCalendarIdentifier(CalendarKind k) -{ - switch (k) { - case CalendarKind::Iso8601: - return "iso8601"_s; - case CalendarKind::Buddhist: - return "buddhist"_s; - case CalendarKind::Chinese: - return "chinese"_s; - case CalendarKind::Coptic: - return "coptic"_s; - case CalendarKind::Dangi: - return "dangi"_s; - case CalendarKind::Ethiopic: - return "ethiopic"_s; - case CalendarKind::EthioAA: - return "ethioaa"_s; - case CalendarKind::Gregory: - return "gregory"_s; - case CalendarKind::Hebrew: - return "hebrew"_s; - case CalendarKind::Indian: - return "indian"_s; - case CalendarKind::Islamic: - return "islamic"_s; - case CalendarKind::IslamicCivil: - return "islamic-civil"_s; - case CalendarKind::IslamicRGSA: - return "islamic-rgsa"_s; - case CalendarKind::IslamicTBLA: - return "islamic-tbla"_s; - case CalendarKind::IslamicUmmAlQura: - return "islamic-umalqura"_s; - case CalendarKind::Japanese: - return "japanese"_s; - case CalendarKind::Persian: - return "persian"_s; - case CalendarKind::Roc: - return "roc"_s; - case CalendarKind::Bangla: - return "bangla"_s; - case CalendarKind::Gujarati: - return "gujarati"_s; - case CalendarKind::Kannada: - return "kannada"_s; - case CalendarKind::Marathi: - return "marathi"_s; - case CalendarKind::Odia: - return "odia"_s; - case CalendarKind::Tamil: - return "tamil"_s; - case CalendarKind::Telugu: - return "telugu"_s; - case CalendarKind::Vikram: - return "vikram"_s; - } - RELEASE_ASSERT_NOT_REACHED(); -} - -inline CalendarKind toCalendarKind(WTF::StringView s) -{ - if (s.isEmpty() || s == "iso8601"_s) - return CalendarKind::Iso8601; - if (s == "buddhist"_s) - return CalendarKind::Buddhist; - if (s == "chinese"_s) - return CalendarKind::Chinese; - if (s == "coptic"_s) - return CalendarKind::Coptic; - if (s == "dangi"_s) - return CalendarKind::Dangi; - if (s == "ethiopic"_s) - return CalendarKind::Ethiopic; - if (s == "ethioaa"_s || s == "ethiopic-amete-alem"_s) - return CalendarKind::EthioAA; - if (s == "gregory"_s || s == "gregorian"_s) - return CalendarKind::Gregory; - if (s == "hebrew"_s) - return CalendarKind::Hebrew; - if (s == "indian"_s) - return CalendarKind::Indian; - if (s == "islamic"_s) - return CalendarKind::Islamic; - if (s == "islamic-civil"_s || s == "islamicc"_s) - return CalendarKind::IslamicCivil; - if (s == "islamic-rgsa"_s) - return CalendarKind::IslamicRGSA; - if (s == "islamic-tbla"_s) - return CalendarKind::IslamicTBLA; - if (s == "islamic-umalqura"_s) - return CalendarKind::IslamicUmmAlQura; - if (s == "japanese"_s) - return CalendarKind::Japanese; - if (s == "persian"_s) - return CalendarKind::Persian; - if (s == "roc"_s) - return CalendarKind::Roc; - if (s == "bangla"_s) - return CalendarKind::Bangla; - if (s == "gujarati"_s) - return CalendarKind::Gujarati; - if (s == "kannada"_s) - return CalendarKind::Kannada; - if (s == "marathi"_s) - return CalendarKind::Marathi; - if (s == "odia"_s) - return CalendarKind::Odia; - if (s == "tamil"_s) - return CalendarKind::Tamil; - if (s == "telugu"_s) - return CalendarKind::Telugu; - if (s == "vikram"_s) - return CalendarKind::Vikram; - return CalendarKind::Iso8601; -} - // ----------------------------------------------------------------------- // Temporal unit // ----------------------------------------------------------------------- @@ -280,6 +133,9 @@ constexpr Int128 lengthInNanoseconds(TemporalUnit unit) RELEASE_ASSERT_NOT_REACHED(); } +// https://tc39.es/proposal-temporal/#sec-temporal-iscalendarunit +constexpr bool isCalendarUnit(TemporalUnit unit) { return unit <= TemporalUnit::Week; } + // ----------------------------------------------------------------------- // Rounding enums // ----------------------------------------------------------------------- diff --git a/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.cpp b/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.cpp index 00564fbaae31..f8998c3d6a8e 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.cpp +++ b/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.cpp @@ -473,7 +473,7 @@ TemporalResult getEpochNanosecondsFor(const TimeZone& timeZo // addZonedDateTime — temporal_rs: ZonedDateTime::add_zoned_date_time (src/builtins/core/zoned_date_time.rs) // https://tc39.es/proposal-temporal/#sec-temporal-addzoneddatetime -TemporalResult addZonedDateTime(ISO8601::ExactTime startEpochNs, const TimeZone& timeZone, const ISO8601::Duration& duration, TemporalOverflow overflow, StringView calendarId) +TemporalResult addZonedDateTime(ISO8601::ExactTime startEpochNs, const TimeZone& timeZone, const ISO8601::Duration& duration, TemporalOverflow overflow, CalendarID calendarId) { // NOTE: Pre-compute the time duration (norm) as nanoseconds; used by AddInstant in steps 1 and 7. CheckedInt128 m = checkedCastDoubleToInt128(duration.minutes()) + checkedCastDoubleToInt128(duration.hours()) * Int128(60); @@ -508,11 +508,10 @@ TemporalResult addZonedDateTime(ISO8601::ExactTime startEpoc // 3. Let addedDate be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], duration.[[Date]], overflow). ISO8601::Duration dateDuration(duration.years(), duration.months(), duration.weeks(), duration.days(), 0, 0, 0, 0, 0, 0); TemporalResult addedDateResult; - CalendarID calId = calendarIDFromString(calendarId); - if (calendarIsISO(calId)) + if (calendarIsISO(calendarId)) addedDateResult = calendarDateAdd(date, dateDuration, overflow); else - addedDateResult = TemporalCore::calendarDateAdd(calId, date, dateDuration, overflow); + addedDateResult = TemporalCore::calendarDateAdd(calendarId, date, dateDuration, overflow); if (!addedDateResult) return makeUnexpected(addedDateResult.error()); diff --git a/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.h b/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.h index 1beca1386a7a..df5dfc59ad09 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.h +++ b/Source/JavaScriptCore/runtime/temporal/core/TimeZoneICUBridge.h @@ -82,7 +82,7 @@ TemporalResult> JS_EXPORT_PRIVATE getTimeZoneT TemporalResult JS_EXPORT_PRIVATE getEpochNanosecondsFor(const TimeZone&, const ISO8601::PlainDate&, const ISO8601::PlainTime&, TemporalDisambiguation); -TemporalResult JS_EXPORT_PRIVATE addZonedDateTime(ISO8601::ExactTime startEpochNs, const TimeZone&, const ISO8601::Duration&, TemporalOverflow, StringView calendarId = "iso8601"_s); +TemporalResult JS_EXPORT_PRIVATE addZonedDateTime(ISO8601::ExactTime startEpochNs, const TimeZone&, const ISO8601::Duration&, TemporalOverflow, CalendarID calendarKind = iso8601CalendarID()); } // namespace TemporalCore } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.cpp b/Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.cpp new file mode 100644 index 000000000000..f12b1d52ce38 --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.cpp @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ZonedDateTimeCore.h" + +#include "CalendarICUBridge.h" +#include "DurationArithmetic.h" +#include "ISO8601.h" +#include "ISOArithmetic.h" +#include "Rounding.h" +#include "TemporalObject.h" +#include "TimeZoneICUBridge.h" +#include +#include + +namespace JSC { +namespace TemporalCore { + +// InterpretISODateTimeOffset — temporal_rs: interpret_isodatetime_offset in zoned_date_time.rs +// https://tc39.es/proposal-temporal/#sec-temporal-interpretisodatetimeoffset +TemporalResult interpretISODateTimeOffset( + const ISO8601::PlainDate& date, + const ISO8601::PlainTime& time, + bool useStartOfDay, + OffsetBehaviour offsetBehaviour, + TemporalOffsetDisambiguation offsetOpt, + int64_t inlineOffsetNs, + bool offsetHasSubMinutePrecision, + const TimeZone& timeZone, + TemporalDisambiguation disambiguation) +{ + // Step 1: If time is start-of-day, then + if (useStartOfDay) { + ASSERT(offsetBehaviour == OffsetBehaviour::Wall); // 1.a + ASSERT(!inlineOffsetNs); // 1.b + return getStartOfDay(timeZone, date); // 1.c + } + + // Step 2: isoDateTime = CombineISODateAndTimeRecord(isoDate, time). (implicit — date+time passed separately) + // Step 3: If offsetBehaviour is wall, or offsetBehaviour is option and offsetOption is ignore. + if (offsetBehaviour == OffsetBehaviour::Wall + || (offsetBehaviour == OffsetBehaviour::Option && offsetOpt == TemporalOffsetDisambiguation::Ignore)) + return getEpochNanosecondsFor(timeZone, date, time, disambiguation); + + // Step 4: offsetBehaviour is exact or option+use: compute epoch ns = UTC(date,time) - offset. + // Steps 4a-4d fused: GetUTCEpochNanoseconds(date,time) - offset ≡ BalanceISODateTime + GetUTCEpochNanoseconds + // since both are linear; IsValidEpochNanoseconds ↔ CheckISODaysRange (same ±10^8 day bound). + if (offsetBehaviour == OffsetBehaviour::Exact + || (offsetBehaviour == OffsetBehaviour::Option && offsetOpt == TemporalOffsetDisambiguation::Use)) { + Int128 naiveNs = getUTCEpochNanoseconds(date, time); + ISO8601::ExactTime result(naiveNs - Int128(inlineOffsetNs)); + if (!result.isValid()) + return makeUnexpected(rangeError("date/time offset combination is outside the supported range for Temporal.ZonedDateTime"_s)); + return result; + } + + // Step 5: Assert offsetBehaviour is option. + ASSERT(offsetBehaviour == OffsetBehaviour::Option); + // Step 6: Assert offsetOption is prefer or reject. + ASSERT(offsetOpt == TemporalOffsetDisambiguation::Prefer || offsetOpt == TemporalOffsetDisambiguation::Reject); + // Step 7: CheckISODaysRange(isoDate). + if (std::abs(dateToDaysFrom1970(date.year(), static_cast(date.month()) - 1, static_cast(date.day()))) > 1e8) + return makeUnexpected(rangeError("wall-clock date is outside the representable range for Temporal.ZonedDateTime"_s)); + + // Step 8: utcEpochNanoseconds = GetUTCEpochNanoseconds(isoDateTime). + Int128 utcEpochNs = getUTCEpochNanoseconds(date, time); + + // Step 9: possibleEpochNs = GetPossibleEpochNanoseconds(timeZone, isoDateTime). + auto possible = getPossibleEpochNanosecondsFor(timeZone, date, time); + if (!possible) + return makeUnexpected(possible.error()); + + // Step 10: For each element candidate of possibleEpochNs. + bool matchMinutes = !offsetHasSubMinutePrecision; + for (auto& candidate : epochCandidates(*possible)) { + // Step 10a: candidateOffset = utcEpochNanoseconds - candidate. + Int128 candidateOffset = utcEpochNs - candidate.epochNanoseconds(); + // Step 10b: If candidateOffset = offsetNanoseconds, return candidate. + if (candidateOffset == Int128(inlineOffsetNs)) + return candidate; + // Step 10c: If matchBehaviour is match-minutes, check rounded. + if (matchMinutes) { + if (roundNumberToIncrementInt128(candidateOffset, ISO8601::ExactTime::nsPerMinute, RoundingMode::HalfExpand) == Int128(inlineOffsetNs)) + return candidate; + } + } + + // Step 11: If offsetOption is reject, throw RangeError. + if (offsetOpt == TemporalOffsetDisambiguation::Reject) + return makeUnexpected(rangeError("offset does not agree with timezone for the given date/time"_s)); + + // Step 12: Return ? DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation). + // Note: getEpochNanosecondsFor recomputes possibleEpochNs; functionally equivalent. + return getEpochNanosecondsFor(timeZone, date, time, disambiguation); +} + +// GetStartOfDay — temporal_rs: TimeZone::get_start_of_day(iso_date, provider) +// https://tc39.es/proposal-temporal/#sec-temporal-getstartofday +TemporalResult getStartOfDay(const TimeZone& tz, ISO8601::PlainDate date) +{ + // Step 1: isoDateTime = CombineISODateAndTimeRecord(isoDate, MidnightTimeRecord()). + ISO8601::PlainTime midnight; + // Step 2: possibleEpochNs = GetPossibleEpochNanoseconds(timeZone, isoDateTime). + auto possible = getPossibleEpochNanosecondsFor(tz, date, midnight); + if (!possible) + return makeUnexpected(possible.error()); + // Step 3: If possibleEpochNs is not empty, return possibleEpochNs[0]. + if (!isGap(*possible)) { + auto candidate = epochCandidates(*possible)[0]; + // Validate: the start-of-day epoch must be within Temporal range. + // e.g. midnight of +275760-09-13 in America/Vancouver = UTC+7h > max epoch. + if (!candidate.isValid()) + return makeUnexpected(rangeError("start of day is outside the representable range of Temporal.ZonedDateTime"_s)); + return candidate; + } + // Step 4: Assert IsOffsetTimeZoneIdentifier(timeZone) is false (gap is only possible for IANA zones). + ASSERT(tz.isID()); + // Step 5: possibleEpochNsAfter = first instant after the gap transition. + // We find it via: getEpochNanosecondsFor(Earlier) -> last instant before gap, + // then getTimeZoneTransition(Next) -> first instant after gap. + auto beforeGap = getEpochNanosecondsFor(tz, date, midnight, TemporalDisambiguation::Earlier); + if (!beforeGap) + return makeUnexpected(beforeGap.error()); + auto transition = getTimeZoneTransition(tz, *beforeGap, TransitionDirection::Next); + if (!transition) + return makeUnexpected(transition.error()); + if (!transition->has_value()) + return makeUnexpected(rangeError("no start of day: time zone has no future transitions"_s)); + // Step 6: Assert: The number of elements in possibleEpochNsAfter = 1. + // Step 7: Return the sole element of possibleEpochNsAfter. + return transition->value(); +} + +// DifferenceZonedDateTime — temporal_rs: ZonedDateTime::diff_zoned_datetime inner path +// https://tc39.es/proposal-temporal/#sec-temporal-differencezoneddatetime +static TemporalResult differenceZonedDateTime(ISO8601::ExactTime ns1, ISO8601::ExactTime ns2, const TimeZone& timeZone, CalendarID calendarId, TemporalUnit largestUnit) +{ + Int128 nsA = ns1.epochNanoseconds(); + Int128 nsB = ns2.epochNanoseconds(); + + // Step 1: If ns1 = ns2, return zero. + if (nsA == nsB) + return ISO8601::InternalDuration::combineDateAndTimeDuration(ISO8601::Duration(), 0); + + // Steps 2–3: GetISODateTimeFor(timeZone, ns1/ns2). (split: getOffsetNanosecondsFor + exactTimeToLocalDateAndTime) + auto offset1Result = getOffsetNanosecondsFor(timeZone, ns1); + if (!offset1Result) + return makeUnexpected(offset1Result.error()); + auto offset2Result = getOffsetNanosecondsFor(timeZone, ns2); + if (!offset2Result) + return makeUnexpected(offset2Result.error()); + ISO8601::PlainDate startDate, endDate; + ISO8601::PlainTime startTime, endTime; + exactTimeToLocalDateAndTime(ns1, *offset1Result, startDate, startTime); + exactTimeToLocalDateAndTime(ns2, *offset2Result, endDate, endTime); + + // Step 4: If startDate = endDate -> timeDuration = ns2 - ns1, return. + if (!isoDateCompare(startDate, endDate)) + return ISO8601::InternalDuration::combineDateAndTimeDuration(ISO8601::Duration(), nsB - nsA); + + // Step 5: sign = (ns2 - ns1 < 0) ? 1 : -1. + int32_t sign = (nsB - nsA < Int128 { 0 }) ? 1 : -1; + // Step 6: maxDayCorrection = (sign=-1) ? 2 : 1. + int32_t maxDayCorrection = (sign == -1) ? 2 : 1; + // Step 7: dayCorrection = 0. + int32_t dayCorrection = 0; + // Step 8: timeDuration = DifferenceTime(startTime, endTime). + Int128 timeDiff = timeDurationFromComponents( + static_cast(endTime.hour()) - static_cast(startTime.hour()), + static_cast(endTime.minute()) - static_cast(startTime.minute()), + static_cast(endTime.second()) - static_cast(startTime.second()), + static_cast(endTime.millisecond()) - static_cast(startTime.millisecond()), + static_cast(endTime.microsecond()) - static_cast(startTime.microsecond()), + static_cast(endTime.nanosecond()) - static_cast(startTime.nanosecond())); + // Step 9: If TimeDurationSign(timeDuration) = sign -> dayCorrection++. + int32_t timeSign = (timeDiff < 0) ? -1 : (timeDiff > 0) ? 1 : 0; + if (timeSign == sign) + dayCorrection++; + + // Steps 10-12: day-correction loop. + ISO8601::PlainDate intermediateDate = endDate; + Int128 adjustedTimeDiff = timeDiff; + bool success = false; + while (dayCorrection <= maxDayCorrection && !success) { + // Step 11.a: intermediateDate = AddDaysToISODate(endDate, dayCorrection × sign). + intermediateDate = balanceISODate(endDate.year(), static_cast(endDate.month()), + static_cast(endDate.day()) + dayCorrection * sign); + // Step 11.c: intermediateNs = GetEpochNanosecondsFor(timeZone, intermediateDateTime, compatible). + auto intermediateNsResult = getEpochNanosecondsFor(timeZone, intermediateDate, startTime, TemporalDisambiguation::Compatible); + if (!intermediateNsResult) + return makeUnexpected(intermediateNsResult.error()); + // Step 11.d: timeDuration = ns2 - intermediateNs. + adjustedTimeDiff = nsB - intermediateNsResult->epochNanoseconds(); + int32_t adjTimeSign = (adjustedTimeDiff < 0) ? -1 : (adjustedTimeDiff > 0) ? 1 : 0; + // Step 11.f: If sign ≠ timeSign -> success. + if (sign != adjTimeSign) + success = true; + dayCorrection++; + } + // Step 13: Assert success. + ASSERT(success); + + // Step 14: dateLargestUnit = max(largestUnit, day). + TemporalUnit dateLargestUnit = (largestUnit > TemporalUnit::Day) ? TemporalUnit::Day : largestUnit; + // Step 15: dateDifference = CalendarDateUntil(startDate, intermediateDate, dateLargestUnit). + auto dateDiffResult = TemporalCore::calendarDateUntil(calendarId, startDate, intermediateDate, dateLargestUnit); + if (!dateDiffResult) + return makeUnexpected(dateDiffResult.error()); + auto& dateDiff = *dateDiffResult; + + // Step 16: Return CombineDateAndTimeDuration(dateDifference, timeDuration). + ISO8601::Duration datePart(static_cast(dateDiff.years()), static_cast(dateDiff.months()), + static_cast(dateDiff.weeks()), static_cast(dateDiff.days()), 0, 0, 0, 0, 0, 0); + return ISO8601::InternalDuration::combineDateAndTimeDuration(datePart, adjustedTimeDiff); +} + +// DifferenceZonedDateTimeWithRounding — temporal_rs: ZonedDateTime::diff_zoned_datetime (src/builtins/core/zoned_date_time.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-differencezoneddatetimewithrounding +TemporalResult differenceZonedDateTimeWithRounding( + ISO8601::ExactTime ns1, + ISO8601::ExactTime ns2, + const TimeZone& timeZone, + TemporalUnit largestUnit, + TemporalUnit smallestUnit, + RoundingMode roundingMode, + double increment, + CalendarID calendarId) +{ + Int128 nsA = ns1.epochNanoseconds(); + Int128 nsB = ns2.epochNanoseconds(); + + // Step 1: If largestUnit is time category -> DifferenceInstant (pure nanosecond arithmetic). + if (largestUnit > TemporalUnit::Day) { + Int128 nsTotal = nsB - nsA; + ISO8601::InternalDuration internalDuration = ISO8601::InternalDuration::combineDateAndTimeDuration(ISO8601::Duration(), nsTotal); + // Step 3: If smallestUnit=nanosecond and increment=1 -> return difference without rounding. + if (smallestUnit != TemporalUnit::Nanosecond || increment != 1) { + Int128 roundedTime = roundNumberToIncrementInt128(internalDuration.time(), + lengthInNanoseconds(smallestUnit) * (Int128)std::trunc(increment), roundingMode); + internalDuration = ISO8601::InternalDuration::combineDateAndTimeDuration(ISO8601::Duration(), roundedTime); + } + return temporalDurationFromInternal(internalDuration, largestUnit); + } + + // Step 2: difference = DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, largestUnit). + auto differenceResult = differenceZonedDateTime(ns1, ns2, timeZone, calendarId, largestUnit); + if (!differenceResult) + return makeUnexpected(differenceResult.error()); + auto internalDuration = *differenceResult; + + // Step 3: If smallestUnit=nanosecond and increment=1 -> return difference. + if (smallestUnit == TemporalUnit::Nanosecond && increment == 1) + return temporalDurationFromInternal(internalDuration, largestUnit); + + // Step 4: dateTime = GetISODateTimeFor(timeZone, ns1). (split: getOffsetNanosecondsFor + exactTimeToLocalDateAndTime) + auto offset1 = getOffsetNanosecondsFor(timeZone, ns1); + if (!offset1) + return makeUnexpected(offset1.error()); + ISO8601::PlainDate startDate; + ISO8601::PlainTime startTime; + exactTimeToLocalDateAndTime(ns1, *offset1, startDate, startTime); + + // Step 5: Return RoundRelativeDuration(difference, ns1, ns2, dateTime, ...). + auto roundResult = roundRelativeDuration(internalDuration, nsA, nsB, startDate, startTime, + largestUnit, increment, smallestUnit, roundingMode, &timeZone, calendarId); + if (!roundResult) + return makeUnexpected(roundResult.error()); + return temporalDurationFromInternal(internalDuration, largestUnit); +} + +} // namespace TemporalCore +} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.h b/Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.h new file mode 100644 index 000000000000..29e8403ed26c --- /dev/null +++ b/Source/JavaScriptCore/runtime/temporal/core/ZonedDateTimeCore.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// JSC Temporal Core — ZonedDateTime algorithms +// temporal_rs reference: src/builtins/core/zoned_date_time.rs +// Last synced: v0.2.3 + +#include +#include +#include +#include +#include + +namespace JSC { +namespace TemporalCore { + +TemporalResult JS_EXPORT_PRIVATE interpretISODateTimeOffset( + const ISO8601::PlainDate&, + const ISO8601::PlainTime&, + bool useStartOfDay, + OffsetBehaviour, + TemporalOffsetDisambiguation, + int64_t inlineOffsetNs, + bool offsetHasSubMinutePrecision, + const TimeZone&, + TemporalDisambiguation); + +TemporalResult JS_EXPORT_PRIVATE getStartOfDay(const TimeZone&, ISO8601::PlainDate); + +TemporalResult JS_EXPORT_PRIVATE differenceZonedDateTimeWithRounding( + ISO8601::ExactTime ns1, + ISO8601::ExactTime ns2, + const TimeZone&, + TemporalUnit largestUnit, + TemporalUnit smallestUnit, + RoundingMode, + double increment, + CalendarID = iso8601CalendarID()); + +} // namespace TemporalCore +} // namespace JSC From 93e7a14ff5bc2aad775dd5b2d53cc67499adac10 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Thu, 14 May 2026 09:49:32 -0700 Subject: [PATCH 027/424] Twitch.tv may stall at ad transition during live streaming. https://bugs.webkit.org/show_bug.cgi?id=314306 rdar://176446372 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Jer Noble Avoid buffered-range gap when a reordered B-frame tail slightly overshoots the next buffered sample To play an ad, Twitch will insert video content over an existing MediaSource. The last video frame's timestamp of the ad is then adjusted to provide continuous playback with the live content. At the tail of such fMP4 stream, sometimes the last frame to be visible is a b-frame (pts > dts); the trun.sample_duration of this last-in- presentation-order sample is a decode-to-next-decode placeholder rather than a real presentation duration as there's no sample to present after. The sample declared frame_end (pts + duration) can overshoot the next buffered sample's pts by a few milliseconds — not a true overlap, just a carryover from the decode grid. Taken at face value, MSE "Coded Frame Processing" step 1.14 treats the overshoot as a real overlap and removes the overlapped frame, and step 1.15 extends that removal in dts order up to the next sync sample. The result is a gap in the buffered range that stalls playback. Observed repeatedly at Twitch content-to-ad splice boundaries on Safari and Firefox (Chrome tolerates the overshoot so does not stall). Fix: if the per-track presentation tail is a B-frame and its overshoot of the next buffered sample's pts is less than timeFudgeFactor(), treat the overshoot as placeholder noise and shift the overlapped sample forward by the overshoot — pts and dts shift equally, duration shrinks, presentationEndTime is preserved. Larger overshoots are kept on the default path. Implementation: - Add MediaSample::createCopyWithAdjustedStartTime(const MediaTime& offset): new virtual producing an immutable copy with pts/dts shifted forward by `offset` (clamped to duration) and duration correspondingly shrunk. Overridden in MediaSampleAVFObjC (via CMSampleBufferCreateCopyWithNewTiming, applying the offset to each sub-sample's timing so multi-sample buffers are preserved), MediaSampleGStreamer (via gst_buffer_copy + timestamp update on the copy to avoid corrupting the original's shared GstBuffer), and MockMediaSample. - Add TrackBuffer::adjustSampleStartTime(original, offset): creates the adjusted copy via createCopyWithAdjustedStartTime, subtracts the original's [pts, presentationEndTime) from m_buffered, swaps the sample in the SampleMap and m_decodeQueue (if present), and re-adds the adjusted range. Keeping m_decodeQueue in sync with the SampleMap is required so a subsequent step-1.14/1.15 removeSamples cascade — which erases m_decodeQueue by the current decode key — matches the adjusted entry rather than leaving the pre-adjustment key behind as an orphan. - Add SampleMap::replaceSample: swap an already-buffered sample for an adjusted copy carrying nearly-identical keys. Uses erase() + insert with the erase-return iterator as the insertion hint in both the presentation-order and decode-order submaps, keeping the operation to two map ops rather than two full find+erase+insert cycles. - In SourceBufferPrivate::processPendingMediaSamples, compute the per-track presentation tail (sample with the highest presentationEndTime in each track) and pass isPresentationTail to processMediaSample. Per-track keying is required: an audio sample's presentationEndTime often exceeds the last video sample's, and a global-max tail would deny the video tail its shift-eligibility. - In processMediaSample, inside the step-1.14 overlap handling: if the incoming sample is isPresentationTail and reordered (pts > dts), search the buffered samples whose pts lies in (presentationTimestamp, frameEndTimestamp) and within timeFudgeFactor of frameEndTimestamp; if one is found and is not fully overwritten by the incoming frame, shift it forward via TrackBuffer::adjustSampleStartTime. Fully- overwritten samples are left to the default 1.14 removal. Test: media/media-source/media-source-append-b-frame-tail-overshoot.html * LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot-expected.txt: Added. * LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot.html: Added. Canonical link: https://commits.webkit.org/313253@main --- ...append-b-frame-tail-overshoot-expected.txt | 16 +++ ...-source-append-b-frame-tail-overshoot.html | 107 ++++++++++++++++++ .../WebCore/Modules/mediasource/SampleMap.cpp | 31 +++++ .../WebCore/Modules/mediasource/SampleMap.h | 1 + Source/WebCore/platform/MediaSample.h | 13 +++ .../platform/graphics/SourceBufferPrivate.cpp | 48 +++++++- .../platform/graphics/SourceBufferPrivate.h | 7 +- .../WebCore/platform/graphics/TrackBuffer.cpp | 37 ++++++ .../WebCore/platform/graphics/TrackBuffer.h | 5 + .../avfoundation/objc/MediaSampleAVFObjC.h | 1 + .../avfoundation/objc/MediaSampleAVFObjC.mm | 35 ++++++ .../gstreamer/MediaSampleGStreamer.cpp | 29 +++++ .../graphics/gstreamer/MediaSampleGStreamer.h | 1 + .../platform/mock/mediasource/MockBox.h | 1 + .../mediasource/MockSourceBufferPrivate.cpp | 11 ++ 15 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot-expected.txt create mode 100644 LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot.html diff --git a/LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot-expected.txt b/LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot-expected.txt new file mode 100644 index 000000000000..54dc2ffc5422 --- /dev/null +++ b/LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot-expected.txt @@ -0,0 +1,16 @@ + +RUN(video.src = URL.createObjectURL(source)) +EVENT(sourceopen) +RUN(sourceBuffer = source.addSourceBuffer('video/mp4; codecs="avc1.4d401e"')) +RUN(sourceBuffer.appendBuffer(MP4.concat(init, segment1))) +EVENT(updateend) +EXPECTED (sourceBuffer.buffered.length == '1') OK +EXPECTED (sourceBuffer.buffered.start(0) == '0.1') OK +EXPECTED (sourceBuffer.buffered.end(0) == '0.2') OK +RUN(sourceBuffer.appendBuffer(segment2)) +EVENT(updateend) +EXPECTED (sourceBuffer.buffered.length == '1') OK +EXPECTED (sourceBuffer.buffered.start(0) == '0') OK +EXPECTED (sourceBuffer.buffered.end(0) == '0.2') OK +END OF TEST + diff --git a/LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot.html b/LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot.html new file mode 100644 index 000000000000..0a6323bcf01c --- /dev/null +++ b/LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot.html @@ -0,0 +1,107 @@ + + + + media-source-append-b-frame-tail-overshoot + + + + + + + + diff --git a/Source/WebCore/Modules/mediasource/SampleMap.cpp b/Source/WebCore/Modules/mediasource/SampleMap.cpp index a29b107482ed..a4e9c58ba58b 100644 --- a/Source/WebCore/Modules/mediasource/SampleMap.cpp +++ b/Source/WebCore/Modules/mediasource/SampleMap.cpp @@ -129,6 +129,37 @@ void SampleMap::removeSample(const MediaSample& sample) decodeOrder().m_samples.erase(decodeKey); } +void SampleMap::replaceSample(const MediaSample& original, Ref&& replacement) +{ + // Swap an already-buffered sample for a replacement carrying nearly-identical + // keys (typically a small timing shift). The erase() return iterator is a + // valid insertion-point hint for the new entry as long as the replacement's + // key still sorts just ahead of the erased position; this holds for the + // small shifts that `createCopyWithAdjustedStartTime` produces. A wrong + // hint is merely a performance regression, not a correctness issue. + // The replacement carries the same payload as the original, so the total + // size is unchanged. + ASSERT(original.sizeInBytes() == replacement->sizeInBytes()); + ASSERT(original.trackID() == replacement->trackID()); + + MediaTime originalPts = original.presentationTime(); + auto originalDecodeKey = DecodeOrderSampleMap::KeyType(original.decodeTime(), originalPts); + MediaTime replacementPts = replacement->presentationTime(); + auto replacementDecodeKey = DecodeOrderSampleMap::KeyType(replacement->decodeTime(), replacementPts); + + auto& presentationSamples = presentationOrder().m_samples; + if (auto pIt = presentationSamples.find(originalPts); pIt != presentationSamples.end()) { + auto hint = presentationSamples.erase(pIt); + presentationSamples.insert(hint, { replacementPts, replacement.copyRef() }); + } + + auto& decodeSamples = decodeOrder().m_samples; + if (auto dIt = decodeSamples.find(originalDecodeKey); dIt != decodeSamples.end()) { + auto hint = decodeSamples.erase(dIt); + decodeSamples.insert(hint, { replacementDecodeKey, WTF::move(replacement) }); + } +} + PresentationOrderSampleMap::iterator PresentationOrderSampleMap::findSampleWithPresentationTime(const MediaTime& time) LIFETIME_BOUND { auto range = m_samples.equal_range(time); diff --git a/Source/WebCore/Modules/mediasource/SampleMap.h b/Source/WebCore/Modules/mediasource/SampleMap.h index 382fa1ff02b7..6ae33c70baf2 100644 --- a/Source/WebCore/Modules/mediasource/SampleMap.h +++ b/Source/WebCore/Modules/mediasource/SampleMap.h @@ -118,6 +118,7 @@ class SampleMap { WEBCORE_EXPORT void clear(); WEBCORE_EXPORT void addSample(Ref&&); WEBCORE_EXPORT void removeSample(const MediaSample&); + WEBCORE_EXPORT void replaceSample(const MediaSample& original, Ref&& replacement); size_t sizeInBytes() const { return m_totalSize; } template diff --git a/Source/WebCore/platform/MediaSample.h b/Source/WebCore/platform/MediaSample.h index 9048ea347108..b867e8738454 100644 --- a/Source/WebCore/platform/MediaSample.h +++ b/Source/WebCore/platform/MediaSample.h @@ -100,6 +100,19 @@ class MediaSample : public ThreadSafeRefCounted { } virtual Ref createNonDisplayingCopy() const = 0; + // Return a new sample whose presentation time, decode time, and duration have been adjusted by + // shifting start forward by `offset` while keeping presentationEndTime() unchanged: + // pts' = pts + offset, dts' = dts + offset, duration' = duration - offset. `offset` is clamped + // to duration; if offset == duration the shifted copy has zero duration (callers should prefer + // removal in that case). Payload, format description, and attachments are copied unchanged. + // Callers use this instead of mutating an existing sample, which would alter an object + // already referenced by SampleMap keys / enqueued decoders. + virtual Ref createCopyWithAdjustedStartTime(const MediaTime& /*offset*/) const + { + ASSERT_NOT_REACHED(); + return const_cast(*this); + } + enum SampleFlags { None = 0, IsSync = 1 << 0, diff --git a/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp b/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp index ef388609a0bf..f20fb38ea48f 100644 --- a/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp +++ b/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp @@ -967,6 +967,13 @@ void SourceBufferPrivate::didReceiveSample(Ref&& sample) assertIsCurrent(m_dispatcher.get()); DEBUG_LOG(LOGIDENTIFIER, sample.get()); + // Only video tracks produce B-frame reordering (pts > dts); processMediaSample's isBFrame + // gate never fires for audio, so audio tails are not tracked. + if (!sample->presentationSize().isEmpty()) { + auto [entry, inserted] = m_presentationTailPerTrack.try_emplace(sample->trackID(), sample.ptr()); + if (!inserted && sample->presentationEndTime() > protect(entry->second)->presentationEndTime()) + entry->second = sample.ptr(); + } m_pendingSamples.append(WTF::move(sample)); } @@ -1044,7 +1051,8 @@ void SourceBufferPrivate::processPendingMediaSamples() if (m_pendingSamples.isEmpty()) return; auto samples = std::exchange(m_pendingSamples, { }); - m_currentAppendProcessing = protect(m_currentAppendProcessing)->whenSettled(m_dispatcher, [weakThis = ThreadSafeWeakPtr { *this }, samples = WTF::move(samples), abortCount = m_abortCount.load()](auto result) mutable { + auto presentationTailPerTrack = std::exchange(m_presentationTailPerTrack, { }); + m_currentAppendProcessing = protect(m_currentAppendProcessing)->whenSettled(m_dispatcher, [weakThis = ThreadSafeWeakPtr { *this }, samples = WTF::move(samples), presentationTailPerTrack = WTF::move(presentationTailPerTrack), abortCount = m_abortCount.load()](auto result) mutable { RefPtr protectedThis = weakThis.get(); if (!protectedThis || !result) return MediaPromise::createAndReject(!result ? result.error() : PlatformMediaError::BufferRemoved); @@ -1056,14 +1064,16 @@ void SourceBufferPrivate::processPendingMediaSamples() return MediaPromise::createAndReject(PlatformMediaError::BufferRemoved); for (auto& sample : samples) { - if (!protectedThis->processMediaSample(*client, WTF::move(sample))) + auto it = presentationTailPerTrack.find(sample->trackID()); + bool isPresentationTail = it != presentationTailPerTrack.end() && sample.ptr() == it->second; + if (!protectedThis->processMediaSample(*client, WTF::move(sample), isPresentationTail)) return MediaPromise::createAndReject(PlatformMediaError::ParsingError); } return MediaPromise::createAndResolve(); }); } -bool SourceBufferPrivate::processMediaSample(SourceBufferPrivateClient& client, Ref&& sample) +bool SourceBufferPrivate::processMediaSample(SourceBufferPrivateClient& client, Ref&& sample, bool isPresentationTail) { assertIsCurrent(m_dispatcher.get()); @@ -1400,6 +1410,38 @@ bool SourceBufferPrivate::processMediaSample(SourceBufferPrivateClient& client, MediaTime eraseBeginTime = trackBuffer.highestPresentationTimestamp(); MediaTime eraseEndTime = frameEndTimestamp - contiguousFrameTolerance; + // If the incoming sample is the **presentation tail for this track** AND a + // reordered frame (B-frame: pts > dts), its declared frame_end may be a trun + // decode-to-next-decode placeholder that overshoots the next buffered sample's + // pts by a small margin. Taken at face value, step 1.14 treats the overshoot as + // a real overlap and removes the overlapped frame, leaving a gap in the buffered + // range that stalls playback. If the overshoot is less than timeFudgeFactor, + // attribute it to the trun placeholder rather than a genuine overlap and shift + // the overlapped frame forward by the overshoot: pts and dts shift equally, + // duration shrinks, presentationEndTime is preserved. Any larger overshoot is + // treated as a real overlap and left to the default path. + bool isBFrame = sample->presentationTime() > sample->decodeTime(); + if (isPresentationTail && isBFrame) { + MediaTime fudge = PlatformTimeRanges::timeFudgeFactor(); + if (frameEndTimestamp > fudge) { + auto it = trackBuffer.samples().presentationOrder().findSampleStartingOnOrAfterPresentationTime(frameEndTimestamp - fudge); + auto presentationEnd = trackBuffer.samples().presentationOrder().end(); + for (; it != presentationEnd && it->first < frameEndTimestamp; ++it) { + if (it->first <= presentationTimestamp) + continue; + // Overlapped frame: pts ∈ (presentationTimestamp, frameEndTimestamp) + // and within timeFudgeFactor of frame_end. If fully inside the erase + // range, leave it to the default removal. Otherwise shift forward. + Ref original = it->second; + if (original->presentationEndTime() <= frameEndTimestamp) + break; + MediaTime offset = frameEndTimestamp - original->presentationTime(); + trackBuffer.adjustSampleStartTime(original.get(), offset); + break; + } + } + } + if (eraseEndTime <= eraseBeginTime) break; diff --git a/Source/WebCore/platform/graphics/SourceBufferPrivate.h b/Source/WebCore/platform/graphics/SourceBufferPrivate.h index d3884c92510f..4fa1b3991c66 100644 --- a/Source/WebCore/platform/graphics/SourceBufferPrivate.h +++ b/Source/WebCore/platform/graphics/SourceBufferPrivate.h @@ -266,7 +266,7 @@ class SourceBufferPrivate std::atomic m_abortCount { 0 }; void processPendingMediaSamples(); - bool processMediaSample(SourceBufferPrivateClient&, Ref&&); + bool processMediaSample(SourceBufferPrivateClient&, Ref&&, bool isPresentationTail); enum class ComputeEvictionDataRule { Default, @@ -276,6 +276,11 @@ class SourceBufferPrivate using SamplesVector = Vector>; SamplesVector m_pendingSamples WTF_GUARDED_BY_CAPABILITY(m_dispatcher.get()); + // Per video track, the pending sample with the highest presentationEndTime. Maintained + // incrementally in didReceiveSample and drained in lockstep with m_pendingSamples so + // processPendingMediaSamples does not need to rescan the batch. Raw pointers are valid + // while the owning Ref lives in m_pendingSamples. + StdUnorderedMap m_presentationTailPerTrack WTF_GUARDED_BY_CAPABILITY(m_dispatcher.get()); Ref m_currentAppendProcessing WTF_GUARDED_BY_CAPABILITY(m_dispatcher.get()) { MediaPromise::createAndResolve() }; MediaTime m_appendWindowStart WTF_GUARDED_BY_LOCK(m_lock) { MediaTime::zeroTime() }; diff --git a/Source/WebCore/platform/graphics/TrackBuffer.cpp b/Source/WebCore/platform/graphics/TrackBuffer.cpp index 98bd6b7e3aba..f5d9ca621f9c 100644 --- a/Source/WebCore/platform/graphics/TrackBuffer.cpp +++ b/Source/WebCore/platform/graphics/TrackBuffer.cpp @@ -84,6 +84,43 @@ void TrackBuffer::addBufferedRange(const MediaTime& start, const MediaTime& end, m_buffered.add(start, end, addTimeRangeOption); } +void TrackBuffer::adjustSampleStartTime(MediaSample& original, const MediaTime& offset) +{ + // Replace an already-buffered sample with a copy whose presentation and + // decode timestamps are shifted forward by `offset` (duration shrinks + // correspondingly; presentationEndTime is preserved; payload is unchanged). + // + // Both the SampleMap and m_decodeQueue need to be kept consistent: later + // removals look samples up by their current decode key, and leaving the + // pre-adjustment entry in the queue would orphan it. + Ref replacement = original.createCopyWithAdjustedStartTime(offset); + + MediaTime originalStart = original.presentationTime(); + MediaTime originalEnd = original.presentationEndTime(); + DecodeOrderSampleMap::KeyType originalDecodeKey(original.decodeTime(), original.presentationTime()); + + MediaTime replacementStart = replacement->presentationTime(); + MediaTime replacementEnd = replacement->presentationEndTime(); + DecodeOrderSampleMap::KeyType replacementDecodeKey(replacement->decodeTime(), replacementStart); + + PlatformTimeRanges invertedRange(originalStart, originalEnd); + invertedRange.invert(); + m_buffered.intersectWith(invertedRange); + + m_samples.replaceSample(original, replacement.copyRef()); + + addBufferedRange(replacementStart, replacementEnd, AddTimeRangeOption::EliminateSmallGaps); + + // Since `offset` moves the key forward by at most timeFudgeFactor (much + // smaller than the typical inter-sample DTS gap), the iterator returned by + // erase() is a valid insertion-point hint for the adjusted entry. + auto queueIt = m_decodeQueue.find(originalDecodeKey); + if (queueIt != m_decodeQueue.end()) { + auto hint = m_decodeQueue.erase(queueIt); + m_decodeQueue.insert(hint, { replacementDecodeKey, WTF::move(replacement) }); + } +} + void TrackBuffer::addSample(MediaSample& sample) { m_samples.addSample(sample); diff --git a/Source/WebCore/platform/graphics/TrackBuffer.h b/Source/WebCore/platform/graphics/TrackBuffer.h index 37c27cb1c6a8..f5379106b386 100644 --- a/Source/WebCore/platform/graphics/TrackBuffer.h +++ b/Source/WebCore/platform/graphics/TrackBuffer.h @@ -52,6 +52,11 @@ class TrackBuffer final MediaTime NODELETE maximumBufferedTime() const; void addBufferedRange(const MediaTime& start, const MediaTime& end, AddTimeRangeOption = AddTimeRangeOption::None); void addSample(MediaSample&); + // Replace an already-buffered sample with a copy whose presentation and + // decode timestamps are shifted forward by `offset` (duration shrinks + // accordingly; presentationEndTime is preserved). Updates both the + // SampleMap and m_decodeQueue, and adjusts m_buffered to match. + void adjustSampleStartTime(MediaSample& original, const MediaTime& offset); bool reenqueueMediaForTime(const MediaTime&, const MediaTime& timeFudgeFactor, bool isEnded = false); MediaTime findSeekTimeForTargetTime(const MediaTime& targetTime, const MediaTime& negativeThreshold, const MediaTime& positiveThreshold); diff --git a/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.h b/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.h index 7bcb8066e714..cc05cab6b420 100644 --- a/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.h +++ b/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.h @@ -66,6 +66,7 @@ class MediaSampleAVFObjC : public MediaSample { WEBCORE_EXPORT std::pair, RefPtr> divide(const MediaTime& presentationTime, UseEndTime) override; WEBCORE_EXPORT Ref createNonDisplayingCopy() const override; + Ref createCopyWithAdjustedStartTime(const MediaTime& offset) const override; CMSampleBufferRef sampleBuffer() const { return m_sample.get(); } diff --git a/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.mm b/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.mm index 613bed1169e2..84ce858f080d 100644 --- a/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.mm +++ b/Source/WebCore/platform/graphics/avfoundation/objc/MediaSampleAVFObjC.mm @@ -300,6 +300,41 @@ static bool isCMSampleBufferAttachmentNonDisplaying(CFDictionaryRef attachmentDi return { MediaSampleAVFObjC::create(sampleBefore.get(), m_id), MediaSampleAVFObjC::create(sampleAfter.get(), m_id) }; } +Ref MediaSampleAVFObjC::createCopyWithAdjustedStartTime(const MediaTime& offset) const +{ + MediaTime clampedOffset = std::max(MediaTime::zeroTime(), std::min(offset, duration())); + + CMItemCount itemCount = 0; + if (noErr != PAL::CMSampleBufferGetSampleTimingInfoArray(m_sample.get(), 0, nullptr, &itemCount)) + return const_cast(*this); + + Vector timingInfoArray; + timingInfoArray.grow(itemCount); + if (noErr != PAL::CMSampleBufferGetSampleTimingInfoArray(m_sample.get(), itemCount, timingInfoArray.mutableSpan().data(), nullptr)) + return const_cast(*this); + + CMTime cmOffset = PAL::toCMTime(clampedOffset); + + for (auto& timing : timingInfoArray) { + if (!CMTIME_IS_INVALID(timing.presentationTimeStamp)) + timing.presentationTimeStamp = PAL::CMTimeAdd(timing.presentationTimeStamp, cmOffset); + if (!CMTIME_IS_INVALID(timing.decodeTimeStamp)) + timing.decodeTimeStamp = PAL::CMTimeAdd(timing.decodeTimeStamp, cmOffset); + if (!CMTIME_IS_INVALID(timing.duration)) { + CMTime newDuration = PAL::CMTimeSubtract(timing.duration, cmOffset); + if (PAL::CMTimeCompare(newDuration, PAL::kCMTimeZero) < 0) + newDuration = PAL::kCMTimeZero; + timing.duration = newDuration; + } + } + + CMSampleBufferRef newSampleBuffer = nullptr; + if (noErr != PAL::CMSampleBufferCreateCopyWithNewTiming(kCFAllocatorDefault, m_sample.get(), itemCount, timingInfoArray.span().data(), &newSampleBuffer) || !newSampleBuffer) + return const_cast(*this); + + return MediaSampleAVFObjC::create(adoptCF(newSampleBuffer).get(), m_id); +} + Ref MediaSampleAVFObjC::createNonDisplayingCopy() const { CMSampleBufferRef newSampleBuffer = 0; diff --git a/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.cpp b/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.cpp index c235dac4114b..a927534bf0e1 100644 --- a/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.cpp +++ b/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.cpp @@ -199,6 +199,35 @@ Ref MediaSampleGStreamer::createNonDisplayingCopy() const return adoptRef(*new MediaSampleGStreamer(WTF::move(sample), m_presentationSize, m_trackId)); } +Ref MediaSampleGStreamer::createCopyWithAdjustedStartTime(const MediaTime& offset) const +{ + MediaTime clampedOffset = std::max(MediaTime::zeroTime(), std::min(offset, m_duration)); + + MediaTime newPresentationTime = m_pts + clampedOffset; + MediaTime newDecodeTime = m_dts + clampedOffset; + MediaTime adjustedDuration = m_duration - clampedOffset; + + if (!m_sample) + return createFakeSample(nullptr, newPresentationTime, newDecodeTime, adjustedDuration, m_presentationSize, m_trackId); + + GRefPtr newSample = adoptGRef(gst_sample_copy(m_sample.get())); + + GRefPtr buffer = gst_sample_get_buffer(newSample.get()); + if (!buffer) [[unlikely]] + return createFakeSample(nullptr, newPresentationTime, newDecodeTime, adjustedDuration, m_presentationSize, m_trackId); + + auto newBuffer = adoptGRef(gst_buffer_make_writable(buffer.leakRef())); + GST_BUFFER_PTS(newBuffer.get()) = toGstClockTime(newPresentationTime); + GST_BUFFER_DTS(newBuffer.get()) = toGstClockTime(newDecodeTime); + GST_BUFFER_DURATION(newBuffer.get()) = toGstClockTime(adjustedDuration); + newSample = adoptGRef(gst_sample_make_writable(newSample.leakRef())); + gst_sample_set_buffer(newSample.get(), newBuffer.get()); + + Ref copy = adoptRef(*new MediaSampleGStreamer(WTF::move(newSample), m_presentationSize, m_trackId)); + copy->m_flags = m_flags; + return copy; +} + void MediaSampleGStreamer::dump(PrintStream& out) const { out.print("{PTS(", presentationTime(), "), DTS(", decodeTime(), "), duration(", duration(), "), flags("); diff --git a/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.h b/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.h index 20f8293a0a47..209c90c8e73b 100644 --- a/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.h +++ b/Source/WebCore/platform/graphics/gstreamer/MediaSampleGStreamer.h @@ -49,6 +49,7 @@ class MediaSampleGStreamer : public MediaSample { void offsetTimestampsBy(const MediaTime&) override; void setTimestamps(const MediaTime&, const MediaTime&) override; Ref createNonDisplayingCopy() const override; + Ref createCopyWithAdjustedStartTime(const MediaTime& offset) const override; SampleFlags flags() const override { return m_flags; } PlatformSample platformSample() const override; Type type() const override { return Type::GStreamerSample; } diff --git a/Source/WebCore/platform/mock/mediasource/MockBox.h b/Source/WebCore/platform/mock/mediasource/MockBox.h index 58c245a0c54c..b9fd053d0777 100644 --- a/Source/WebCore/platform/mock/mediasource/MockBox.h +++ b/Source/WebCore/platform/mock/mediasource/MockBox.h @@ -140,6 +140,7 @@ class MockSampleBox final : public MockBox { m_presentationTimestamp = presentationTimestamp; m_decodeTimestamp = decodeTimestamp; } + void setDuration(const MediaTime& duration) { m_duration = duration; } void clearFlag(uint8_t flag) { m_flags &= ~flag; } void setFlag(uint8_t flag) { m_flags |= flag; } diff --git a/Source/WebCore/platform/mock/mediasource/MockSourceBufferPrivate.cpp b/Source/WebCore/platform/mock/mediasource/MockSourceBufferPrivate.cpp index 3d29749f66ba..48c143b02174 100644 --- a/Source/WebCore/platform/mock/mediasource/MockSourceBufferPrivate.cpp +++ b/Source/WebCore/platform/mock/mediasource/MockSourceBufferPrivate.cpp @@ -69,6 +69,7 @@ class MockMediaSample final : public MediaSample { void offsetTimestampsBy(const MediaTime& offset) override { m_box.offsetTimestampsBy(offset); } void setTimestamps(const MediaTime& presentationTimestamp, const MediaTime& decodeTimestamp) override { m_box.setTimestamps(presentationTimestamp, decodeTimestamp); } Ref createNonDisplayingCopy() const override; + Ref createCopyWithAdjustedStartTime(const MediaTime& offset) const override; unsigned NODELETE generation() const { return m_box.generation(); } @@ -103,6 +104,16 @@ Ref MockMediaSample::createNonDisplayingCopy() const return copy; } +Ref MockMediaSample::createCopyWithAdjustedStartTime(const MediaTime& offset) const +{ + MediaTime clampedOffset = std::max(MediaTime::zeroTime(), std::min(offset, m_box.duration())); + + auto copy = MockMediaSample::create(m_box); + copy->m_box.setTimestamps(m_box.presentationTimestamp() + clampedOffset, m_box.decodeTimestamp() + clampedOffset); + copy->m_box.setDuration(m_box.duration() - clampedOffset); + return copy; +} + class MockMediaDescription final : public MediaDescription { public: static Ref create(const MockTrackBox& box) { return adoptRef(*new MockMediaDescription(box)); } From 5aadb31ea06464b0272464e7db5d17754387d832 Mon Sep 17 00:00:00 2001 From: Jessica Cheung Date: Thu, 14 May 2026 10:06:57 -0700 Subject: [PATCH 028/424] UIProcess Stack-Use-After-Scope in WebPageProxy::propagateDragAndDrop https://bugs.webkit.org/show_bug.cgi?id=314737 rdar://176191124 Reviewed by Aditya Keerthi. forwardingData should be moved instead of captured by reference because it is possible for forwardingData to be destroyed when grantAccessToCurrentPasteboardData's async completion handler fires. Add an API test that makes sure dropping a file in a cross origin subframe does not crash. This test will crash without this fix. * Source/WebKit/UIProcess/WebPageProxy.cpp: (WebKit::WebPageProxy::propagateDragAndDrop): * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm: (TestWebKitAPI::(SiteIsolation, DropExternalFileInCrossOriginSubframe)): Canonical link: https://commits.webkit.org/313254@main --- Source/WebKit/UIProcess/WebPageProxy.cpp | 6 ++- .../Tests/WebKit/WKWebView/SiteIsolation.mm | 51 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp index 712e40c1a79a..925bafd14028 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp @@ -3995,7 +3995,9 @@ void WebPageProxy::dragExited(DragData& dragData) #if PLATFORM(COCOA) void WebPageProxy::propagateDragAndDrop(DragEventForwardingData&& forwardingData, const String& dragStorageName, DragData&& dragData) { - grantAccessToCurrentPasteboardData(dragStorageName, [weakThis = WeakPtr { *this }, &forwardingData, dragStorageName, dragData = WTF::move(dragData)] () mutable { + auto targetFrameID = forwardingData.targetFrameID; + + grantAccessToCurrentPasteboardData(dragStorageName, [weakThis = WeakPtr { *this }, forwardingData = WTF::move(forwardingData), dragStorageName, dragData = WTF::move(dragData)] () mutable { RefPtr protectedThis = weakThis.get(); if (!protectedThis) return; @@ -4010,7 +4012,7 @@ void WebPageProxy::propagateDragAndDrop(DragEventForwardingData&& forwardingData protectedThis->propagateDragAndDrop(WTF::move(forwardingData), dragStorageName, WTF::move(dragDataCopy)); }); }); - }, forwardingData.targetFrameID); + }, targetFrameID); } #endif diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm index a05161569bdb..2246df957210 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm @@ -7581,6 +7581,57 @@ HTTPServer server({ EXPECT_TRUE(dragStarted); } +TEST(SiteIsolation, DropExternalFileInCrossOriginSubframe) +{ + static constexpr ASCIILiteral mainframeHTML = "" + "" + ""_s; + + static constexpr ASCIILiteral subframeHTML = "" + "" + ""_s; + + HTTPServer server({ + { "/mainframe"_s, { mainframeHTML } }, + { "/subframe"_s, { subframeHTML } } + }, HTTPServer::Protocol::HttpsProxy); + + RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]); + [navigationDelegate allowAnyTLSCertificate]; + + RetainPtr configuration = server.httpsProxyConfiguration(); + enableSiteIsolation(configuration.get()); + + RetainPtr simulator = adoptNS([[DragAndDropSimulator alloc] initWithWebViewFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]); + RetainPtr webView = [simulator webView]; + [webView setNavigationDelegate:navigationDelegate.get()]; + + [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]]; + [navigationDelegate waitForDidFinishNavigation]; + [webView waitForNextPresentationUpdate]; + + RetainPtr fileURL = [NSBundle.test_resourcesBundle URLForResource:@"apple" withExtension:@"gif"]; + [simulator writeFiles:@[ fileURL.get() ]]; + [simulator runFrom:CGPointMake(200, 200) to:CGPointMake(200, 200)]; + + __block RetainPtr dropFileName; + __block bool done = false; + [webView evaluateJavaScript:@"window.dropFileName" inFrame:[webView firstChildFrame] completionHandler:^(id result, NSError *error) { + dropFileName = result; + done = true; + }]; + TestWebKitAPI::Util::run(&done); + + EXPECT_WK_STREQ(dropFileName.get(), "apple.gif"); +} + #endif // ENABLE(DRAG_SUPPORT) && PLATFORM(MAC) TEST(SiteIsolation, AlternateRequest) From 4af385bb802bdaed9ed1c5a9c8bb417fa2601fa5 Mon Sep 17 00:00:00 2001 From: Simon Lewis Date: Thu, 14 May 2026 10:16:01 -0700 Subject: [PATCH 029/424] Update CoreIPCNSURLRequest serialization members https://bugs.webkit.org/show_bug.cgi?id=314784 rdar://176487693 Reviewed by Abrar Rahman Protyasha. A previous hardening change (307823@main) caused members to be omitted. This change adds them back in explicitly. Test: Tools/TestWebKitAPI/Tests/IPC/IPCSerialization.mm * Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.h: * Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.mm: (WebKit::CoreIPCNSURLRequest::CoreIPCNSURLRequest): (WebKit::CoreIPCNSURLRequest::toID const): * Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.serialization.in: * Tools/TestWebKitAPI/Tests/IPC/IPCSerialization.mm: (TEST(IPCSerialization, NSURLRequestProtocolProperties)): (TEST(IPCSerialization, NSURLRequestNSURLProtocolProperties)): Canonical link: https://commits.webkit.org/313255@main --- .../WebKit/Shared/Cocoa/CoreIPCNSURLRequest.h | 10 +++++++ .../Shared/Cocoa/CoreIPCNSURLRequest.mm | 14 ++++++++- .../CoreIPCNSURLRequest.serialization.in | 11 +++++++ .../Tests/IPC/IPCSerialization.mm | 30 ++++++++++++++++++- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.h b/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.h index 515e684d6183..feeb7dca781c 100644 --- a/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.h +++ b/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.h @@ -97,6 +97,12 @@ enum class NSURLRequestFlags : int16_t { ShouldStartSynchronously = (1 << 12) }; +enum class APProxyRequestType : uint8_t { + Undefined, + WebView, + Video +}; + struct ProtocolProperties { std::optional isTopLevelNavigation; std::optional allowAllPOSTCaching; @@ -106,6 +112,10 @@ struct ProtocolProperties { std::optional fileProtocolExpectedDevice; std::optional shouldSniff; std::optional contentDecoderSkipURLCheck; + std::optional adIdentifier; + std::optional maximumRequestCount; + std::optional apProxyIsRecursive; + std::optional requestType; }; struct CoreIPCNSURLRequestData { diff --git a/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.mm b/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.mm index 7dbe7b962429..2b06548dd4d4 100644 --- a/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.mm +++ b/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.mm @@ -99,6 +99,13 @@ - (instancetype)_initWithWebKitPropertyListData:(NSDictionary *)plist; SET_PROTOCOL_PROPERTY(protocolPropertiesDict, @"NSURLRequestFileProtocolExpectedDevice", NSNumber, CoreIPCNumber, props.fileProtocolExpectedDevice); SET_PROTOCOL_PROPERTY_BOOL(protocolPropertiesDict, @"_kCFURLConnectionPropertyShouldSniff", props.shouldSniff); SET_PROTOCOL_PROPERTY_BOOL(protocolPropertiesDict, @"kCFURLRequestContentDecoderSkipURLCheck", props.contentDecoderSkipURLCheck); + SET_PROTOCOL_PROPERTY(protocolPropertiesDict, @"adIdentifier", NSString, CoreIPCString, props.adIdentifier); + SET_PROTOCOL_PROPERTY(protocolPropertiesDict, @"maximumRequestCount", NSNumber, CoreIPCNumber, props.maximumRequestCount); + SET_PROTOCOL_PROPERTY_BOOL(protocolPropertiesDict, @"com.apple.ap.pc.proxy-is-recursive", props.apProxyIsRecursive); + if (RetainPtr value = dynamic_objc_cast([protocolPropertiesDict objectForKey:@"requestType"])) { + if (auto integerValue = [value integerValue]; isValidEnum(integerValue)) + props.requestType = static_cast(integerValue); + } m_data.protocolProperties = WTF::move(props); } @@ -255,7 +262,7 @@ - (instancetype)_initWithWebKitPropertyListData:(NSDictionary *)plist; RetainPtr dict = adoptNS([[NSMutableDictionary alloc] initWithCapacity:CoreIPCNSURLRequestData::numberOfFields]); if (m_data.protocolProperties) { - RetainPtr protocolPropertiesDict = adoptNS([[NSMutableDictionary alloc] initWithCapacity:8]); + RetainPtr protocolPropertiesDict = adoptNS([[NSMutableDictionary alloc] initWithCapacity:12]); SET_PROTOCOL_DICT_BOOL(protocolPropertiesDict, @"_kCFHTTPCookiePolicyPropertyIsTopLevelNavigation", m_data.protocolProperties->isTopLevelNavigation); SET_PROTOCOL_DICT_BOOL(protocolPropertiesDict, @"kCFURLRequestAllowAllPOSTCaching", m_data.protocolProperties->allowAllPOSTCaching); SET_PROTOCOL_DICT_MEMBER(protocolPropertiesDict, @"_kCFHTTPCookiePolicyPropertySiteForCookies", m_data.protocolProperties->siteForCookies); @@ -264,6 +271,11 @@ - (instancetype)_initWithWebKitPropertyListData:(NSDictionary *)plist; SET_PROTOCOL_DICT_MEMBER(protocolPropertiesDict, @"NSURLRequestFileProtocolExpectedDevice", m_data.protocolProperties->fileProtocolExpectedDevice); SET_PROTOCOL_DICT_BOOL(protocolPropertiesDict, @"_kCFURLConnectionPropertyShouldSniff", m_data.protocolProperties->shouldSniff); SET_PROTOCOL_DICT_BOOL(protocolPropertiesDict, @"kCFURLRequestContentDecoderSkipURLCheck", m_data.protocolProperties->contentDecoderSkipURLCheck); + SET_PROTOCOL_DICT_MEMBER(protocolPropertiesDict, @"adIdentifier", m_data.protocolProperties->adIdentifier); + SET_PROTOCOL_DICT_MEMBER(protocolPropertiesDict, @"maximumRequestCount", m_data.protocolProperties->maximumRequestCount); + SET_PROTOCOL_DICT_BOOL(protocolPropertiesDict, @"com.apple.ap.pc.proxy-is-recursive", m_data.protocolProperties->apProxyIsRecursive); + if (m_data.protocolProperties->requestType) + [protocolPropertiesDict setObject:[NSNumber numberWithInteger:static_cast(*m_data.protocolProperties->requestType)] forKey:@"requestType"]; [dict setObject:protocolPropertiesDict.get() forKey:@"protocolProperties"]; } diff --git a/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.serialization.in b/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.serialization.in index c36b990c5a78..25fc4bbb80de 100644 --- a/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.serialization.in +++ b/Source/WebKit/Shared/Cocoa/CoreIPCNSURLRequest.serialization.in @@ -96,6 +96,13 @@ header: "CoreIPCNSURLRequest.h" using WebKit::CoreIPCNSURLRequestData::BodyParts = Variant; using WebKit::CoreIPCNSURLRequestData::HeaderField = std::pair>>; +header: "CoreIPCNSURLRequest.h" +[CustomHeader, WebKitPlatform] enum class WebKit::APProxyRequestType : uint8_t { + Undefined, + WebView, + Video +} + [CustomHeader, WebKitPlatform] struct WebKit::ProtocolProperties { std::optional isTopLevelNavigation; std::optional allowAllPOSTCaching; @@ -105,6 +112,10 @@ using WebKit::CoreIPCNSURLRequestData::HeaderField = std::pair fileProtocolExpectedDevice; std::optional shouldSniff; std::optional contentDecoderSkipURLCheck; + std::optional adIdentifier; + std::optional maximumRequestCount; + std::optional apProxyIsRecursive; + std::optional requestType; } header: "CoreIPCNSURLRequest.h" diff --git a/Tools/TestWebKitAPI/Tests/IPC/IPCSerialization.mm b/Tools/TestWebKitAPI/Tests/IPC/IPCSerialization.mm index 46930b5a4d57..441580cc9106 100644 --- a/Tools/TestWebKitAPI/Tests/IPC/IPCSerialization.mm +++ b/Tools/TestWebKitAPI/Tests/IPC/IPCSerialization.mm @@ -1745,6 +1745,10 @@ - (instancetype)_initWithWebKitPropertyListData:(NSDictionary *)plist; props.fileProtocolExpectedDevice = WebKit::CoreIPCNumber(@123); props.shouldSniff = false; props.contentDecoderSkipURLCheck = true; + props.adIdentifier = WebKit::CoreIPCString(@"test-ad-id"); + props.maximumRequestCount = WebKit::CoreIPCNumber(@5); + props.apProxyIsRecursive = true; + props.requestType = WebKit::APProxyRequestType::WebView; requestData.protocolProperties = WTF::move(props); WebKit::CoreIPCNSURLRequest wrapper(WTF::move(requestData)); @@ -1756,7 +1760,7 @@ - (instancetype)_initWithWebKitPropertyListData:(NSDictionary *)plist; RetainPtr plistData = [reconstructedRequest _webKitPropertyListData]; RetainPtr protocolProperties = [plistData.get() objectForKey:@"protocolProperties"]; EXPECT_TRUE(protocolProperties != nil); - EXPECT_EQ([protocolProperties count], 8U); + EXPECT_EQ([protocolProperties count], 12U); EXPECT_TRUE([[protocolProperties objectForKey:@"_kCFHTTPCookiePolicyPropertyIsTopLevelNavigation"] boolValue]); EXPECT_FALSE([[protocolProperties objectForKey:@"kCFURLRequestAllowAllPOSTCaching"] boolValue]); @@ -1766,6 +1770,10 @@ - (instancetype)_initWithWebKitPropertyListData:(NSDictionary *)plist; EXPECT_EQ([[protocolProperties objectForKey:@"NSURLRequestFileProtocolExpectedDevice"] intValue], 123); EXPECT_FALSE([[protocolProperties objectForKey:@"_kCFURLConnectionPropertyShouldSniff"] boolValue]); EXPECT_TRUE([[protocolProperties objectForKey:@"kCFURLRequestContentDecoderSkipURLCheck"] boolValue]); + EXPECT_TRUE([[protocolProperties objectForKey:@"adIdentifier"] isEqualToString:@"test-ad-id"]); + EXPECT_EQ([[protocolProperties objectForKey:@"maximumRequestCount"] intValue], 5); + EXPECT_TRUE([[protocolProperties objectForKey:@"com.apple.ap.pc.proxy-is-recursive"] boolValue]); + EXPECT_EQ([[protocolProperties objectForKey:@"requestType"] intValue], 1); // Test full round-trip serialization runTestNS({ reconstructedRequest }); @@ -1821,6 +1829,26 @@ - (instancetype)_initWithWebKitPropertyListData:(NSDictionary *)plist; runTestNS({ reconstructedRequest3 }); } +TEST(IPCSerialization, NSURLRequestNSURLProtocolProperties) +{ + RetainPtr request = adoptNS([[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://webkit.org/"]]); + [NSURLProtocol setProperty:@"ad-123" forKey:@"adIdentifier" inRequest:request.get()]; + [NSURLProtocol setProperty:@3 forKey:@"maximumRequestCount" inRequest:request.get()]; + [NSURLProtocol setProperty:@YES forKey:@"com.apple.ap.pc.proxy-is-recursive" inRequest:request.get()]; + [NSURLProtocol setProperty:@2 forKey:@"requestType" inRequest:request.get()]; + + WebKit::CoreIPCNSURLRequest wrapper(request.get()); + RetainPtr reconstructed = dynamic_objc_cast(wrapper.toID().get()); + EXPECT_TRUE(reconstructed); + + EXPECT_TRUE([[NSURLProtocol propertyForKey:@"adIdentifier" inRequest:reconstructed.get()] isEqualToString:@"ad-123"]); + EXPECT_EQ([[NSURLProtocol propertyForKey:@"maximumRequestCount" inRequest:reconstructed.get()] intValue], 3); + EXPECT_TRUE([[NSURLProtocol propertyForKey:@"com.apple.ap.pc.proxy-is-recursive" inRequest:reconstructed.get()] boolValue]); + EXPECT_EQ([[NSURLProtocol propertyForKey:@"requestType" inRequest:reconstructed.get()] integerValue], 2); + + runTestNS({ request.get() }); +} + #endif // PLATFORM(COCOA) && HAVE(WK_SECURE_CODING_NSURLREQUEST) #if USE(AVFOUNDATION) && PLATFORM(MAC) From 20da9872141e117fb9f839f4e5571102f62c8e0a Mon Sep 17 00:00:00 2001 From: Alan Baradlay Date: Thu, 14 May 2026 10:31:48 -0700 Subject: [PATCH 030/424] [Flex] Flex container with aspect-ratio-derived height does not provide definite cross size to flex items https://bugs.webkit.org/show_bug.cgi?id=314734 Reviewed by Antti Koivisto.
The image should be 200x200 (flex container height = 400/2 = 200, image stretches to 200, aspect ratio 1:1 gives width 200). Instead it was 1x200 because the flex algorithm didn't recognize the container's height as definite. hasDefiniteCrossSizeForFlexItem only checked fixed heights and percentages. It missed the case where height is auto but determinable from aspect-ratio - the height says auto but we have an aspect ratio and we know the width, so we can figure out the height. This is only safe during layout (when the container's width is set from recomputeLogicalWidth). During intrinsic sizing the width isn't final yet so aspect-ratio can't resolve the height. The fix uses m_inLayout to distinguish the two phases. Extract canResolveCrossSizeFromAspectRatioDuringLayout() as a named check used by both hasDefiniteCrossSizeForFlexItem and innerCrossSizeForFlexItem. Tests: imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-ref.html imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001.html * LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-expected.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-ref.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001.html: Added. * Source/WebCore/rendering/RenderFlexibleBox.cpp: (WebCore::RenderFlexibleBox::hasDefiniteCrossSizeForFlexItem const): (WebCore::RenderFlexibleBox::canResolveCrossSizeFromAspectRatioDuringLayout const): (WebCore::RenderFlexibleBox::innerCrossSizeForFlexItem const): * Source/WebCore/rendering/RenderFlexibleBox.h: Canonical link: https://commits.webkit.org/313256@main --- ...-aspect-ratio-cross-size-001-expected.html | 20 +++++++++++++++++ .../flex-aspect-ratio-cross-size-001-ref.html | 20 +++++++++++++++++ .../flex-aspect-ratio-cross-size-001.html | 22 +++++++++++++++++++ .../WebCore/rendering/RenderFlexibleBox.cpp | 13 ++++++++++- Source/WebCore/rendering/RenderFlexibleBox.h | 1 + 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-ref.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-expected.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-expected.html new file mode 100644 index 000000000000..a9f80b6aaa8a --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-expected.html @@ -0,0 +1,20 @@ + +Reference + +

Test passes if the two images below are the same size.

+
+ +
+
+ +
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-ref.html new file mode 100644 index 000000000000..a9f80b6aaa8a --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001-ref.html @@ -0,0 +1,20 @@ + +Reference + +

Test passes if the two images below are the same size.

+
+ +
+
+ +
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001.html new file mode 100644 index 000000000000..cb52bb1de736 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-aspect-ratio-cross-size-001.html @@ -0,0 +1,22 @@ + +Flex container with aspect-ratio-derived height provides definite cross size to flex items + + + +

Test passes if the two images below are the same size.

+
+ +
+
+ +
diff --git a/Source/WebCore/rendering/RenderFlexibleBox.cpp b/Source/WebCore/rendering/RenderFlexibleBox.cpp index 77e8ee5070be..2cde5cb58f2b 100644 --- a/Source/WebCore/rendering/RenderFlexibleBox.cpp +++ b/Source/WebCore/rendering/RenderFlexibleBox.cpp @@ -1436,10 +1436,21 @@ bool RenderFlexibleBox::hasDefiniteCrossSizeForFlexItem(const RenderBox& flexIte return true; if (canResolveFullyConstrainedLogicalHeight(*this)) return true; + if (canResolveCrossSizeFromAspectRatioDuringLayout()) + return true; } return false; } +bool RenderFlexibleBox::canResolveCrossSizeFromAspectRatioDuringLayout() const +{ + // CSS Sizing 4 section 5.1: "When a box has a preferred aspect ratio and an automatic + // size in one axis, the automatic size is resolved from the definite size in the other axis." + // During layout the container's width is set, so aspect-ratio can resolve the height. + auto& crossSize = isHorizontalFlow() ? style().height() : style().width(); + return m_inLayout && crossSize.isAuto() && style().aspectRatio().hasRatio(); +} + template bool RenderFlexibleBox::flexItemCrossSizeIsDefinite(const RenderBox& flexItem, const SizeType& size) { if constexpr (!std::same_as) { @@ -2352,7 +2363,7 @@ LayoutUnit RenderFlexibleBox::innerCrossSizeForFlexItem(const RenderBox& flexIte innerCrossSize = adjustContentBoxLogicalHeightForBoxSizing(LayoutUnit { fixedSize->resolveZoom(style().usedZoomForLength()) }); else if (size.isPercent()) innerCrossSize = availableLogicalHeightForPercentageComputation().value_or(0_lu); - else if (canResolveFullyConstrainedLogicalHeight(*this)) + else if (canResolveFullyConstrainedLogicalHeight(*this) || canResolveCrossSizeFromAspectRatioDuringLayout()) innerCrossSize = std::max(0_lu, computeLogicalHeight(logicalHeight(), 0_lu).extent - borderAndPaddingLogicalHeight() - scrollbarLogicalHeight()); else ASSERT_NOT_REACHED(); diff --git a/Source/WebCore/rendering/RenderFlexibleBox.h b/Source/WebCore/rendering/RenderFlexibleBox.h index f05e04dadaaf..9ddec3fbed1b 100644 --- a/Source/WebCore/rendering/RenderFlexibleBox.h +++ b/Source/WebCore/rendering/RenderFlexibleBox.h @@ -113,6 +113,7 @@ class RenderFlexibleBox : public RenderBlock { ItemPosition alignmentForFlexItem(const RenderBox&) const; Style::FlexBasis flexBasisForFlexItem(const RenderBox&) const; bool hasDefiniteCrossSizeForFlexItem(const RenderBox& flexItem) const; + bool canResolveCrossSizeFromAspectRatioDuringLayout() const; protected: void computeIntrinsicLogicalWidths(LayoutUnit& minLogicalWidth, LayoutUnit& maxLogicalWidth) const override; From 40f2d454bc8084bbc80e188b6595dcc3d883dad2 Mon Sep 17 00:00:00 2001 From: Alan Baradlay Date: Thu, 14 May 2026 10:45:18 -0700 Subject: [PATCH 031/424] [Flex] SVG image with no intrinsic dimensions collapses to height 0 in column flex container https://bugs.webkit.org/show_bug.cgi?id=314774 Reviewed by Antti Koivisto.
The image should be 150x150 (the spec's "use 150px for the height" fallback for boxes without a preferred aspect ratio). Instead it was 150x0 - the flex algorithm took its automatic minimum size from a bogus aspect-ratio computation that returned 0. flexItemHasComputableAspectRatio used "intrinsicSize().height() != 0" as a shortcut for "this item has a real aspect ratio". RenderReplaced caches a (300, 150) fallback intrinsic for boxes with no real intrinsic dimensions, so the check returned true even for our empty SVG. The aspect-ratio branch then asked preferredAspectRatioAsSize() for the actual ratio, got (0, 0), computed crossSize * (0/0) = NaN, and the LayoutUnit constructor turned NaN into 0. Two of the three || checks in this function just repeated what flexItemHasAspectRatio (called right above) already checks, so they were doing nothing. The intrinsicSize().height() check was the only one that actually mattered - and it was the broken one. The fix replaces the unreliable intrinsicSize().height() shortcut with the same test the math already uses: preferredAspectRatioAsSize().aspectRatioDouble() > 0. This is true exactly when there is a real ratio (CSS aspect-ratio, real intrinsic dimensions, or SVG viewBox). The same trick is already used by the existing isSVGRootWithIntrinsicAspectRatio helper. * Source/WebCore/rendering/RenderFlexibleBox.cpp: (WebCore::RenderFlexibleBox::flexItemHasComputableAspectRatio const): * LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001{,-ref,-expected}.html: Added. Canonical link: https://commits.webkit.org/313257@main --- ...-svg-no-intrinsic-column-001-expected.html | 4 ++++ .../flex-svg-no-intrinsic-column-001-ref.html | 4 ++++ .../flex-svg-no-intrinsic-column-001.html | 20 +++++++++++++++++++ .../WebCore/rendering/RenderFlexibleBox.cpp | 4 +--- 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-ref.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-expected.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-expected.html new file mode 100644 index 000000000000..95119f597019 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-expected.html @@ -0,0 +1,4 @@ + +Reference +

Test passes if there is a filled green square.

+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-ref.html new file mode 100644 index 000000000000..95119f597019 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001-ref.html @@ -0,0 +1,4 @@ + +Reference +

Test passes if there is a filled green square.

+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001.html new file mode 100644 index 000000000000..c048c317c988 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flex-svg-no-intrinsic-column-001.html @@ -0,0 +1,20 @@ + +SVG image flex item with no intrinsic dimensions falls back to 150px height in column flex container + + + + +

Test passes if there is a filled green square.

+
+ +
diff --git a/Source/WebCore/rendering/RenderFlexibleBox.cpp b/Source/WebCore/rendering/RenderFlexibleBox.cpp index 2cde5cb58f2b..7e8761f8c6f7 100644 --- a/Source/WebCore/rendering/RenderFlexibleBox.cpp +++ b/Source/WebCore/rendering/RenderFlexibleBox.cpp @@ -1397,9 +1397,7 @@ bool RenderFlexibleBox::flexItemHasComputableAspectRatio(const RenderBox& flexIt { if (!flexItemHasAspectRatio(flexItem)) return false; - return flexItem.intrinsicSize().height() - || flexItem.style().aspectRatio().hasRatio() - || isSVGRootWithIntrinsicAspectRatio(flexItem); + return flexItem.preferredAspectRatioAsSize().aspectRatioDouble() > 0; } bool RenderFlexibleBox::flexItemHasComputableAspectRatioAndCrossSizeIsConsideredDefinite(const RenderBox& flexItem) From 7b509571d1cc10b9862b052d3222f2f68f7791ec Mon Sep 17 00:00:00 2001 From: Abrar Rahman Protyasha Date: Thu, 14 May 2026 10:56:26 -0700 Subject: [PATCH 032/424] [AppKit Gestures] Occasional debug assert when scrolling (ASSERT(m_layerHitTestMutex.isLocked())) https://bugs.webkit.org/show_bug.cgi?id=314782 rdar://177023194 Reviewed by Richard Robinson. ScrollingTree::pinnedStateIncludingAncestorsAtPoint() walks the scrolling tree via scrollingNodeForPoint(), but only acquires the tree state lock and not the mutex for layer hit-tests. This is problematic for the UI process scrolling path, since scrollingNodeForPoint() assumes that as a caller responsibility. In this patch, we acquire said mutex for the ancestor walk scope. We also move our state tree lock scope to only cover our read of the main frame scroll position, so that the lock ordering is preserved. In writing this patch, I noticed that we have multiple RAII utilities for this same pattern. We consolidate to ScrollingTree::HitTestLocker in WebCore as the canonical helper and migrate the other lockers away from their respective helpers. * Source/WebCore/page/scrolling/ScrollingTree.cpp: (WebCore::ScrollingTree::pinnedStateIncludingAncestorsAtPoint): * Source/WebCore/page/scrolling/ScrollingTree.h: (WebCore::ScrollingTree::HitTestLocker::HitTestLocker): (WebCore::ScrollingTree::HitTestLocker::~HitTestLocker): * Source/WebKit/UIProcess/RemoteLayerTree/RemoteScrollingTree.h: (WebKit::RemoteLayerTreeHitTestLocker::RemoteLayerTreeHitTestLocker): Deleted. (WebKit::RemoteLayerTreeHitTestLocker::~RemoteLayerTreeHitTestLocker): Deleted. * Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.mm: (WebKit::RemoteLayerTreeEventDispatcher::scrollingThreadHandleWheelEvent): (WebKit::RemoteLayerTreeEventDispatcher::didRefreshDisplay): Canonical link: https://commits.webkit.org/313258@main --- Source/WebCore/page/scrolling/ScrollingTree.cpp | 9 ++++++--- Source/WebCore/page/scrolling/ScrollingTree.h | 15 +++++++++++++++ .../RemoteLayerTree/RemoteScrollingTree.h | 17 ----------------- .../mac/RemoteLayerTreeEventDispatcher.mm | 4 ++-- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Source/WebCore/page/scrolling/ScrollingTree.cpp b/Source/WebCore/page/scrolling/ScrollingTree.cpp index 0b116c6e96f5..e561f765fe32 100644 --- a/Source/WebCore/page/scrolling/ScrollingTree.cpp +++ b/Source/WebCore/page/scrolling/ScrollingTree.cpp @@ -782,10 +782,13 @@ WebCore::RectEdges ScrollingTree::pinnedStateIncludingAncestorsAtPoint(Flo if (!rootNode) return false; - Locker locker { m_treeStateLock }; - FloatPoint position = viewPoint; - position.move(rootNode->viewToContentsOffset(m_treeState.mainFrameScrollPosition)); + { + Locker locker { m_treeStateLock }; + position.move(rootNode->viewToContentsOffset(m_treeState.mainFrameScrollPosition)); + } + + HitTestLocker hitTestLocker { *this }; WebCore::RectEdges pinnedState = { true, true, true, true }; diff --git a/Source/WebCore/page/scrolling/ScrollingTree.h b/Source/WebCore/page/scrolling/ScrollingTree.h index afc24fd645ae..4b93b160bb9b 100644 --- a/Source/WebCore/page/scrolling/ScrollingTree.h +++ b/Source/WebCore/page/scrolling/ScrollingTree.h @@ -234,6 +234,21 @@ class ScrollingTree : public ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtrlockLayersForHitTesting(); + } + ~HitTestLocker() + { + m_tree->unlockLayersForHitTesting(); + } + private: + const Ref m_tree; + }; + virtual bool isScrollingSynchronizedWithMainThread() WTF_REQUIRES_LOCK(m_treeLock) { return true; } Lock& treeLock() LIFETIME_BOUND WTF_RETURNS_LOCK(m_treeLock) { return m_treeLock; } diff --git a/Source/WebKit/UIProcess/RemoteLayerTree/RemoteScrollingTree.h b/Source/WebKit/UIProcess/RemoteLayerTree/RemoteScrollingTree.h index 409035e53c6c..a60bf35e0aaa 100644 --- a/Source/WebKit/UIProcess/RemoteLayerTree/RemoteScrollingTree.h +++ b/Source/WebKit/UIProcess/RemoteLayerTree/RemoteScrollingTree.h @@ -143,23 +143,6 @@ class RemoteScrollingTree : public WebCore::ScrollingTree { #endif }; -class RemoteLayerTreeHitTestLocker { -public: - RemoteLayerTreeHitTestLocker(RemoteScrollingTree& scrollingTree) - : m_scrollingTree(scrollingTree) - { - m_scrollingTree->lockLayersForHitTesting(); - } - - ~RemoteLayerTreeHitTestLocker() - { - m_scrollingTree->unlockLayersForHitTesting(); - } - -private: - const Ref m_scrollingTree; -}; - } // namespace WebKit SPECIALIZE_TYPE_TRAITS_SCROLLING_TREE(WebKit::RemoteScrollingTree, isRemoteScrollingTree()); diff --git a/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.mm b/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.mm index 03a9139c0c6a..79a216d40c24 100644 --- a/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.mm +++ b/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.mm @@ -237,7 +237,7 @@ void displayLinkFired(PlatformDisplayID displayID, DisplayUpdate, bool wantsFull if (!scrollingTree) return; - auto locker = RemoteLayerTreeHitTestLocker { *scrollingTree }; + ScrollingTree::HitTestLocker locker { *scrollingTree }; auto platformWheelEvent = platform(webWheelEvent); auto processingSteps = determineWheelEventProcessing(platformWheelEvent, rubberBandableEdges); @@ -468,7 +468,7 @@ void displayLinkFired(PlatformDisplayID displayID, DisplayUpdate, bool wantsFull #if ENABLE(MOMENTUM_EVENT_DISPATCHER) { // Make sure the lock is held for the handleSyntheticWheelEvent callback. - auto locker = RemoteLayerTreeHitTestLocker { *scrollingTree }; + ScrollingTree::HitTestLocker locker { *scrollingTree }; m_momentumEventDispatcher->displayDidRefresh(displayID); } #endif From 5711230e6d2eaf8d4f0e5590d10e7831aea13d2a Mon Sep 17 00:00:00 2001 From: Marta Darbinyan Date: Thu, 14 May 2026 11:26:04 -0700 Subject: [PATCH 033/424] [Gardening]: NEW TEST(312734@main): [macOS x86_64] 2 tests in TestWebKitAPI.UnifiedPDF are flaky failures https://bugs.webkit.org/show_bug.cgi?id=314757 rdar://177010062 Unreviewed test Gardening Skipping the tests also in Apple Silicon. * TestExpectations/apitests: Canonical link: https://commits.webkit.org/313259@main --- TestExpectations/apitests | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TestExpectations/apitests b/TestExpectations/apitests index 509fb85727e1..9884834e0905 100644 --- a/TestExpectations/apitests +++ b/TestExpectations/apitests @@ -303,5 +303,5 @@ webkit.org/b/246271 TestWTF.CompletionHandlerDeathTest.MainThreadHandlerOnThread webkit.org/b/246271 TestWTF.CompletionHandlerDeathTest.UncalledHandlerAssertsDeathTest [ Skip ] webkit.org/b/257892 TestWTF.SequenceLockedTest.WithoutSequenceLockedFails [ Skip ] rdar://134447305 [ mac debug ] TestWTF.SequenceLockedTest.Works [ Skip ] -webkit.org/b/314757 [ mac x86_64 ] TestWebKitAPI.UnifiedPDF.SelectAllTextInEmbedHostedPDF [ Skip ] -webkit.org/b/314757 [ mac x86_64 ] TestWebKitAPI.UnifiedPDF.SelectAllTextInObjectHostedPDF [ Skip ] \ No newline at end of file +webkit.org/b/314757 [ mac ] TestWebKitAPI.UnifiedPDF.SelectAllTextInEmbedHostedPDF [ Skip ] +webkit.org/b/314757 [ mac ] TestWebKitAPI.UnifiedPDF.SelectAllTextInObjectHostedPDF [ Skip ] \ No newline at end of file From 4532e715a8aa4513c77b24f4e375168ecfaf96fb Mon Sep 17 00:00:00 2001 From: Marta Darbinyan Date: Thu, 14 May 2026 12:26:58 -0700 Subject: [PATCH 034/424] [Gardening]: [macOS iOS] imported/w3c/web-platform-tests/IndexedDB/interleaved-cursors-small.any.serviceworker.html is a failure with Harness Error (TIMEOUT) https://bugs.webkit.org/show_bug.cgi?id=314830 rdar://177086725 Unreviewed test Gardening Updating test expectation for Debug. * LayoutTests/TestExpectations: * LayoutTests/tests-options.json: Canonical link: https://commits.webkit.org/313260@main --- LayoutTests/TestExpectations | 2 ++ LayoutTests/tests-options.json | 3 +++ 2 files changed, 5 insertions(+) diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations index 50f77f862efa..b57692fdf5f5 100644 --- a/LayoutTests/TestExpectations +++ b/LayoutTests/TestExpectations @@ -8103,3 +8103,5 @@ webkit.org/b/314100 imported/w3c/web-platform-tests/css/selectors/has-specificit imported/w3c/web-platform-tests/scroll-animations/animation-trigger/animation-trigger-fill-mode-both.tentative.html [ Pass ImageOnlyFailure ] imported/w3c/web-platform-tests/scroll-animations/animation-trigger/animation-trigger-fill-mode-none.tentative.html [ Pass ImageOnlyFailure ] imported/w3c/web-platform-tests/scroll-animations/animation-trigger/timeline-trigger-source-starts-in-trigger-range.tentative.html [ Pass Failure ] + +webkit.org/b/314830 [ Debug ] imported/w3c/web-platform-tests/IndexedDB/interleaved-cursors-small.any.serviceworker.html [ Failure ] diff --git a/LayoutTests/tests-options.json b/LayoutTests/tests-options.json index dedce7a77e6d..5bb8b858bda7 100644 --- a/LayoutTests/tests-options.json +++ b/LayoutTests/tests-options.json @@ -761,6 +761,9 @@ "imported/w3c/web-platform-tests/IndexedDB/database-names-by-origin.html": [ "slow" ], + "imported/w3c/web-platform-tests/IndexedDB/interleaved-cursors-small.any.serviceworker.html": [ + "slow" + ], "imported/w3c/web-platform-tests/XMLHttpRequest/progress-events-response-data-gzip.htm": [ "slow" ], From 76a66db2ed1becc00ad1684587aa69a8a84ad87e Mon Sep 17 00:00:00 2001 From: Jonathan Bedard Date: Thu, 14 May 2026 12:28:33 -0700 Subject: [PATCH 035/424] REGRESSION(313222@main): error: double-quoted include "Node.h" in framework header https://bugs.webkit.org/show_bug.cgi?id=314839 rdar://177093060 Unreviewed build fix. * Source/WebCore/accessibility/AXObjectTypes.h: Canonical link: https://commits.webkit.org/313261@main --- Source/WebCore/accessibility/AXObjectTypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/WebCore/accessibility/AXObjectTypes.h b/Source/WebCore/accessibility/AXObjectTypes.h index 4682f08e86d9..2195ce57fff8 100644 --- a/Source/WebCore/accessibility/AXObjectTypes.h +++ b/Source/WebCore/accessibility/AXObjectTypes.h @@ -25,7 +25,7 @@ #pragma once -#include "Node.h" +#include #include #include #include From 9a9c868ef9d268d0ef5571f93cb620deaed05875 Mon Sep 17 00:00:00 2001 From: Kiara Rose Date: Thu, 14 May 2026 15:23:33 -0700 Subject: [PATCH 036/424] NEW TESTS(312463@main): [macOS iOS] 2 failing tests in TestWebKitAPI.WKWebExtensionAPI (314126) https://bugs.webkit.org/show_bug.cgi?id=314126 rdar://176303192 Reviewed by Timothy Hatcher. A couple issues were going wrong here: 1) PortPostMessageGestureFromContentScriptIsNotPropagated was a constant failure because a user gesture was always active, but not because of it being propagated from the content script. The gesture was active because browser.action.onClicked carries one. To fix that, use browser.test instead to send the message to signal the background page and content script are ready. 2) For ExecuteScriptWithUserGesture, it's possible that performActionForTab runs before the page has fully loaded. To fix that, use browser.tabs.onUpdated to send a 'Page Ready' message before continuing on with the test. Testing: - Verified PortPostMessageGestureFromContentScriptIsNotPropagated passes (previously 100% failure) - Verified ExecuteScriptWithUserGesture passes with 50 iterations (previously at least one failure) * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIScripting.mm: (TestWebKitAPI::TEST(WKWebExtensionAPIScripting, ExecuteScriptWithUserGesture)): * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPITabs.mm: (TestWebKitAPI::TEST(WKWebExtensionAPITabs, PortPostMessageGestureFromContentScriptIsNotPropagated)): (TestWebKitAPI::TEST(WKWebExtensionAPITabs, DISABLED_PortPostMessageGestureFromContentScriptIsNotPropagated)): Deleted. Canonical link: https://commits.webkit.org/313262@main --- .../WebKit/WKWebView/WKWebExtensionAPIScripting.mm | 14 ++++++++------ .../WebKit/WKWebView/WKWebExtensionAPITabs.mm | 14 +++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIScripting.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIScripting.mm index cf54f851db99..dfcd2ce18cba 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIScripting.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIScripting.mm @@ -429,12 +429,7 @@ [manager run]; } -// FIXME when webkit.org/b/314126 is resolved. -#if PLATFORM(MAC) -TEST(WKWebExtensionAPIScripting, DISABLED_ExecuteScriptWithUserGesture) -#else TEST(WKWebExtensionAPIScripting, ExecuteScriptWithUserGesture) -#endif { TestWebKitAPI::HTTPServer server({ { "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } }, @@ -443,6 +438,11 @@ auto *backgroundScript = Util::constructScript(@[ @"browser.action.setPopup({ popup: '' })", + @"browser.tabs.onUpdated.addListener((tabId, changeInfo) => {", + @" if (changeInfo.status === 'complete')", + @" browser.test.sendMessage('Page Ready')", + @"})", + @"browser.action.onClicked.addListener(async (tab) => {", @" browser.test.assertTrue(navigator.userActivation.isActive, 'User gesture should be active at the scripting.executeScript call site')", @@ -463,9 +463,11 @@ auto manager = Util::loadExtension(scriptingActionManifest, @{ @"background.js": backgroundScript }); + [manager runUntilTestMessage:@"Background Ready"]; + [manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()]; - [manager runUntilTestMessage:@"Background Ready"]; + [manager runUntilTestMessage:@"Page Ready"]; [manager.get().context performActionForTab:manager.get().defaultTab]; diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPITabs.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPITabs.mm index 0f880a265856..9b343d1f255f 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPITabs.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPITabs.mm @@ -2565,8 +2565,7 @@ [manager run]; } -// FIXME when webkit.org/b/314126 is resolved. -TEST(WKWebExtensionAPITabs, DISABLED_PortPostMessageGestureFromContentScriptIsNotPropagated) +TEST(WKWebExtensionAPITabs, PortPostMessageGestureFromContentScriptIsNotPropagated) { // Gestures from content scripts are intentionally not propagated to extension pages, // even when the content script sends a port message inside a user gesture. @@ -2585,7 +2584,6 @@ @"type": @"module", @"persistent": @NO, }, - @"action": @{ }, @"content_scripts": @[ @{ @"js": @[ @"content.js" ], @"matches": @[ @"*://localhost/*" ], @@ -2593,10 +2591,12 @@ }; auto *backgroundScript = Util::constructScript(@[ - @"browser.action.setPopup({ popup: '' })", + @"browser.test.onMessage.addListener(async (message) => {", + @" if (message !== 'Connect')", + @" return", - @"browser.action.onClicked.addListener(async (tab) => {", - @" const port = browser.tabs.connect(tab.id, { name: 'gesturePort' })", + @" const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })", + @" const port = browser.tabs.connect(currentTab.id, { name: 'gesturePort' })", @" port.onMessage.addListener((message) => {", @" if (message !== 'check-gesture')", @" return", @@ -2630,7 +2630,7 @@ [manager runUntilTestMessage:@"Background Ready"]; [manager runUntilTestMessage:@"Content Ready"]; - [manager.get().context performActionForTab:manager.get().defaultTab]; + [manager sendTestMessage:@"Connect"]; [manager run]; } From 080f93ba4f5c3804a716970256ff959b1de9658a Mon Sep 17 00:00:00 2001 From: Marta Darbinyan Date: Thu, 14 May 2026 15:24:21 -0700 Subject: [PATCH 037/424] [Gardening]: REGRESSION(313197-313199@main?) [iOS] 2 tests in TestWebKitAPI.WKAttachmentTestsIOS are flaky failure https://bugs.webkit.org/show_bug.cgi?id=314846 rdar://177102817 Unreviewed test Gardening Update test expectation for flaky tests * TestExpectations/apitests: Canonical link: https://commits.webkit.org/313263@main --- TestExpectations/apitests | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TestExpectations/apitests b/TestExpectations/apitests index 9884834e0905..5f3e9061ecd4 100644 --- a/TestExpectations/apitests +++ b/TestExpectations/apitests @@ -304,4 +304,6 @@ webkit.org/b/246271 TestWTF.CompletionHandlerDeathTest.UncalledHandlerAssertsDea webkit.org/b/257892 TestWTF.SequenceLockedTest.WithoutSequenceLockedFails [ Skip ] rdar://134447305 [ mac debug ] TestWTF.SequenceLockedTest.Works [ Skip ] webkit.org/b/314757 [ mac ] TestWebKitAPI.UnifiedPDF.SelectAllTextInEmbedHostedPDF [ Skip ] -webkit.org/b/314757 [ mac ] TestWebKitAPI.UnifiedPDF.SelectAllTextInObjectHostedPDF [ Skip ] \ No newline at end of file +webkit.org/b/314757 [ mac ] TestWebKitAPI.UnifiedPDF.SelectAllTextInObjectHostedPDF [ Skip ] +webkit.org/b/314846 [ ios ] TestWebKitAPI.WKAttachmentTestsIOS.PasteRichTextCopiedFromNotes [ Skip ] +webkit.org/b/314846 [ ios ] TestWebKitAPI.WKAttachmentTestsIOS.InsertPastedMapItemAsAttachment [ Skip ] From 8cd68b84dc1d8920588049b0ca3f3aa74317b656 Mon Sep 17 00:00:00 2001 From: Alex Christensen Date: Thu, 14 May 2026 15:25:12 -0700 Subject: [PATCH 038/424] Remove WebKitBuffer.asUTF8String https://bugs.webkit.org/show_bug.cgi?id=314739 rdar://176990134 Reviewed by Ben Nham. It is not necessary. Using it removes the efficiency gains offered by WebKitBuffer because we need to read the entire buffer and check for ASCII-ness. If UTF-8 decoding is desired, an application can use TextDecoder instead. * Source/WebCore/page/WebKitBuffer.h: * Source/WebCore/page/WebKitBuffer.idl: * Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.cpp: (WebKit::SharedMemoryJSBuffer::asUTF8String const): Deleted. * Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.h: Canonical link: https://commits.webkit.org/313264@main --- Source/WebCore/page/WebKitBuffer.h | 1 - Source/WebCore/page/WebKitBuffer.idl | 1 - .../WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.cpp | 7 ------- .../WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.h | 1 - 4 files changed, 10 deletions(-) diff --git a/Source/WebCore/page/WebKitBuffer.h b/Source/WebCore/page/WebKitBuffer.h index 5ce7fd1b578f..a1dc81d8efbd 100644 --- a/Source/WebCore/page/WebKitBuffer.h +++ b/Source/WebCore/page/WebKitBuffer.h @@ -36,7 +36,6 @@ class WEBCORE_EXPORT WebKitBuffer : public RefCounted { public: virtual ~WebKitBuffer(); - virtual ExceptionOr asUTF8String() const = 0; virtual ExceptionOr asUTF16String() const = 0; virtual String asLatin1String() const = 0; diff --git a/Source/WebCore/page/WebKitBuffer.idl b/Source/WebCore/page/WebKitBuffer.idl index 81ad0ad607a7..bf26b02143ac 100644 --- a/Source/WebCore/page/WebKitBuffer.idl +++ b/Source/WebCore/page/WebKitBuffer.idl @@ -27,7 +27,6 @@ LegacyNoInterfaceObject, SkipVTableValidation, ] interface WebKitBuffer { - DOMString asUTF8String(); DOMString asUTF16String(); DOMString asLatin1String(); }; diff --git a/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.cpp b/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.cpp index eb552d6c938b..bb6a66103c89 100644 --- a/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.cpp +++ b/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.cpp @@ -42,13 +42,6 @@ SharedMemoryJSBuffer::SharedMemoryJSBuffer(Ref&& sharedMe { } -WebCore::ExceptionOr SharedMemoryJSBuffer::asUTF8String() const -{ - if (charactersAreAllASCII(m_sharedMemory->span())) - return asLatin1String(); - return String::fromUTF8(m_sharedMemory->span()); -} - WebCore::ExceptionOr SharedMemoryJSBuffer::asUTF16String() const { if (m_sharedMemory->size() % sizeof(char16_t)) diff --git a/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.h b/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.h index fa48fa79e033..c039e4ed07b8 100644 --- a/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.h +++ b/Source/WebKit/WebProcess/UserContent/SharedMemoryJSBuffer.h @@ -41,7 +41,6 @@ class SharedMemoryJSBuffer final : public WebCore::WebKitBuffer { private: SharedMemoryJSBuffer(Ref&&); - WebCore::ExceptionOr asUTF8String() const final; WebCore::ExceptionOr asUTF16String() const final; String asLatin1String() const final; From afd51141f19bf17198f9cb6dc81dec0014b8a6cc Mon Sep 17 00:00:00 2001 From: Qianlang Chen Date: Thu, 14 May 2026 15:25:55 -0700 Subject: [PATCH 039/424] Regression (311781@main): Web Inspector: Incremental build is broken; code changes in UserInterface are ignored https://bugs.webkit.org/show_bug.cgi?id=314831 Reviewed by BJ Burg. The "Copy User Interface Resources" build phase declares `$(SRCROOT)/UserInterface` as an inputPath. Xcode compares that directory's own mtime to the stamp file output to decide whether to rerun the phase. A directory's mtime does not change when a file inside it is edited in place, so in-place edits to any file under UserInterface can silently fail to propagate into the built WebInspectorUI.framework resources. 311781@main introduced the directory-based input tracking to enable no-change skipping. 312305@main attempted a fix via a checked-in UserInterface-input.xcfilelist, which was reverted in 312537@main. The tree has since been parked in a bad state introduced by 311781@main. Set `alwaysOutOfDate = 1` on the phase as a minimal unblocker, restoring the pre-311781@main behavior where the phase runs on every build of WebInspectorUI.framework. This reintroduces a small fixed cost per build in exchange for correctness. We'll work on a better solution in a follow-up later (https://webkit.org/b/314844). * Source/WebInspectorUI/WebInspectorUI.xcodeproj/project.pbxproj: Canonical link: https://commits.webkit.org/313265@main --- Source/WebInspectorUI/WebInspectorUI.xcodeproj/project.pbxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/WebInspectorUI/WebInspectorUI.xcodeproj/project.pbxproj b/Source/WebInspectorUI/WebInspectorUI.xcodeproj/project.pbxproj index 677e01a708b8..a5807d09436b 100644 --- a/Source/WebInspectorUI/WebInspectorUI.xcodeproj/project.pbxproj +++ b/Source/WebInspectorUI/WebInspectorUI.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ /* Begin PBXShellScriptBuildPhase section */ 1C60FF1214E6D9AF006CD77D /* Copy User Interface Resources */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); From 663a9e52289f2d46d004c0129a88ebc811f164b2 Mon Sep 17 00:00:00 2001 From: Diego De La Toba Date: Thu, 14 May 2026 15:27:25 -0700 Subject: [PATCH 040/424] [Gardening]REGRESSION(313207@main): [macOS] http/tests/site-isolation/inspector/debugger/provisional-frame-target-pausing.html is a constant text failure. https://bugs.webkit.org/show_bug.cgi?id=314851 rdar://177105765 Unreviewed test gardening. * LayoutTests/platform/mac-wk2/TestExpectations: Canonical link: https://commits.webkit.org/313266@main --- LayoutTests/platform/mac-wk2/TestExpectations | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LayoutTests/platform/mac-wk2/TestExpectations b/LayoutTests/platform/mac-wk2/TestExpectations index 16f3d195ce0a..7e93c000dd02 100644 --- a/LayoutTests/platform/mac-wk2/TestExpectations +++ b/LayoutTests/platform/mac-wk2/TestExpectations @@ -2295,6 +2295,8 @@ webkit.org/b/314668 webrtc/getUserMedia-webaudio-autoplay.html [ Pass Failure ] webkit.org/b/314136 accessibility/mac/client/div-bounds.html [ Pass Failure ] +webkit.org/b/314851 http/tests/site-isolation/inspector/debugger/provisional-frame-target-pausing.html [ Failure ] + webkit.org/b/308334 [ Debug ] http/tests/webgpu/webgpu/api/validation/gpu_external_texture_expiration.html [ Skip ] webkit.org/b/311677 [ Tahoe+ Release ] imported/w3c/web-platform-tests/svg/animations/svglength-animation-invalid-value-5.html [ Pass Failure ] From b8414eccb3b1ca2bd5d48b490e51e0dbcd41bc01 Mon Sep 17 00:00:00 2001 From: Simon Fraser Date: Thu, 14 May 2026 15:33:43 -0700 Subject: [PATCH 041/424] REGRESSION (303819@main): Child with filter:blur() ignores border radius overflow clipping https://bugs.webkit.org/show_bug.cgi?id=312584 rdar://175519148 Reviewed by Said Abou-Hallawa. When using the TransparencyLayerContextSwitcher path for filters, we need to be able to supply a complex clip for items clipped by border-radius on ancestors; this requires running the code in `applyAncestorClippingForBorderRadius()` after saving the graphics state, before starting the transparency layer, so factor the code to allow this by passing a function to GraphicsContextSwitcher. The previous attempt to fix this (312531@main) was reverted for causing rendering regressions of youtube buttons and on wayfair.com; this turned out to be a Core Graphics accelerated rendering bug (rdar://177036180) which we work around by applying the rounded clip in an outer transparency layer. `grayscale-clip-border-radius.html` tests this workaround. Tests: imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-ref.html imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius.html imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-ref.html imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius.html * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-expected.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-ref.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-expected.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-ref.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius.html: Added. * Source/WebCore/platform/graphics/GraphicsContextSwitcher.h: (WebCore::GraphicsContextSwitcher::beginClipAndDrawSourceImage): * Source/WebCore/platform/graphics/ImageBufferContextSwitcher.cpp: (WebCore::ImageBufferContextSwitcher::beginClipAndDrawSourceImage): * Source/WebCore/platform/graphics/ImageBufferContextSwitcher.h: * Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.cpp: (WebCore::TransparencyLayerContextSwitcher::beginClipAndDrawSourceImage): (WebCore::TransparencyLayerContextSwitcher::endDrawSourceImage): * Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.h: * Source/WebCore/rendering/RenderLayer.cpp: (WebCore::RenderLayer::clipToRect): (WebCore::RenderLayer::applyAncestorClippingForBorderRadius): (WebCore::RenderLayer::setupFilters): * Source/WebCore/rendering/RenderLayer.h: * Source/WebCore/rendering/RenderLayerFilters.cpp: (WebCore::RenderLayerFilters::beginFilterEffect): * Source/WebCore/rendering/RenderLayerFilters.h: Canonical link: https://commits.webkit.org/313267@main --- .../blur-clip-border-radius-expected.html | 36 +++++++++++ .../blur-clip-border-radius-ref.html | 36 +++++++++++ .../blur-clip-border-radius.html | 58 ++++++++++++++++++ ...grayscale-clip-border-radius-expected.html | 12 ++++ .../grayscale-clip-border-radius-ref.html | 12 ++++ .../grayscale-clip-border-radius.html | 25 ++++++++ .../graphics/GraphicsContextSwitcher.h | 3 +- .../graphics/ImageBufferContextSwitcher.cpp | 2 +- .../graphics/ImageBufferContextSwitcher.h | 2 +- .../TransparencyLayerContextSwitcher.cpp | 17 +++++- .../TransparencyLayerContextSwitcher.h | 3 +- Source/WebCore/rendering/RenderLayer.cpp | 60 ++++++++++++------- Source/WebCore/rendering/RenderLayer.h | 1 + .../WebCore/rendering/RenderLayerFilters.cpp | 4 +- Source/WebCore/rendering/RenderLayerFilters.h | 2 +- 15 files changed, 243 insertions(+), 30 deletions(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-ref.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-ref.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-expected.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-expected.html new file mode 100644 index 000000000000..e5b7254af7da --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-expected.html @@ -0,0 +1,36 @@ + + +

You should see no red above

+
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-ref.html new file mode 100644 index 000000000000..e5b7254af7da --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius-ref.html @@ -0,0 +1,36 @@ + + +

You should see no red above

+
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius.html new file mode 100644 index 000000000000..f489e6eacb6c --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/blur-clip-border-radius.html @@ -0,0 +1,58 @@ + +CSS Test: Ancestors with rounded clips should correctly clip a filtered element + + + + + + +

You should see no red above

+
+
+
+
+
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-expected.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-expected.html new file mode 100644 index 000000000000..034c68ea5e9d --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-expected.html @@ -0,0 +1,12 @@ + + +

You should see a muted green rounded rectangle.

+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-ref.html new file mode 100644 index 000000000000..034c68ea5e9d --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius-ref.html @@ -0,0 +1,12 @@ + + +

You should see a muted green rounded rectangle.

+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius.html new file mode 100644 index 000000000000..fd99d2c087bc --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/grayscale-clip-border-radius.html @@ -0,0 +1,25 @@ + +CSS Test: Filtered descendant of an element with border-radius and overflow:hidden should render correctly + + + + + + +

You should see a muted green rounded rectangle.

+
+
+
diff --git a/Source/WebCore/platform/graphics/GraphicsContextSwitcher.h b/Source/WebCore/platform/graphics/GraphicsContextSwitcher.h index d0cf866aee30..d7fb1bc1750a 100644 --- a/Source/WebCore/platform/graphics/GraphicsContextSwitcher.h +++ b/Source/WebCore/platform/graphics/GraphicsContextSwitcher.h @@ -27,6 +27,7 @@ #include "DestinationColorSpace.h" #include "FloatRect.h" +#include #include namespace WebCore { @@ -46,7 +47,7 @@ class GraphicsContextSwitcher { virtual bool hasSourceImage() const { return false; } - virtual void beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect& clipRect) = 0; + virtual void beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect& clipRect, NOESCAPE const Function& applyAdditionalDestinationClip = { }) = 0; virtual void endClipAndDrawSourceImage(GraphicsContext& destinationContext, const DestinationColorSpace&) = 0; virtual void beginDrawSourceImage(GraphicsContext& destinationContext, float opacity = 1.f) = 0; diff --git a/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.cpp b/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.cpp index 155dabc23416..81e2f6049d80 100644 --- a/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.cpp +++ b/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.cpp @@ -63,7 +63,7 @@ GraphicsContext* ImageBufferContextSwitcher::drawingContext(GraphicsContext& con return m_sourceImage ? &m_sourceImage->context() : &context; } -void ImageBufferContextSwitcher::beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect&) +void ImageBufferContextSwitcher::beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect&, NOESCAPE const Function&) { if (auto* context = drawingContext(destinationContext)) { context->save(); diff --git a/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.h b/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.h index 4b3651efe090..1ad17922f033 100644 --- a/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.h +++ b/Source/WebCore/platform/graphics/ImageBufferContextSwitcher.h @@ -43,7 +43,7 @@ class ImageBufferContextSwitcher final : public GraphicsContextSwitcher { bool hasSourceImage() const override { return m_sourceImage; } - void beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect& clipRect) override; + void beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect& clipRect, NOESCAPE const Function& applyAdditionalDestinationClip) override; void endClipAndDrawSourceImage(GraphicsContext& destinationContext, const DestinationColorSpace&) override; void beginDrawSourceImage(GraphicsContext&, float = 1.f) override { } diff --git a/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.cpp b/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.cpp index e2b38db9153b..2d3a798cca87 100644 --- a/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.cpp +++ b/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.cpp @@ -42,8 +42,17 @@ TransparencyLayerContextSwitcher::TransparencyLayerContextSwitcher(GraphicsConte m_filterStyles = m_filter->createFilterStyles(destinationContext, sourceImageRect); } -void TransparencyLayerContextSwitcher::beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect&, const FloatRect& clipRect) +void TransparencyLayerContextSwitcher::beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect&, const FloatRect& clipRect, NOESCAPE const Function& applyAdditionalDestinationClip) { + // Workaround for a CG accelerated-drawing bug rdar://177036180: CGStyle filters fail if there's a + // non-rectangular clip, so apply the rounded clip in its own wrapping transparency layer. + if (applyAdditionalDestinationClip) { + destinationContext.save(); + applyAdditionalDestinationClip(destinationContext); + destinationContext.beginTransparencyLayer(1); + m_beganOuterClipLayer = true; + } + for (auto& filterStyle : m_filterStyles) { destinationContext.save(); destinationContext.clip(intersection(filterStyle.imageRect, clipRect)); @@ -74,6 +83,12 @@ void TransparencyLayerContextSwitcher::endDrawSourceImage(GraphicsContext& desti destinationContext.restore(); } + if (m_beganOuterClipLayer) { + destinationContext.endTransparencyLayer(); + destinationContext.restore(); + m_beganOuterClipLayer = false; + } + if (m_beganOpacityLayer) { destinationContext.endTransparencyLayer(); m_beganOpacityLayer = false; diff --git a/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.h b/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.h index 1b9f678237db..17de9362474e 100644 --- a/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.h +++ b/Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.h @@ -37,7 +37,7 @@ class TransparencyLayerContextSwitcher final : public GraphicsContextSwitcher { TransparencyLayerContextSwitcher(GraphicsContext& destinationContext, const FloatRect& sourceImageRect, RefPtr&&); private: - void beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect& clipRect) override; + void beginClipAndDrawSourceImage(GraphicsContext& destinationContext, const FloatRect& repaintRect, const FloatRect& clipRect, NOESCAPE const Function& applyAdditionalDestinationClip) override; void endClipAndDrawSourceImage(GraphicsContext& destinationContext, const DestinationColorSpace& colorSpace) override { endDrawSourceImage(destinationContext, colorSpace); } void beginDrawSourceImage(GraphicsContext& destinationContext, float opacity = 1.f) override; @@ -45,6 +45,7 @@ class TransparencyLayerContextSwitcher final : public GraphicsContextSwitcher { FilterStyleVector m_filterStyles; bool m_beganOpacityLayer { false }; + bool m_beganOuterClipLayer { false }; }; } // namespace WebCore diff --git a/Source/WebCore/rendering/RenderLayer.cpp b/Source/WebCore/rendering/RenderLayer.cpp index c218b304b6ef..c4453b5759ba 100644 --- a/Source/WebCore/rendering/RenderLayer.cpp +++ b/Source/WebCore/rendering/RenderLayer.cpp @@ -3234,7 +3234,6 @@ void RenderLayer::paint(GraphicsContext& context, const LayoutRect& damageRect, void RenderLayer::clipToRect(GraphicsContext& context, GraphicsContextStateSaver& stateSaver, RegionContextStateSaver& regionContextStateSaver, const LayerPaintingInfo& paintingInfo, OptionSet paintBehavior, const ClipRect& clipRect, BorderRadiusClippingRule rule) { - float deviceScaleFactor = renderer().document().deviceScaleFactor(); bool needsClipping = !clipRect.isInfinite() && clipRect.rect() != paintingInfo.paintDirtyRect; if (needsClipping || clipRect.affectedByRadius()) stateSaver.save(); @@ -3247,27 +3246,33 @@ void RenderLayer::clipToRect(GraphicsContext& context, GraphicsContextStateSaver regionContextStateSaver.pushClip(enclosingIntRect(snappedClipRect)); } - if (clipRect.affectedByRadius()) { - // If the clip rect has been tainted by a border radius, then we have to walk up our layer chain applying the clips from - // any layers with overflow. The condition for being able to apply these clips is that the overflow object be in our - // containing block chain so we check that also. - for (RenderLayer* layer = rule == IncludeSelfForBorderRadius ? this : parent(); layer; layer = layer->parent()) { - if (paintBehavior.contains(PaintBehavior::CompositedOverflowScrollContent) && layer->usesCompositedScrolling()) - break; - - if (layer->renderer().hasNonVisibleOverflow() && layer->renderer().style().border().hasBorderRadius() && ancestorLayerIsInContainingBlockChain(*layer)) { - auto adjustedClipRect = LayoutRect { LayoutPoint { layer->offsetFromAncestor(paintingInfo.rootLayer, AdjustForColumns) }, layer->rendererBorderBoxRect().size() }; - adjustedClipRect.move(paintingInfo.subpixelOffset); - auto borderShape = BorderShape::shapeForBorderRect(layer->renderer().style(), adjustedClipRect); - if (borderShape.innerShapeContains(paintingInfo.paintDirtyRect)) - context.clip(snapRectToDevicePixels(intersection(paintingInfo.paintDirtyRect, adjustedClipRect), deviceScaleFactor)); - else - borderShape.clipToInnerShape(context, deviceScaleFactor); - } - - if (layer == paintingInfo.rootLayer) - break; + if (clipRect.affectedByRadius()) + applyAncestorClippingForBorderRadius(context, paintingInfo, paintBehavior, rule); +} + +void RenderLayer::applyAncestorClippingForBorderRadius(GraphicsContext& context, const LayerPaintingInfo& paintingInfo, OptionSet paintBehavior, BorderRadiusClippingRule rule) +{ + float deviceScaleFactor = renderer().document().deviceScaleFactor(); + + // If the clip rect has been tainted by a border radius, then we have to walk up our layer chain applying the clips from + // any layers with overflow. The condition for being able to apply these clips is that the overflow object be in our + // containing block chain so we check that also. + for (RenderLayer* layer = rule == IncludeSelfForBorderRadius ? this : parent(); layer; layer = layer->parent()) { + if (paintBehavior.contains(PaintBehavior::CompositedOverflowScrollContent) && layer->usesCompositedScrolling()) + break; + + if (layer->renderer().hasNonVisibleOverflow() && layer->renderer().style().border().hasBorderRadius() && ancestorLayerIsInContainingBlockChain(*layer)) { + auto adjustedClipRect = LayoutRect { LayoutPoint { layer->offsetFromAncestor(paintingInfo.rootLayer, AdjustForColumns) }, layer->rendererBorderBoxRect().size() }; + adjustedClipRect.move(paintingInfo.subpixelOffset); + auto borderShape = BorderShape::shapeForBorderRect(layer->renderer().style(), adjustedClipRect); + if (borderShape.innerShapeContains(paintingInfo.paintDirtyRect)) + context.clip(snapRectToDevicePixels(intersection(paintingInfo.paintDirtyRect, adjustedClipRect), deviceScaleFactor)); + else + borderShape.clipToInnerShape(context, deviceScaleFactor); } + + if (layer == paintingInfo.rootLayer) + break; } } @@ -3624,7 +3629,18 @@ GraphicsContext* RenderLayer::setupFilters(GraphicsContext& destinationContext, auto rootRelativeBounds = calculateLayerBounds(paintingInfo.rootLayer, offsetFromRoot, { RenderLayer::PreserveAncestorFlags }); - GraphicsContext* filterContext = paintingFilters->beginFilterEffect(renderer(), destinationContext, enclosingIntRect(rootRelativeBounds), enclosingIntRect(paintingInfo.paintDirtyRect), enclosingIntRect(filterRepaintRect), backgroundRect.rect()); + // When the filter is applied via a transparency layer directly on the destination context (e.g. CG drop-shadow), + // the switcher doesn't consult applyFilters's clipToRect path, so the ancestor border-radius clip would be lost. + // Provide a callback that applies that rounded clip on the destination before the transparency layer begins. + Function applyAdditionalDestinationClip; + if (backgroundRect.affectedByRadius()) { + applyAdditionalDestinationClip = [checkedThis = CheckedPtr { this }, &paintingInfo](GraphicsContext& context) { + checkedThis->applyAncestorClippingForBorderRadius(context, paintingInfo, paintingInfo.paintBehavior); + }; + } + + GraphicsContext* filterContext = paintingFilters->beginFilterEffect(renderer(), destinationContext, enclosingIntRect(rootRelativeBounds), enclosingIntRect(paintingInfo.paintDirtyRect), enclosingIntRect(filterRepaintRect), + backgroundRect.rect(), applyAdditionalDestinationClip); if (!filterContext) return nullptr; diff --git a/Source/WebCore/rendering/RenderLayer.h b/Source/WebCore/rendering/RenderLayer.h index f4c094fa4197..bdfbdcbb9775 100644 --- a/Source/WebCore/rendering/RenderLayer.h +++ b/Source/WebCore/rendering/RenderLayer.h @@ -1117,6 +1117,7 @@ class RenderLayer final : public UniquelyOwned { LayoutRect clipRectRelativeToAncestor(const RenderLayer* ancestor, LayoutSize offsetFromAncestor, const LayoutRect& constrainingRect, bool temporaryClipRects = false) const; void clipToRect(GraphicsContext&, GraphicsContextStateSaver&, RegionContextStateSaver&, const LayerPaintingInfo&, OptionSet, const ClipRect&, BorderRadiusClippingRule = IncludeSelfForBorderRadius); + void applyAncestorClippingForBorderRadius(GraphicsContext&, const LayerPaintingInfo&, OptionSet, BorderRadiusClippingRule = IncludeSelfForBorderRadius); bool shouldRepaintAfterLayout() const; diff --git a/Source/WebCore/rendering/RenderLayerFilters.cpp b/Source/WebCore/rendering/RenderLayerFilters.cpp index 0cdf20e868dc..078f1995478e 100644 --- a/Source/WebCore/rendering/RenderLayerFilters.cpp +++ b/Source/WebCore/rendering/RenderLayerFilters.cpp @@ -156,7 +156,7 @@ IntOutsets RenderLayerFilters::calculateOutsets(RenderElement& renderer, const F return CSSFilterRenderer::calculateOutsets(renderer, filter, targetBoundingBox); } -GraphicsContext* RenderLayerFilters::beginFilterEffect(RenderElement& renderer, GraphicsContext& context, const LayoutRect& filterBoxRect, const LayoutRect& dirtyRect, const LayoutRect& layerRepaintRect, const LayoutRect& clipRect) +GraphicsContext* RenderLayerFilters::beginFilterEffect(RenderElement& renderer, GraphicsContext& context, const LayoutRect& filterBoxRect, const LayoutRect& dirtyRect, const LayoutRect& layerRepaintRect, const LayoutRect& clipRect, NOESCAPE const Function& applyAdditionalDestinationClip) { auto preferredFilterRenderingModes = renderer.page().preferredFilterRenderingModes(context); auto outsets = calculateOutsets(renderer, filterBoxRect); @@ -236,7 +236,7 @@ GraphicsContext* RenderLayerFilters::beginFilterEffect(RenderElement& renderer, if (!m_targetSwitcher) return nullptr; - m_targetSwitcher->beginClipAndDrawSourceImage(context, m_repaintRect, clipRect); + m_targetSwitcher->beginClipAndDrawSourceImage(context, m_repaintRect, clipRect, applyAdditionalDestinationClip); return m_targetSwitcher->drawingContext(context); } diff --git a/Source/WebCore/rendering/RenderLayerFilters.h b/Source/WebCore/rendering/RenderLayerFilters.h index 5b3ad97a15f0..65374eb21693 100644 --- a/Source/WebCore/rendering/RenderLayerFilters.h +++ b/Source/WebCore/rendering/RenderLayerFilters.h @@ -76,7 +76,7 @@ class RenderLayerFilters final : public RefCounted, private // Per render LayoutRect repaintRect() const { return m_repaintRect; } - GraphicsContext* beginFilterEffect(RenderElement&, GraphicsContext&, const LayoutRect& filterBoxRect, const LayoutRect& dirtyRect, const LayoutRect& layerRepaintRect, const LayoutRect& clipRect); + GraphicsContext* beginFilterEffect(RenderElement&, GraphicsContext&, const LayoutRect& filterBoxRect, const LayoutRect& dirtyRect, const LayoutRect& layerRepaintRect, const LayoutRect& clipRect, NOESCAPE const Function& applyAdditionalDestinationClip = { }); void applyFilterEffect(GraphicsContext& destinationContext); private: From 42a531abe63602f463a4b02b82c7b4386284d61f Mon Sep 17 00:00:00 2001 From: Nipun Shukla Date: Thu, 14 May 2026 15:39:11 -0700 Subject: [PATCH 042/424] Dynamically inserting `` elements with `src` attribute is slow https://bugs.webkit.org/show_bug.cgi?id=314251 rdar://166201075 Reviewed by Chris Dumez. Dynamically inserting elements with src was exceedingly slow due to DFABytecodeInterpreter, which re-walks the full URL from byte 0 on every call. Since DFA state at byte N is a function of url[0..N], we checkpoint each DFA's program counter and accumulated actions at the first '/' after '://' in a 4-slot LRU on ContentExtension. Subsequent same-origin URLs resume from the checkpoint instead of re-walking the prefix. The included performance test progresses from a mean of 84 ms to 4 ms when testing locally. An APITest is included to ensure that we do not regress behavior when dealing with domains with the same starting characters. * PerformanceTests/Bindings/insert-img-with-src.html: Added. * Source/WebCore/contentextensions/ContentExtension.cpp: (WebCore::ContentExtensions::ContentExtension::ContentExtension): (WebCore::ContentExtensions::ContentExtension::interpretURLFilters const): * Source/WebCore/contentextensions/ContentExtension.h: * Source/WebCore/contentextensions/ContentExtensionsBackend.cpp: (WebCore::ContentExtensions::ContentExtensionsBackend::actionsFromContentRuleList const): * Source/WebCore/contentextensions/DFABytecodeInterpreter.cpp: (WebCore::ContentExtensions::DFABytecodeInterpreter::DFABytecodeInterpreter): (WebCore::ContentExtensions::DFABytecodeInterpreter::interpret): * Source/WebCore/contentextensions/DFABytecodeInterpreter.h: (WebCore::ContentExtensions::DFABytecodeInterpreter::DFABytecodeInterpreter): Deleted. * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ContentRuleListNotification.mm: (TEST(ContentRuleList, InterpretURLFilterCacheDoesNotConfuseSameStartDomains)): Canonical link: https://commits.webkit.org/313268@main --- .../Bindings/insert-img-with-src.html | 32 +++++++ .../contentextensions/ContentExtension.cpp | 6 ++ .../contentextensions/ContentExtension.h | 2 + .../ContentExtensionsBackend.cpp | 3 +- .../DFABytecodeInterpreter.cpp | 94 ++++++++++++++++--- .../DFABytecodeInterpreter.h | 18 +++- .../WKWebView/ContentRuleListNotification.mm | 44 +++++++++ 7 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 PerformanceTests/Bindings/insert-img-with-src.html diff --git a/PerformanceTests/Bindings/insert-img-with-src.html b/PerformanceTests/Bindings/insert-img-with-src.html new file mode 100644 index 000000000000..9dde7f86d8f1 --- /dev/null +++ b/PerformanceTests/Bindings/insert-img-with-src.html @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/Source/WebCore/contentextensions/ContentExtension.cpp b/Source/WebCore/contentextensions/ContentExtension.cpp index bd99de50cd9a..af98d12bc886 100644 --- a/Source/WebCore/contentextensions/ContentExtension.cpp +++ b/Source/WebCore/contentextensions/ContentExtension.cpp @@ -47,6 +47,7 @@ ContentExtension::ContentExtension(const String& identifier, RefurlFiltersBytecode(), DFABytecodeInterpreter::EnableResumeCache::Yes) { DFABytecodeInterpreter interpreter(m_compiledExtension->urlFiltersBytecode()); m_universalActions = copyToVector(interpreter.actionsMatchingEverything()); @@ -56,6 +57,11 @@ ContentExtension::ContentExtension(const String& identifier, RefserializedActions(); diff --git a/Source/WebCore/contentextensions/ContentExtension.h b/Source/WebCore/contentextensions/ContentExtension.h index 3b6ff0f75547..333815477ead 100644 --- a/Source/WebCore/contentextensions/ContentExtension.h +++ b/Source/WebCore/contentextensions/ContentExtension.h @@ -51,6 +51,7 @@ class ContentExtension : public RefCounted { const DFABytecodeInterpreter::Actions& topURLActions(const URL& topURL) const; const DFABytecodeInterpreter::Actions& frameURLActions(const URL& frameURL) const; const Vector& universalActions() const LIFETIME_BOUND { return m_universalActions; } + DFABytecodeInterpreter::Actions interpretURLFilters(const String& url, ResourceFlags) const; private: ContentExtension(const String& identifier, Ref&&, URL&&, ShouldCompileCSS); @@ -72,6 +73,7 @@ class ContentExtension : public RefCounted { mutable DFABytecodeInterpreter::Actions m_cachedFrameURLActions; Vector m_universalActions; + mutable DFABytecodeInterpreter m_urlFiltersInterpreter; }; } // namespace ContentExtensions diff --git a/Source/WebCore/contentextensions/ContentExtensionsBackend.cpp b/Source/WebCore/contentextensions/ContentExtensionsBackend.cpp index ba6ec109c1e0..2b11fd98ae55 100644 --- a/Source/WebCore/contentextensions/ContentExtensionsBackend.cpp +++ b/Source/WebCore/contentextensions/ContentExtensionsBackend.cpp @@ -111,8 +111,7 @@ auto ContentExtensionsBackend::actionsFromContentRuleList(const ContentExtension const auto& compiledExtension = contentExtension.compiledExtension(); - DFABytecodeInterpreter interpreter(compiledExtension.urlFiltersBytecode()); - auto actionLocations = interpreter.interpret(urlString, flags); + auto actionLocations = contentExtension.interpretURLFilters(urlString, flags); auto& topURLActions = contentExtension.topURLActions(resourceLoadInfo.mainDocumentURL); auto& frameURLActions = contentExtension.frameURLActions(resourceLoadInfo.frameURL); diff --git a/Source/WebCore/contentextensions/DFABytecodeInterpreter.cpp b/Source/WebCore/contentextensions/DFABytecodeInterpreter.cpp index a6acc18a0ed5..dfb1ded59b78 100644 --- a/Source/WebCore/contentextensions/DFABytecodeInterpreter.cpp +++ b/Source/WebCore/contentextensions/DFABytecodeInterpreter.cpp @@ -28,12 +28,15 @@ #include "ContentExtensionsDebugging.h" #include +#include #include #if ENABLE(CONTENT_EXTENSIONS) namespace WebCore::ContentExtensions { +WTF_MAKE_TZONE_ALLOCATED_IMPL(DFABytecodeInterpreter); + template static IntType NODELETE getBits(std::span bytecode, uint32_t index) { @@ -246,6 +249,13 @@ auto DFABytecodeInterpreter::actionsMatchingEverything() -> Actions return actions; } +DFABytecodeInterpreter::DFABytecodeInterpreter(std::span bytecode, EnableResumeCache enableCache) + : m_bytecode(bytecode) +{ + if (enableCache == EnableResumeCache::Yes) + m_resumeCache = makeUnique(); +} + auto DFABytecodeInterpreter::interpret(const String& urlString, ResourceFlags flags) -> Actions { CString urlCString; @@ -258,9 +268,45 @@ auto DFABytecodeInterpreter::interpret(const String& urlString, ResourceFlags fl url = byteCast(urlCString.span()); } ASSERT(url.data()); - + + uint32_t checkpointURLIndex = 0; + auto urlView = StringView(urlString); + if (auto schemeEnd = urlView.find("://"_s); schemeEnd != notFound) { + if (auto slash = urlView.find('/', schemeEnd + 3); slash != notFound && slash < std::numeric_limits::max()) + checkpointURLIndex = static_cast(slash); + } + + bool canResume = false; + if (checkpointURLIndex && m_resumeCache) { + auto& cache = *m_resumeCache; + if (cache.isEmpty()) + cache.grow(4); + auto urlPrefix = urlView.left(checkpointURLIndex + 1); + for (size_t i = 0; i < cache.size(); ++i) { + auto& slot = cache[i]; + if (slot.flags == flags && !slot.perDFA.isEmpty() && StringView(slot.url).startsWith(urlPrefix)) { + if (i) + std::swap(cache[0], slot); + canResume = true; + break; + } + } + if (!canResume) { + for (size_t i = cache.size() - 1; i > 0; --i) + cache[i] = WTF::move(cache[i - 1]); + cache[0] = { urlString, flags, { } }; + } + } + Actions actions; - + size_t dfaIndex = 0; + Actions dfaDelta; + + auto recordCheckpoint = [&](uint32_t pc) { + if (m_resumeCache && checkpointURLIndex && !canResume) + (*m_resumeCache)[0].perDFA.append({ pc, dfaDelta }); + }; + uint32_t programCounter = 0; while (programCounter < m_bytecode.size()) { @@ -269,30 +315,49 @@ auto DFABytecodeInterpreter::interpret(const String& urlString, ResourceFlags fl uint32_t dfaBytecodeLength = getBits(m_bytecode, programCounter); programCounter += sizeof(uint32_t); - // Skip the actions without flags on the DFA root. These are accessed via actionsMatchingEverything. - if (!dfaStart) { + uint32_t urlIndex = 0; + bool snapshottedThisDFA = false; + dfaDelta.clear(); + + if (canResume && dfaIndex < (*m_resumeCache)[0].perDFA.size()) { + const auto& checkpoint = (*m_resumeCache)[0].perDFA[dfaIndex]; + actions.addAll(checkpoint.actions); + if (checkpoint.programCounter == DFACheckpoint::terminatedBeforeCheckpoint) + goto nextDFA; + programCounter = checkpoint.programCounter; + urlIndex = checkpointURLIndex; + snapshottedThisDFA = true; + } else if (!dfaStart) { + // Skip the actions without flags on the DFA root. These are accessed via actionsMatchingEverything. while (programCounter < dfaBytecodeLength) { DFABytecodeInstruction instruction = getInstruction(m_bytecode, programCounter); if (instruction == DFABytecodeInstruction::AppendAction) { auto instructionLocation = programCounter++; consumeAction(m_bytecode, programCounter, instructionLocation); } else if (instruction == DFABytecodeInstruction::TestFlagsAndAppendAction) - interpretTestFlagsAndAppendAction(programCounter, flags, actions); + interpretTestFlagsAndAppendAction(programCounter, flags, dfaDelta); else break; } - if (programCounter >= m_bytecode.size()) - return actions; + if (programCounter >= m_bytecode.size()) { + recordCheckpoint(DFACheckpoint::terminatedBeforeCheckpoint); + actions.addAll(dfaDelta); + break; + } } else { ASSERT_WITH_MESSAGE(getInstruction(m_bytecode, programCounter) != DFABytecodeInstruction::AppendAction && getInstruction(m_bytecode, programCounter) != DFABytecodeInstruction::TestFlagsAndAppendAction, "Triggers that match everything should only be in the first DFA."); } - + // Interpret the bytecode from this DFA. // This should always terminate if interpreting correctly compiled bytecode. - uint32_t urlIndex = 0; while (true) { + if (!snapshottedThisDFA && checkpointURLIndex && urlIndex >= checkpointURLIndex) { + recordCheckpoint(programCounter); + snapshottedThisDFA = true; + } + ASSERT(programCounter <= m_bytecode.size()); switch (getInstruction(m_bytecode, programCounter)) { @@ -388,11 +453,11 @@ auto DFABytecodeInterpreter::interpret(const String& urlString, ResourceFlags fl } case DFABytecodeInstruction::AppendAction: - interpretAppendAction(programCounter, actions); + interpretAppendAction(programCounter, dfaDelta); break; - + case DFABytecodeInstruction::TestFlagsAndAppendAction: - interpretTestFlagsAndAppendAction(programCounter, flags, actions); + interpretTestFlagsAndAppendAction(programCounter, flags, dfaDelta); break; default: @@ -403,9 +468,14 @@ auto DFABytecodeInterpreter::interpret(const String& urlString, ResourceFlags fl } RELEASE_ASSERT_NOT_REACHED(); // The while loop can only be exited using goto nextDFA. nextDFA: + if (!snapshottedThisDFA) + recordCheckpoint(DFACheckpoint::terminatedBeforeCheckpoint); + actions.addAll(dfaDelta); + ++dfaIndex; ASSERT(dfaBytecodeLength); programCounter = dfaStart + dfaBytecodeLength; } + return actions; } diff --git a/Source/WebCore/contentextensions/DFABytecodeInterpreter.h b/Source/WebCore/contentextensions/DFABytecodeInterpreter.h index 2284b0a23a04..5f920d3b0635 100644 --- a/Source/WebCore/contentextensions/DFABytecodeInterpreter.h +++ b/Source/WebCore/contentextensions/DFABytecodeInterpreter.h @@ -36,12 +36,25 @@ namespace WebCore::ContentExtensions { class DFABytecodeInterpreter { + WTF_MAKE_TZONE_ALLOCATED_EXPORT(DFABytecodeInterpreter, WEBCORE_EXPORT); public: - DFABytecodeInterpreter(std::span bytecode) - : m_bytecode(bytecode) { } + enum class EnableResumeCache : bool { No, Yes }; + WEBCORE_EXPORT DFABytecodeInterpreter(std::span bytecode, EnableResumeCache = EnableResumeCache::No); using Actions = HashSet, WTF::UnsignedWithZeroKeyHashTraits>; + struct DFACheckpoint { + static constexpr uint32_t terminatedBeforeCheckpoint = std::numeric_limits::max(); + uint32_t programCounter { 0 }; + Actions actions; + }; + struct ResumeSlot { + String url; + ResourceFlags flags { 0 }; + Vector perDFA; + }; + using ResumeSlots = Vector; + WEBCORE_EXPORT Actions interpret(const String&, ResourceFlags); WEBCORE_EXPORT Actions actionsMatchingEverything(); @@ -53,6 +66,7 @@ class DFABytecodeInterpreter { void NODELETE interpretJumpTable(std::span url, uint32_t& urlIndex, uint32_t& programCounter); const std::span m_bytecode; + std::unique_ptr m_resumeCache; }; } // namespace WebCore::ContentExtensions diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ContentRuleListNotification.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ContentRuleListNotification.mm index bcfa4686c4ac..699397a773f0 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ContentRuleListNotification.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ContentRuleListNotification.mm @@ -656,3 +656,47 @@ HTTPServer server({ [webView loadRequest:server.request("/to-be-blocked"_s)]; EXPECT_WK_STREQ([webView _test_waitForAlert], "Redirected!"); } + +TEST(ContentRuleList, InterpretURLFilterCacheDoesNotConfuseSameStartDomains) +{ + NSString *ruleSource = @"[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\"^apitest://example\\\\.com/\"}}]"; + NSString *attackerURL = @"apitest://example.com.evil.com/test.html"; + NSString *legitURL = @"apitest://example.com/test.html"; + static unsigned counter = 0; + + auto makeWebView = [&]() -> RetainPtr { + NSString *identifier = [NSString stringWithFormat:@"sameStartDomains-%u", counter++]; + RetainPtr delegate = adoptNS([[ContentRuleListNotificationDelegate alloc] init]); + RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); + [[configuration userContentController] addContentRuleList:makeContentRuleList(ruleSource, identifier).get()]; + [configuration setURLSchemeHandler:delegate.get() forURLScheme:@"apitest"]; + RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration.get()]); + [webView setNavigationDelegate:delegate.get()]; + [webView setUIDelegate:delegate.get()]; + return webView; + }; + + auto fetchIsBlocked = [](WKWebView *webView, NSString *urlString) -> bool { + notificationList.clear(); + receivedAlert = false; + NSString *html = [NSString stringWithFormat:@"", urlString]; + [webView loadHTMLString:html baseURL:[NSURL URLWithString:@"apitest:///"]]; + TestWebKitAPI::Util::run(&receivedAlert); + for (const Notification& notification : notificationList) { + if (notification.url == String(urlString) && notification.blockedLoad) + return true; + } + return false; + }; + + auto checkSequence = [&](NSArray *urls) { + RetainPtr cached = makeWebView(); + for (NSString *url in urls) { + RetainPtr fresh = makeWebView(); + EXPECT_EQ(fetchIsBlocked(fresh.get(), url), fetchIsBlocked(cached.get(), url)); + } + }; + + checkSequence(@[attackerURL, legitURL, attackerURL, legitURL]); + checkSequence(@[legitURL, attackerURL, legitURL, attackerURL]); +} From ec22c1f4973c3e5c71a3c31d2f1644854c00b1cc Mon Sep 17 00:00:00 2001 From: Ahmad Saleem Date: Thu, 14 May 2026 15:43:29 -0700 Subject: [PATCH 043/424] Remove redundant frame resize in SVGImage::setContainerSize() https://bugs.webkit.org/show_bug.cgi?id=314799 rdar://177049202 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Brent Fulgham. setContainerSize() resized the frame view to containerSize() before updating the renderer's container size. Since containerSize() reads from the renderer (which hasn't been updated yet), this resized the view to its existing size — a no-op in steady state. Additionally, SVGImage::draw() already calls view->resize(containerSize()) before painting, after the renderer has the new size. Both internal call sites (drawForContainer and nativeImage) invoke draw() shortly after, so the intermediate resize is redundant. No new test, since this just removes a redundant intermediate resize. * Source/WebCore/svg/graphics/SVGImage.cpp: (WebCore::SVGImage::setContainerSize): Canonical link: https://commits.webkit.org/313269@main --- Source/WebCore/svg/graphics/SVGImage.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/Source/WebCore/svg/graphics/SVGImage.cpp b/Source/WebCore/svg/graphics/SVGImage.cpp index 8629e0ed216b..82189e0b101f 100644 --- a/Source/WebCore/svg/graphics/SVGImage.cpp +++ b/Source/WebCore/svg/graphics/SVGImage.cpp @@ -171,9 +171,6 @@ void SVGImage::setContainerSize(const FloatSize& size) if (!rootElement || !rootElement->renderer() || !rootElement->renderer()->isRenderOrLegacyRenderSVGRoot()) return; - RefPtr view = frameView(); - view->resize(containerSize()); - if (CheckedPtr renderer = dynamicDowncast(rootElement->renderer())) { renderer->setContainerSize(IntSize(size)); return; From dc311bb8313d5b2aac0e817007a024b38ce6e2c2 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Thu, 14 May 2026 15:45:18 -0700 Subject: [PATCH 044/424] [win] Enable `MemoryPressureHandlerWin` and Implement low memory watcher https://bugs.webkit.org/show_bug.cgi?id=297533 Reviewed by Don Olmstead. Enable the memory pressure handler on Windows by calling WebKit::installMemoryPressureHandler() from WebProcessPool::platformInitialize(). Implement a low memory watcher that is invoked by a callback when the system is under memory pressure. Canonical link: https://commits.webkit.org/313270@main --- Source/WTF/wtf/MemoryPressureHandler.h | 4 ++ .../WTF/wtf/win/MemoryPressureHandlerWin.cpp | 43 ++++++++++++++++++- Source/WebKit/PlatformWin.cmake | 1 + .../UIProcess/win/WebProcessPoolWin.cpp | 3 ++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Source/WTF/wtf/MemoryPressureHandler.h b/Source/WTF/wtf/MemoryPressureHandler.h index 8da100545863..a0a17c6585a6 100644 --- a/Source/WTF/wtf/MemoryPressureHandler.h +++ b/Source/WTF/wtf/MemoryPressureHandler.h @@ -264,9 +264,13 @@ class MemoryPressureHandler : public CanMakeWeakPtr { Configuration m_configuration; #if OS(WINDOWS) + friend VOID CALLBACK lowMemoryNotificationCallback(PVOID, BOOLEAN); void windowsMeasurementTimerFired(); + void windowsLowMemoryNotificationFired(); + void beginWaitingForLowMemoryNotification(); RunLoop::Timer m_windowsMeasurementTimer; Win32Handle m_lowMemoryHandle; + HANDLE m_lowMemoryWaitHandle { nullptr }; #endif #if (OS(LINUX) || OS(FREEBSD) || OS(HAIKU) || OS(QNX)) && !OS(ANDROID) diff --git a/Source/WTF/wtf/win/MemoryPressureHandlerWin.cpp b/Source/WTF/wtf/win/MemoryPressureHandlerWin.cpp index 372f8f74a054..3e22c2c414ec 100644 --- a/Source/WTF/wtf/win/MemoryPressureHandlerWin.cpp +++ b/Source/WTF/wtf/win/MemoryPressureHandlerWin.cpp @@ -27,6 +27,8 @@ #include #include +#include +#include #include namespace WTF { @@ -36,13 +38,45 @@ void MemoryPressureHandler::platformInitialize() m_lowMemoryHandle = Win32Handle::adopt(::CreateMemoryResourceNotification(LowMemoryResourceNotification)); } +VOID CALLBACK lowMemoryNotificationCallback(PVOID context, BOOLEAN) +{ + callOnMainThread([handler = static_cast(context)] { + handler->windowsLowMemoryNotificationFired(); + }); +} + +void MemoryPressureHandler::beginWaitingForLowMemoryNotification() +{ + if (m_lowMemoryWaitHandle) + return; + + if (!::RegisterWaitForSingleObject(&m_lowMemoryWaitHandle, m_lowMemoryHandle.get(), lowMemoryNotificationCallback, this, INFINITE, WT_EXECUTEONLYONCE)) + m_lowMemoryWaitHandle = nullptr; +} + +void MemoryPressureHandler::windowsLowMemoryNotificationFired() +{ + m_lowMemoryWaitHandle = nullptr; + + if (!m_installed || !m_lowMemoryHandle) + return; + + BOOL memoryLow; + + if (QueryMemoryResourceNotification(m_lowMemoryHandle.get(), &memoryLow) && memoryLow) { + setMemoryPressureStatus(SystemMemoryPressureStatus::Critical); + releaseMemory(Critical::Yes); + } + beginWaitingForLowMemoryNotification(); +} + void MemoryPressureHandler::windowsMeasurementTimerFired() { setMemoryPressureStatus(SystemMemoryPressureStatus::Normal); BOOL memoryLow; - if (QueryMemoryResourceNotification(m_lowMemoryHandle.get(), &memoryLow) && memoryLow) { + if (m_lowMemoryHandle && QueryMemoryResourceNotification(m_lowMemoryHandle.get(), &memoryLow) && memoryLow) { setMemoryPressureStatus(SystemMemoryPressureStatus::Critical); releaseMemory(Critical::Yes); return; @@ -68,11 +102,14 @@ void MemoryPressureHandler::windowsMeasurementTimerFired() void MemoryPressureHandler::platformReleaseMemory(Critical) { + WTF::releaseFastMallocFreeMemory(); } void MemoryPressureHandler::install() { + platformInitialize(); m_installed = true; + beginWaitingForLowMemoryNotification(); m_windowsMeasurementTimer.startRepeating(60_s); } @@ -82,6 +119,10 @@ void MemoryPressureHandler::uninstall() return; m_windowsMeasurementTimer.stop(); + if (m_lowMemoryWaitHandle) { + ::UnregisterWaitEx(m_lowMemoryWaitHandle, INVALID_HANDLE_VALUE); + m_lowMemoryWaitHandle = nullptr; + } m_installed = false; } diff --git a/Source/WebKit/PlatformWin.cmake b/Source/WebKit/PlatformWin.cmake index 86a1febedca9..1003cc3cc60a 100644 --- a/Source/WebKit/PlatformWin.cmake +++ b/Source/WebKit/PlatformWin.cmake @@ -41,6 +41,7 @@ list(APPEND WebKit_SOURCES UIProcess/DefaultUndoController.cpp UIProcess/LegacySessionStateCodingNone.cpp UIProcess/WebGrammarDetail.cpp + UIProcess/WebMemoryPressureHandler.cpp UIProcess/WebViewportAttributes.cpp UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp diff --git a/Source/WebKit/UIProcess/win/WebProcessPoolWin.cpp b/Source/WebKit/UIProcess/win/WebProcessPoolWin.cpp index 2c9c6573343b..e07c708361cb 100644 --- a/Source/WebKit/UIProcess/win/WebProcessPoolWin.cpp +++ b/Source/WebKit/UIProcess/win/WebProcessPoolWin.cpp @@ -27,6 +27,7 @@ #include "config.h" #include "WebProcessPool.h" +#include "WebMemoryPressureHandler.h" #include "WebProcessCreationParameters.h" #include @@ -63,6 +64,8 @@ static void initializeRemoteInspectorServer(StringView address) void WebProcessPool::platformInitialize(NeedsGlobalStaticInitialization) { + WebKit::installMemoryPressureHandler(); + #if ENABLE(REMOTE_INSPECTOR) if (const char* address = getenv("WEBKIT_INSPECTOR_SERVER")) initializeRemoteInspectorServer(StringView::fromLatin1(address)); From 3fd01c58fab1a580c0187ac8280e7acbcc48e63e Mon Sep 17 00:00:00 2001 From: Vitor Roriz Date: Thu, 14 May 2026 15:46:24 -0700 Subject: [PATCH 045/424] Serialize multi-word font family names without quotes when possible https://bugs.webkit.org/show_bug.cgi?id=313243 rdar://175522811 Reviewed by Elika Etemad. Per CSS Fonts 4 [1], a is a sequence of one or more identifiers. shouldQuoteFontFamily checked the whole string as a single identifier token, which always failed for multi-word names (spaces aren't valid in a single ident). Therefore the serialization always quoted them (e.g. "FB Armada" instead of FB Armada). Split by spaces and check each word individually. [1] https://www.w3.org/TR/css-fonts-4/#font-family-name-syntax * LayoutTests/editing/pasteboard/cjk-line-height-expected.txt: * LayoutTests/fast/css/getComputedStyle/computed-style-font-family-expected.txt: * LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset-expected.txt: * LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset.html: * LayoutTests/fast/css/serialization-with-double-quotes-expected.txt: * LayoutTests/fast/css/serialization-with-double-quotes.html: * LayoutTests/fast/text/font-face-family-expected.txt: * LayoutTests/fast/text/font-face-family.html: * LayoutTests/fast/text/font-stretch-parse-expected.txt: * LayoutTests/fast/text/font-stretch-parse.html: * LayoutTests/fast/text/font-style-parse-expected.txt: * LayoutTests/fast/text/font-style-parse.html: * LayoutTests/fast/text/font-weight-parse-expected.txt: * LayoutTests/fast/text/font-weight-parse.html: * LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative.html: * LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-computed-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-computed-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-valid-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-valid-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/serialize-values-expected.txt: * LayoutTests/platform/glib/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt: * LayoutTests/platform/gtk/fast/css/css2-system-fonts-expected.txt: * LayoutTests/platform/gtk/imported/w3c/web-platform-tests/css/css-fonts/animations/system-fonts-expected.txt: * Source/WebCore/css/CSSMarkup.cpp: (WebCore::shouldQuoteFontFamily): * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebViewEditActions.mm: (TestWebKitAPI::TEST(WKWebViewEditActions, SetFontFamily)): * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/FontManagerTests.mm: (TestWebKitAPI::TEST(FontManagerTests, ChangeFontWithPanel)): Canonical link: https://commits.webkit.org/313271@main --- .../pasteboard/cjk-line-height-expected.txt | 2 +- .../computed-style-font-family-expected.txt | 2 +- .../font-family-fallback-reset-expected.txt | 2 +- .../font-family-fallback-reset.html | 2 +- ...ialization-with-double-quotes-expected.txt | 4 +- .../css/serialization-with-double-quotes.html | 4 +- .../fast/text/font-face-family-expected.txt | 2 +- LayoutTests/fast/text/font-face-family.html | 2 +- .../fast/text/font-stretch-parse-expected.txt | 20 ++--- LayoutTests/fast/text/font-stretch-parse.html | 20 ++--- .../fast/text/font-style-parse-expected.txt | 24 ++--- LayoutTests/fast/text/font-style-parse.html | 24 ++--- .../fast/text/font-weight-parse-expected.txt | 24 ++--- LayoutTests/fast/text/font-weight-parse.html | 24 ++--- ...face-invalid-family.tentative-expected.txt | 3 + .../fontface-invalid-family.tentative.html | 3 + .../parsing/font-computed-expected.txt | 88 +++++++++---------- .../parsing/font-family-computed-expected.txt | 2 +- .../parsing/font-family-valid-expected.txt | 2 +- .../css-fonts/parsing/font-valid-expected.txt | 88 +++++++++---------- ...ight-matching-installed-fonts-expected.txt | 12 +-- .../css/cssom/serialize-values-expected.txt | 2 +- ...ight-matching-installed-fonts-expected.txt | 12 +-- .../fast/css/css2-system-fonts-expected.txt | 18 ++-- .../animations/system-fonts-expected.txt | 72 +++++++-------- Source/WebCore/css/CSSMarkup.cpp | 33 +++++-- .../WebKit/WKWebView/WKWebViewEditActions.mm | 2 +- .../WebKit/WKWebView/mac/FontManagerTests.mm | 2 +- 28 files changed, 262 insertions(+), 233 deletions(-) diff --git a/LayoutTests/editing/pasteboard/cjk-line-height-expected.txt b/LayoutTests/editing/pasteboard/cjk-line-height-expected.txt index ddf67386759d..75caf62698ce 100644 --- a/LayoutTests/editing/pasteboard/cjk-line-height-expected.txt +++ b/LayoutTests/editing/pasteboard/cjk-line-height-expected.txt @@ -1,6 +1,6 @@ This tests copying and pasting text with a font that influences the used line-height value. To manually test, copy and paste the selected content below. WebKit should not generate line-height property in the pasted content. | -| style="font-family: "Hiragino Kaku Gothic ProN";" +| style="font-family: Hiragino Kaku Gothic ProN;" | "hello<#selection-caret>" |
diff --git a/LayoutTests/fast/css/getComputedStyle/computed-style-font-family-expected.txt b/LayoutTests/fast/css/getComputedStyle/computed-style-font-family-expected.txt index d5d40f60a1db..8472e9e4e585 100644 --- a/LayoutTests/fast/css/getComputedStyle/computed-style-font-family-expected.txt +++ b/LayoutTests/fast/css/getComputedStyle/computed-style-font-family-expected.txt @@ -1,6 +1,6 @@ Font attributes. The font-family should list three families: -font-family: monospace, "Lucida Grande", sans-serif; +font-family: monospace, Lucida Grande, sans-serif; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; diff --git a/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset-expected.txt b/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset-expected.txt index 427bb2d1a962..e40c54df5b03 100644 --- a/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset-expected.txt +++ b/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset-expected.txt @@ -3,7 +3,7 @@ Setting a new font-family should reset the fallback list to empty before adding On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE". -PASS window.getComputedStyle(outerDiv, null).fontFamily is "\"courier new\", cursive" +PASS window.getComputedStyle(outerDiv, null).fontFamily is "courier new, cursive" PASS window.getComputedStyle(timesDiv, null).fontFamily is "foo" PASS window.getComputedStyle(cursiveDiv, null).fontFamily is "cursive" PASS successfullyParsed is true diff --git a/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset.html b/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset.html index b7efdfd93a5f..b86c0650019c 100644 --- a/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset.html +++ b/LayoutTests/fast/css/getComputedStyle/font-family-fallback-reset.html @@ -17,7 +17,7 @@ '
should be cursive
' + '
'; -shouldBeEqualToString("window.getComputedStyle(outerDiv, null).fontFamily", `"courier new", cursive`); +shouldBeEqualToString("window.getComputedStyle(outerDiv, null).fontFamily", "courier new, cursive"); shouldBeEqualToString("window.getComputedStyle(timesDiv, null).fontFamily", "foo"); shouldBeEqualToString("window.getComputedStyle(cursiveDiv, null).fontFamily", "cursive"); diff --git a/LayoutTests/fast/css/serialization-with-double-quotes-expected.txt b/LayoutTests/fast/css/serialization-with-double-quotes-expected.txt index 232508980acd..a31518f63a49 100644 --- a/LayoutTests/fast/css/serialization-with-double-quotes-expected.txt +++ b/LayoutTests/fast/css/serialization-with-double-quotes-expected.txt @@ -11,8 +11,8 @@ PASS parsed = eval(value.replace(/^'/, "").replace(/'$/, "")); is "{foo: \"bar\" PASS ruleWithAttributeSelector.selectorText is "span[class=\"foo bar\"]" PASS getComputedStyle(document.querySelector("span[class='foo bar']")).getPropertyValue("color") is "rgb(0, 128, 0)" PASS counterRule.style.content is "counters(section, \".\")" -PASS fontFamilyRule.style.fontFamily is "\"Two Infinite Loop\", \"Cupertino CA\"" -PASS getComputedStyle(document.querySelector("article")).getPropertyValue("font-family") is "\"Two Infinite Loop\", \"Cupertino CA\"" +PASS fontFamilyRule.style.fontFamily is "Two Infinite Loop, Cupertino CA" +PASS getComputedStyle(document.querySelector("article")).getPropertyValue("font-family") is "Two Infinite Loop, Cupertino CA" PASS backgroundImageRule.style.backgroundImage is "url(\"data:image/svg+xml,\")" PASS getComputedStyle(document.querySelector("section")).backgroundImage is "url(\"data:image/svg+xml,\")" PASS shapeRule.style["-webkit-clip-path"] is "path(\"M 100 40 l 20 0 l 0 60\")" diff --git a/LayoutTests/fast/css/serialization-with-double-quotes.html b/LayoutTests/fast/css/serialization-with-double-quotes.html index 3ec841be57f3..0b62b91b107c 100644 --- a/LayoutTests/fast/css/serialization-with-double-quotes.html +++ b/LayoutTests/fast/css/serialization-with-double-quotes.html @@ -42,9 +42,9 @@ shouldBeEqualToString('counterRule.style.content', 'counters(section, ".")'); var fontFamilyRule = styleSheet.rules[5]; -shouldBeEqualToString('fontFamilyRule.style.fontFamily', '"Two Infinite Loop", "Cupertino CA"'); +shouldBeEqualToString('fontFamilyRule.style.fontFamily', 'Two Infinite Loop, Cupertino CA'); shouldBeEqualToString('getComputedStyle(document.querySelector("article")).getPropertyValue("font-family")', - '"Two Infinite Loop", "Cupertino CA"'); + 'Two Infinite Loop, Cupertino CA'); var backgroundImageRule = styleSheet.rules[6]; var url = `url("data:image/svg+xml,")`; diff --git a/LayoutTests/fast/text/font-face-family-expected.txt b/LayoutTests/fast/text/font-face-family-expected.txt index c84f580e0bb6..10d3be38415e 100644 --- a/LayoutTests/fast/text/font-face-family-expected.txt +++ b/LayoutTests/fast/text/font-face-family-expected.txt @@ -5,7 +5,7 @@ PASS (new FontFace('4\'a', 'url(garbage.otf)')).family is "\"4'a\"" PASS (new FontFace('4\'a"b', 'url(garbage.otf)')).family is "\"4'a\\\"b\"" PASS (new FontFace('ab', 'url(garbage.otf)')).family is "ab" PASS (new FontFace('"ab"', 'url(garbage.otf)')).family is "\"\\\"ab\\\"\"" -PASS (new FontFace('a b', 'url(garbage.otf)')).family is "\"a b\"" +PASS (new FontFace('a b', 'url(garbage.otf)')).family is "a b" PASS (new FontFace('a b, c', 'url(garbage.otf)')).family is "\"a b, c\"" PASS (new FontFace('ab,c', 'url(garbage.otf)')).family is "\"ab,c\"" PASS successfullyParsed is true diff --git a/LayoutTests/fast/text/font-face-family.html b/LayoutTests/fast/text/font-face-family.html index 7b2775526e34..2f212e6c1566 100644 --- a/LayoutTests/fast/text/font-face-family.html +++ b/LayoutTests/fast/text/font-face-family.html @@ -13,7 +13,7 @@ shouldBeEqualToString("(new FontFace('4\\'a\"b', 'url(garbage.otf)')).family", "\"4'a\\\"b\""); shouldBeEqualToString("(new FontFace('ab', 'url(garbage.otf)')).family", "ab"); shouldBeEqualToString("(new FontFace('\"ab\"', 'url(garbage.otf)')).family", "\"\\\"ab\\\"\""); -shouldBeEqualToString("(new FontFace('a b', 'url(garbage.otf)')).family", "\"a b\""); +shouldBeEqualToString("(new FontFace('a b', 'url(garbage.otf)')).family", "a b"); shouldBeEqualToString("(new FontFace('a b, c', 'url(garbage.otf)')).family", "\"a b, c\""); shouldBeEqualToString("(new FontFace('ab,c', 'url(garbage.otf)')).family", "\"ab,c\""); diff --git a/LayoutTests/fast/text/font-stretch-parse-expected.txt b/LayoutTests/fast/text/font-stretch-parse-expected.txt index 9e8540928473..bcd068fe5fde 100644 --- a/LayoutTests/fast/text/font-stretch-parse-expected.txt +++ b/LayoutTests/fast/text/font-stretch-parse-expected.txt @@ -34,18 +34,18 @@ PASS getComputedStyle(document.getElementById('test12')).font is "ultra-expanded PASS getComputedStyle(document.getElementById('test13')).font is "16px / 18px Times" PASS getComputedStyle(document.getElementById('test14')).font is "16px / 18px Times" PASS getComputedStyle(document.getElementById('test15')).font is "16px / 18px Times" -PASS getComputedStyle(document.getElementById('test16')).font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test17')).font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test18')).font is "100 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test19')).font is "100 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test20')).font is "italic small-caps 100 extra-expanded 48px / 49px \"Helvetica Neue\"" +PASS getComputedStyle(document.getElementById('test16')).font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test17')).font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test18')).font is "100 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test19')).font is "100 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test20')).font is "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue" PASS getComputedStyle(document.getElementById('test21')).font is "" PASS document.getElementById('test1').style.font is "" -PASS document.getElementById('test16').style.font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test17').style.font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test18').style.font is "100 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test19').style.font is "100 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test20').style.font is "italic small-caps 100 extra-expanded 48px / 49px \"Helvetica Neue\"" +PASS document.getElementById('test16').style.font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS document.getElementById('test17').style.font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS document.getElementById('test18').style.font is "100 48px / 49px Helvetica Neue" +PASS document.getElementById('test19').style.font is "100 48px / 49px Helvetica Neue" +PASS document.getElementById('test20').style.font is "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue" PASS successfullyParsed is true TEST COMPLETE diff --git a/LayoutTests/fast/text/font-stretch-parse.html b/LayoutTests/fast/text/font-stretch-parse.html index a5365a4eedfa..c82d04b0ee36 100644 --- a/LayoutTests/fast/text/font-stretch-parse.html +++ b/LayoutTests/fast/text/font-stretch-parse.html @@ -65,19 +65,19 @@ shouldBeEqualToString("getComputedStyle(document.getElementById('test13')).font", "16px / 18px Times"); shouldBeEqualToString("getComputedStyle(document.getElementById('test14')).font", "16px / 18px Times"); shouldBeEqualToString("getComputedStyle(document.getElementById('test15')).font", "16px / 18px Times"); -shouldBeEqualToString("getComputedStyle(document.getElementById('test16')).font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test17')).font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test18')).font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test19')).font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test20')).font", `italic small-caps 100 extra-expanded 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("getComputedStyle(document.getElementById('test16')).font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test17')).font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test18')).font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test19')).font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test20')).font", "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue"); shouldBeEqualToString("getComputedStyle(document.getElementById('test21')).font", ""); shouldBeEqualToString("document.getElementById('test1').style.font", ""); -shouldBeEqualToString("document.getElementById('test16').style.font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test17').style.font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test18').style.font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test19').style.font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test20').style.font", `italic small-caps 100 extra-expanded 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("document.getElementById('test16').style.font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test17').style.font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test18').style.font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test19').style.font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test20').style.font", "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue"); diff --git a/LayoutTests/fast/text/font-style-parse-expected.txt b/LayoutTests/fast/text/font-style-parse-expected.txt index 03e07915aaa2..81fbbec81fa0 100644 --- a/LayoutTests/fast/text/font-style-parse-expected.txt +++ b/LayoutTests/fast/text/font-style-parse-expected.txt @@ -34,22 +34,22 @@ PASS getComputedStyle(document.getElementById('test11')).font is "16px / 18px Ti PASS getComputedStyle(document.getElementById('test12')).font is "16px / 18px Times" PASS getComputedStyle(document.getElementById('test13')).font is "16px / 18px Times" PASS getComputedStyle(document.getElementById('test14')).font is "" -PASS getComputedStyle(document.getElementById('test15')).font is "italic 100 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test16')).font is "italic 100 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test17')).font is "100 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test18')).font is "italic 48px / 49px \"Helvetica Neue\"" -PASS getComputedStyle(document.getElementById('test19')).font is "italic small-caps 100 extra-expanded 48px / 49px \"Helvetica Neue\"" +PASS getComputedStyle(document.getElementById('test15')).font is "italic 100 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test16')).font is "italic 100 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test17')).font is "100 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test18')).font is "italic 48px / 49px Helvetica Neue" +PASS getComputedStyle(document.getElementById('test19')).font is "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue" PASS getComputedStyle(document.getElementById('test20')).font is "16px / 18px Times" -PASS getComputedStyle(document.getElementById('test21')).font is "oblique small-caps 123 extra-expanded 48px / 49px \"Helvetica Neue\"" +PASS getComputedStyle(document.getElementById('test21')).font is "oblique small-caps 123 extra-expanded 48px / 49px Helvetica Neue" PASS getComputedStyle(document.getElementById('test22')).font is "" PASS document.getElementById('test1').style.font is "" -PASS document.getElementById('test15').style.font is "italic 100 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test16').style.font is "italic 100 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test17').style.font is "100 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test18').style.font is "italic 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test19').style.font is "italic small-caps 100 extra-expanded 48px / 49px \"Helvetica Neue\"" +PASS document.getElementById('test15').style.font is "italic 100 48px / 49px Helvetica Neue" +PASS document.getElementById('test16').style.font is "italic 100 48px / 49px Helvetica Neue" +PASS document.getElementById('test17').style.font is "100 48px / 49px Helvetica Neue" +PASS document.getElementById('test18').style.font is "italic 48px / 49px Helvetica Neue" +PASS document.getElementById('test19').style.font is "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue" PASS document.getElementById('test20').style.font is "" -PASS document.getElementById('test21').style.font is "oblique 14deg small-caps 123 extra-expanded 48px / 49px \"Helvetica Neue\"" +PASS document.getElementById('test21').style.font is "oblique 14deg small-caps 123 extra-expanded 48px / 49px Helvetica Neue" PASS successfullyParsed is true TEST COMPLETE diff --git a/LayoutTests/fast/text/font-style-parse.html b/LayoutTests/fast/text/font-style-parse.html index 1868728df071..a8e010b6b027 100644 --- a/LayoutTests/fast/text/font-style-parse.html +++ b/LayoutTests/fast/text/font-style-parse.html @@ -66,23 +66,23 @@ shouldBeEqualToString("getComputedStyle(document.getElementById('test12')).font", "16px / 18px Times"); shouldBeEqualToString("getComputedStyle(document.getElementById('test13')).font", "16px / 18px Times"); shouldBeEqualToString("getComputedStyle(document.getElementById('test14')).font", ""); -shouldBeEqualToString("getComputedStyle(document.getElementById('test15')).font", `italic 100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test16')).font", `italic 100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test17')).font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test18')).font", `italic 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("getComputedStyle(document.getElementById('test19')).font", `italic small-caps 100 extra-expanded 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("getComputedStyle(document.getElementById('test15')).font", "italic 100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test16')).font", "italic 100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test17')).font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test18')).font", "italic 48px / 49px Helvetica Neue"); +shouldBeEqualToString("getComputedStyle(document.getElementById('test19')).font", "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue"); shouldBeEqualToString("getComputedStyle(document.getElementById('test20')).font", "16px / 18px Times"); -shouldBeEqualToString("getComputedStyle(document.getElementById('test21')).font", `oblique small-caps 123 extra-expanded 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("getComputedStyle(document.getElementById('test21')).font", "oblique small-caps 123 extra-expanded 48px / 49px Helvetica Neue"); shouldBeEqualToString("getComputedStyle(document.getElementById('test22')).font", ""); shouldBeEqualToString("document.getElementById('test1').style.font", ""); -shouldBeEqualToString("document.getElementById('test15').style.font", `italic 100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test16').style.font", `italic 100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test17').style.font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test18').style.font", `italic 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test19').style.font", `italic small-caps 100 extra-expanded 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("document.getElementById('test15').style.font", "italic 100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test16').style.font", "italic 100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test17').style.font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test18').style.font", "italic 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test19').style.font", "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue"); shouldBeEqualToString("document.getElementById('test20').style.font", ""); -shouldBeEqualToString("document.getElementById('test21').style.font", `oblique 14deg small-caps 123 extra-expanded 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("document.getElementById('test21').style.font", "oblique 14deg small-caps 123 extra-expanded 48px / 49px Helvetica Neue"); diff --git a/LayoutTests/fast/text/font-weight-parse-expected.txt b/LayoutTests/fast/text/font-weight-parse-expected.txt index 4b5592f58e64..8ced984985d9 100644 --- a/LayoutTests/fast/text/font-weight-parse-expected.txt +++ b/LayoutTests/fast/text/font-weight-parse-expected.txt @@ -45,24 +45,24 @@ PASS window.getComputedStyle(document.getElementById('test15')).font is "16px / PASS window.getComputedStyle(document.getElementById('test16')).font is "7 16px / 18px Times" PASS window.getComputedStyle(document.getElementById('test17')).font is "300 16px / 18px Times" PASS window.getComputedStyle(document.getElementById('test18')).font is "200 16px / 18px Times" -PASS window.getComputedStyle(document.getElementById('test19')).font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS window.getComputedStyle(document.getElementById('test20')).font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS window.getComputedStyle(document.getElementById('test21')).font is "extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS window.getComputedStyle(document.getElementById('test22')).font is "100 48px / 49px \"Helvetica Neue\"" -PASS window.getComputedStyle(document.getElementById('test23')).font is "italic small-caps 100 extra-expanded 48px / 49px \"Helvetica Neue\"" -FAIL window.getComputedStyle(document.getElementById('test24')).font should be 123 48px / 49px "Helvetica Neue". Was italic small-caps 123 extra-expanded 48px / 49px "Helvetica Neue". +PASS window.getComputedStyle(document.getElementById('test19')).font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS window.getComputedStyle(document.getElementById('test20')).font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS window.getComputedStyle(document.getElementById('test21')).font is "extra-condensed 48px / 49px Helvetica Neue" +PASS window.getComputedStyle(document.getElementById('test22')).font is "100 48px / 49px Helvetica Neue" +PASS window.getComputedStyle(document.getElementById('test23')).font is "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue" +FAIL window.getComputedStyle(document.getElementById('test24')).font should be 123 48px / 49px Helvetica Neue. Was italic small-caps 123 extra-expanded 48px / 49px Helvetica Neue. PASS window.getComputedStyle(document.getElementById('test25')).font is "16px / 18px Times" PASS window.getComputedStyle(document.getElementById('test26')).font is "16px / 18px Times" PASS window.getComputedStyle(document.getElementById('test27')).font is "16px / 18px Times" PASS window.getComputedStyle(document.getElementById('test28')).font is "1 16px / 18px Times" PASS window.getComputedStyle(document.getElementById('test29')).font is "1000 16px / 18px Times" PASS document.getElementById('test1').style.font is "" -PASS document.getElementById('test19').style.font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test20').style.font is "100 extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test21').style.font is "extra-condensed 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test22').style.font is "100 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test23').style.font is "italic small-caps 100 extra-expanded 48px / 49px \"Helvetica Neue\"" -PASS document.getElementById('test24').style.font is "italic small-caps 123 extra-expanded 48px / 49px \"Helvetica Neue\"" +PASS document.getElementById('test19').style.font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS document.getElementById('test20').style.font is "100 extra-condensed 48px / 49px Helvetica Neue" +PASS document.getElementById('test21').style.font is "extra-condensed 48px / 49px Helvetica Neue" +PASS document.getElementById('test22').style.font is "100 48px / 49px Helvetica Neue" +PASS document.getElementById('test23').style.font is "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue" +PASS document.getElementById('test24').style.font is "italic small-caps 123 extra-expanded 48px / 49px Helvetica Neue" PASS successfullyParsed is true Some tests failed. diff --git a/LayoutTests/fast/text/font-weight-parse.html b/LayoutTests/fast/text/font-weight-parse.html index 5478aa82338d..84ab6c7173c0 100644 --- a/LayoutTests/fast/text/font-weight-parse.html +++ b/LayoutTests/fast/text/font-weight-parse.html @@ -84,12 +84,12 @@ shouldBeEqualToString("window.getComputedStyle(document.getElementById('test16')).font", "7 16px / 18px Times"); shouldBeEqualToString("window.getComputedStyle(document.getElementById('test17')).font", "300 16px / 18px Times"); shouldBeEqualToString("window.getComputedStyle(document.getElementById('test18')).font", "200 16px / 18px Times"); -shouldBeEqualToString("window.getComputedStyle(document.getElementById('test19')).font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("window.getComputedStyle(document.getElementById('test20')).font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("window.getComputedStyle(document.getElementById('test21')).font", `extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("window.getComputedStyle(document.getElementById('test22')).font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("window.getComputedStyle(document.getElementById('test23')).font", `italic small-caps 100 extra-expanded 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("window.getComputedStyle(document.getElementById('test24')).font", `123 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("window.getComputedStyle(document.getElementById('test19')).font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("window.getComputedStyle(document.getElementById('test20')).font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("window.getComputedStyle(document.getElementById('test21')).font", "extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("window.getComputedStyle(document.getElementById('test22')).font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("window.getComputedStyle(document.getElementById('test23')).font", "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue"); +shouldBeEqualToString("window.getComputedStyle(document.getElementById('test24')).font", "123 48px / 49px Helvetica Neue"); shouldBeEqualToString("window.getComputedStyle(document.getElementById('test25')).font", "16px / 18px Times"); shouldBeEqualToString("window.getComputedStyle(document.getElementById('test26')).font", "16px / 18px Times"); shouldBeEqualToString("window.getComputedStyle(document.getElementById('test27')).font", "16px / 18px Times"); @@ -97,12 +97,12 @@ shouldBeEqualToString("window.getComputedStyle(document.getElementById('test29')).font", "1000 16px / 18px Times"); shouldBeEqualToString("document.getElementById('test1').style.font", ""); -shouldBeEqualToString("document.getElementById('test19').style.font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test20').style.font", `100 extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test21').style.font", `extra-condensed 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test22').style.font", `100 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test23').style.font", `italic small-caps 100 extra-expanded 48px / 49px "Helvetica Neue"`); -shouldBeEqualToString("document.getElementById('test24').style.font", `italic small-caps 123 extra-expanded 48px / 49px "Helvetica Neue"`); +shouldBeEqualToString("document.getElementById('test19').style.font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test20').style.font", "100 extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test21').style.font", "extra-condensed 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test22').style.font", "100 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test23').style.font", "italic small-caps 100 extra-expanded 48px / 49px Helvetica Neue"); +shouldBeEqualToString("document.getElementById('test24').style.font", "italic small-caps 123 extra-expanded 48px / 49px Helvetica Neue"); diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative-expected.txt index c450063fa774..06d84dcc60e1 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative-expected.txt @@ -5,4 +5,7 @@ PASS family: A, B PASS family: inherit PASS family: a 1 PASS family: +PASS family: a b +PASS family: a b +PASS family: a b diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative.html index 893e459e7805..fa27398ddb6a 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative.html +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-font-loading/fontface-invalid-family.tentative.html @@ -16,6 +16,9 @@ "inherit", "a 1", "", + "a b", + " a b", + "a b ", ]; for (let familyName of kInvalidValues) { diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-computed-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-computed-expected.txt index 91466348f47f..72ec11c8c9c0 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-computed-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-computed-expected.txt @@ -11,307 +11,307 @@ PASS Property font value 'normal normal xx-large/1.2 cursive' PASS Property font value 'normal normal normal larger/calc(120% + 1.2em) fantasy' PASS Property font value 'normal normal normal normal smaller monospace' PASS Property font value 'normal normal normal italic 10px/normal Menu' -FAIL Property font value 'normal normal normal small-caps 20%/1.2 Non-Generic Example Family Name' assert_equals: expected "small-caps 8px / 9.6px Non-Generic Example Family Name" but got "small-caps 8px / 9.6px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal normal normal small-caps 20%/1.2 Non-Generic Example Family Name' PASS Property font value 'normal normal normal bold calc(30% - 40px)/calc(120% + 1.2em) serif' PASS Property font value 'normal normal normal ultra-condensed xx-small sans-serif' PASS Property font value 'normal normal italic medium/normal cursive' PASS Property font value 'normal normal italic normal xx-large/1.2 fantasy' PASS Property font value 'normal normal italic small-caps larger/calc(120% + 1.2em) monospace' PASS Property font value 'normal normal italic bolder smaller Menu' -FAIL Property font value 'normal normal italic extra-condensed 10px/normal Non-Generic Example Family Name' assert_equals: expected "italic extra-condensed 10px Non-Generic Example Family Name" but got "italic extra-condensed 10px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal normal italic extra-condensed 10px/normal Non-Generic Example Family Name' PASS Property font value 'normal normal small-caps 20%/1.2 serif' PASS Property font value 'normal normal small-caps normal calc(30% - 40px)/calc(120% + 1.2em) sans-serif' PASS Property font value 'normal normal small-caps italic xx-small cursive' PASS Property font value 'normal normal small-caps lighter medium/normal fantasy' PASS Property font value 'normal normal small-caps condensed xx-large/1.2 monospace' PASS Property font value 'normal normal 100 larger/calc(120% + 1.2em) Menu' -FAIL Property font value 'normal normal 900 normal smaller Non-Generic Example Family Name' assert_equals: expected "900 33.333332px Non-Generic Example Family Name" but got "900 33.333332px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal normal 900 normal smaller Non-Generic Example Family Name' PASS Property font value 'normal normal bold italic 10px/normal serif' PASS Property font value 'normal normal bolder small-caps 20%/1.2 sans-serif' PASS Property font value 'normal normal lighter semi-condensed calc(30% - 40px)/calc(120% + 1.2em) cursive' PASS Property font value 'normal normal semi-expanded xx-small fantasy' PASS Property font value 'normal normal expanded normal medium/normal monospace' PASS Property font value 'normal normal extra-expanded italic xx-large/1.2 Menu' -FAIL Property font value 'normal normal ultra-expanded small-caps larger/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "small-caps ultra-expanded 48px / 115.199997px Non-Generic Example Family Name" but got "small-caps ultra-expanded 48px / 115.199997px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal normal ultra-expanded small-caps larger/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'normal normal ultra-condensed 100 smaller serif' PASS Property font value 'normal italic 10px/normal sans-serif' PASS Property font value 'normal italic normal 20%/1.2 cursive' PASS Property font value 'normal italic normal normal calc(30% - 40px)/calc(120% + 1.2em) fantasy' PASS Property font value 'normal italic normal small-caps xx-small monospace' PASS Property font value 'normal italic normal 900 medium/normal Menu' -FAIL Property font value 'normal italic normal extra-condensed xx-large/1.2 Non-Generic Example Family Name' assert_equals: expected "italic extra-condensed 32px / 38.400002px Non-Generic Example Family Name" but got "italic extra-condensed 32px / 38.400002px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal italic normal extra-condensed xx-large/1.2 Non-Generic Example Family Name' PASS Property font value 'normal italic small-caps larger/calc(120% + 1.2em) serif' PASS Property font value 'normal italic small-caps normal smaller sans-serif' PASS Property font value 'normal italic small-caps bold 10px/normal cursive' PASS Property font value 'normal italic small-caps condensed 20%/1.2 fantasy' PASS Property font value 'normal italic bolder calc(30% - 40px)/calc(120% + 1.2em) monospace' PASS Property font value 'normal italic lighter normal xx-small Menu' -FAIL Property font value 'normal italic 100 small-caps medium/normal Non-Generic Example Family Name' assert_equals: expected "italic small-caps 100 16px Non-Generic Example Family Name" but got "italic small-caps 100 16px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal italic 100 small-caps medium/normal Non-Generic Example Family Name' PASS Property font value 'normal italic 900 semi-condensed xx-large/1.2 serif' PASS Property font value 'normal italic semi-expanded larger/calc(120% + 1.2em) sans-serif' PASS Property font value 'normal italic expanded normal smaller cursive' PASS Property font value 'normal italic extra-expanded small-caps 10px/normal fantasy' PASS Property font value 'normal italic ultra-expanded bold 20%/1.2 monospace' PASS Property font value 'normal small-caps calc(30% - 40px)/calc(120% + 1.2em) Menu' -FAIL Property font value 'normal small-caps normal xx-small Non-Generic Example Family Name' assert_equals: expected "small-caps 9px Non-Generic Example Family Name" but got "small-caps 9px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal small-caps normal xx-small Non-Generic Example Family Name' PASS Property font value 'normal small-caps normal normal medium/normal serif' PASS Property font value 'normal small-caps normal italic xx-large/1.2 sans-serif' PASS Property font value 'normal small-caps normal bolder larger/calc(120% + 1.2em) cursive' PASS Property font value 'normal small-caps normal ultra-condensed smaller fantasy' PASS Property font value 'normal small-caps italic 10px/normal monospace' PASS Property font value 'normal small-caps italic normal 20%/1.2 Menu' -FAIL Property font value 'normal small-caps italic lighter calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "italic small-caps 700 0px / 0px Non-Generic Example Family Name" but got "italic small-caps 700 0px / 0px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal small-caps italic lighter calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'normal small-caps italic extra-condensed xx-small serif' PASS Property font value 'normal small-caps 100 medium/normal sans-serif' PASS Property font value 'normal small-caps 900 normal xx-large/1.2 cursive' PASS Property font value 'normal small-caps bold italic larger/calc(120% + 1.2em) fantasy' PASS Property font value 'normal small-caps bolder condensed smaller monospace' PASS Property font value 'normal small-caps semi-condensed 10px/normal Menu' -FAIL Property font value 'normal small-caps semi-expanded normal 20%/1.2 Non-Generic Example Family Name' assert_equals: expected "small-caps semi-expanded 8px / 9.6px Non-Generic Example Family Name" but got "small-caps semi-expanded 8px / 9.6px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal small-caps semi-expanded normal 20%/1.2 Non-Generic Example Family Name' PASS Property font value 'normal small-caps expanded italic calc(30% - 40px)/calc(120% + 1.2em) serif' PASS Property font value 'normal small-caps extra-expanded lighter xx-small sans-serif' PASS Property font value 'normal 100 medium/normal cursive' PASS Property font value 'normal 900 normal xx-large/1.2 fantasy' PASS Property font value 'normal bold normal normal larger/calc(120% + 1.2em) monospace' PASS Property font value 'normal bolder normal italic smaller Menu' -FAIL Property font value 'normal lighter normal small-caps 10px/normal Non-Generic Example Family Name' assert_equals: expected "small-caps 700 10px Non-Generic Example Family Name" but got "small-caps 700 10px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal lighter normal small-caps 10px/normal Non-Generic Example Family Name' PASS Property font value 'normal 100 normal ultra-expanded 20%/1.2 serif' PASS Property font value 'normal 900 italic calc(30% - 40px)/calc(120% + 1.2em) sans-serif' PASS Property font value 'normal bold italic normal xx-small cursive' PASS Property font value 'normal bolder italic small-caps medium/normal fantasy' PASS Property font value 'normal lighter italic ultra-condensed xx-large/1.2 monospace' PASS Property font value 'normal 100 small-caps larger/calc(120% + 1.2em) Menu' -FAIL Property font value 'normal 900 small-caps normal smaller Non-Generic Example Family Name' assert_equals: expected "small-caps 900 33.333332px Non-Generic Example Family Name" but got "small-caps 900 33.333332px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal 900 small-caps normal smaller Non-Generic Example Family Name' PASS Property font value 'normal bold small-caps italic 10px/normal serif' PASS Property font value 'normal bolder small-caps extra-condensed 20%/1.2 sans-serif' PASS Property font value 'normal lighter condensed calc(30% - 40px)/calc(120% + 1.2em) cursive' PASS Property font value 'normal 100 semi-condensed normal xx-small fantasy' PASS Property font value 'normal 900 semi-expanded italic medium/normal monospace' PASS Property font value 'normal bold expanded small-caps xx-large/1.2 Menu' -FAIL Property font value 'normal extra-expanded larger/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "extra-expanded 48px / 115.199997px Non-Generic Example Family Name" but got "extra-expanded 48px / 115.199997px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal extra-expanded larger/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'normal ultra-expanded normal smaller serif' PASS Property font value 'normal ultra-condensed normal normal 10px/normal sans-serif' PASS Property font value 'normal extra-condensed normal italic 20%/1.2 cursive' PASS Property font value 'normal condensed normal small-caps calc(30% - 40px)/calc(120% + 1.2em) fantasy' PASS Property font value 'normal semi-condensed normal bolder xx-small monospace' PASS Property font value 'normal semi-expanded italic medium/normal Menu' -FAIL Property font value 'normal expanded italic normal xx-large/1.2 Non-Generic Example Family Name' assert_equals: expected "italic expanded 32px / 38.400002px Non-Generic Example Family Name" but got "italic expanded 32px / 38.400002px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal expanded italic normal xx-large/1.2 Non-Generic Example Family Name' PASS Property font value 'normal extra-expanded italic small-caps larger/calc(120% + 1.2em) serif' PASS Property font value 'normal ultra-expanded italic lighter smaller sans-serif' PASS Property font value 'normal ultra-condensed small-caps 10px/normal cursive' PASS Property font value 'normal extra-condensed small-caps normal 20%/1.2 fantasy' PASS Property font value 'normal condensed small-caps italic calc(30% - 40px)/calc(120% + 1.2em) monospace' PASS Property font value 'normal semi-condensed small-caps 100 xx-small Menu' -FAIL Property font value 'normal semi-expanded 900 medium/normal Non-Generic Example Family Name' assert_equals: expected "900 semi-expanded 16px Non-Generic Example Family Name" but got "900 semi-expanded 16px \"Non-Generic Example Family Name\"" +PASS Property font value 'normal semi-expanded 900 medium/normal Non-Generic Example Family Name' PASS Property font value 'normal expanded bold normal xx-large/1.2 serif' PASS Property font value 'normal extra-expanded bolder italic larger/calc(120% + 1.2em) sans-serif' PASS Property font value 'normal ultra-expanded lighter small-caps smaller cursive' PASS Property font value 'italic 10px/normal fantasy' PASS Property font value 'italic normal 20%/1.2 monospace' PASS Property font value 'italic normal normal calc(30% - 40px)/calc(120% + 1.2em) Menu' -FAIL Property font value 'italic normal normal normal xx-small Non-Generic Example Family Name' assert_equals: expected "italic 9px Non-Generic Example Family Name" but got "italic 9px \"Non-Generic Example Family Name\"" +PASS Property font value 'italic normal normal normal xx-small Non-Generic Example Family Name' PASS Property font value 'italic normal normal small-caps medium/normal serif' PASS Property font value 'italic normal normal 100 xx-large/1.2 sans-serif' PASS Property font value 'italic normal normal ultra-condensed larger/calc(120% + 1.2em) cursive' PASS Property font value 'italic normal small-caps smaller fantasy' PASS Property font value 'italic normal small-caps normal 10px/normal monospace' PASS Property font value 'italic normal small-caps 900 20%/1.2 Menu' -FAIL Property font value 'italic normal small-caps extra-condensed calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "italic small-caps extra-condensed 0px / 0px Non-Generic Example Family Name" but got "italic small-caps extra-condensed 0px / 0px \"Non-Generic Example Family Name\"" +PASS Property font value 'italic normal small-caps extra-condensed calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'italic normal bold xx-small serif' PASS Property font value 'italic normal bolder normal medium/normal sans-serif' PASS Property font value 'italic normal lighter small-caps xx-large/1.2 cursive' PASS Property font value 'italic normal 100 condensed larger/calc(120% + 1.2em) fantasy' PASS Property font value 'italic normal semi-condensed smaller monospace' PASS Property font value 'italic normal semi-expanded normal 10px/normal Menu' -FAIL Property font value 'italic normal expanded small-caps 20%/1.2 Non-Generic Example Family Name' assert_equals: expected "italic small-caps expanded 8px / 9.6px Non-Generic Example Family Name" but got "italic small-caps expanded 8px / 9.6px \"Non-Generic Example Family Name\"" +PASS Property font value 'italic normal expanded small-caps 20%/1.2 Non-Generic Example Family Name' PASS Property font value 'italic normal extra-expanded 900 calc(30% - 40px)/calc(120% + 1.2em) serif' PASS Property font value 'italic small-caps xx-small sans-serif' PASS Property font value 'italic small-caps normal medium/normal cursive' PASS Property font value 'italic small-caps normal normal xx-large/1.2 fantasy' PASS Property font value 'italic small-caps normal bold larger/calc(120% + 1.2em) monospace' PASS Property font value 'italic small-caps normal ultra-expanded smaller Menu' -FAIL Property font value 'italic small-caps bolder 10px/normal Non-Generic Example Family Name' assert_equals: expected "italic small-caps 900 10px Non-Generic Example Family Name" but got "italic small-caps 900 10px \"Non-Generic Example Family Name\"" +PASS Property font value 'italic small-caps bolder 10px/normal Non-Generic Example Family Name' PASS Property font value 'italic small-caps lighter normal 20%/1.2 serif' PASS Property font value 'italic small-caps 100 ultra-condensed calc(30% - 40px)/calc(120% + 1.2em) sans-serif' PASS Property font value 'italic small-caps extra-condensed xx-small cursive' PASS Property font value 'italic small-caps condensed normal medium/normal fantasy' PASS Property font value 'italic small-caps semi-condensed 900 xx-large/1.2 monospace' PASS Property font value 'italic bold larger/calc(120% + 1.2em) Menu' -FAIL Property font value 'italic bolder normal smaller Non-Generic Example Family Name' assert_equals: expected "italic 900 33.333332px Non-Generic Example Family Name" but got "italic 900 33.333332px \"Non-Generic Example Family Name\"" +PASS Property font value 'italic bolder normal smaller Non-Generic Example Family Name' PASS Property font value 'italic lighter normal normal 10px/normal serif' PASS Property font value 'italic 100 normal small-caps 20%/1.2 sans-serif' PASS Property font value 'italic 900 normal semi-expanded calc(30% - 40px)/calc(120% + 1.2em) cursive' PASS Property font value 'italic bold small-caps xx-small fantasy' PASS Property font value 'italic bolder small-caps normal medium/normal monospace' PASS Property font value 'italic lighter small-caps expanded xx-large/1.2 Menu' -FAIL Property font value 'italic 100 extra-expanded larger/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "italic 100 extra-expanded 48px / 115.199997px Non-Generic Example Family Name" but got "italic 100 extra-expanded 48px / 115.199997px \"Non-Generic Example Family Name\"" +PASS Property font value 'italic 100 extra-expanded larger/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'italic 900 ultra-expanded normal smaller serif' PASS Property font value 'italic bold ultra-condensed small-caps 10px/normal sans-serif' PASS Property font value 'italic extra-condensed 20%/1.2 cursive' PASS Property font value 'italic condensed normal calc(30% - 40px)/calc(120% + 1.2em) fantasy' PASS Property font value 'italic semi-condensed normal normal xx-small monospace' PASS Property font value 'italic semi-expanded normal small-caps medium/normal Menu' -FAIL Property font value 'italic expanded normal bolder xx-large/1.2 Non-Generic Example Family Name' assert_equals: expected "italic 900 expanded 32px / 38.400002px Non-Generic Example Family Name" but got "italic 900 expanded 32px / 38.400002px \"Non-Generic Example Family Name\"" +PASS Property font value 'italic expanded normal bolder xx-large/1.2 Non-Generic Example Family Name' PASS Property font value 'italic extra-expanded small-caps larger/calc(120% + 1.2em) serif' PASS Property font value 'italic ultra-expanded small-caps normal smaller sans-serif' PASS Property font value 'italic ultra-condensed small-caps lighter 10px/normal cursive' PASS Property font value 'italic extra-condensed 100 20%/1.2 fantasy' PASS Property font value 'italic condensed 900 normal calc(30% - 40px)/calc(120% + 1.2em) monospace' PASS Property font value 'italic semi-condensed bold small-caps xx-small Menu' -FAIL Property font value 'small-caps medium/normal Non-Generic Example Family Name' assert_equals: expected "small-caps 16px Non-Generic Example Family Name" but got "small-caps 16px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps medium/normal Non-Generic Example Family Name' PASS Property font value 'small-caps normal xx-large/1.2 serif' PASS Property font value 'small-caps normal normal larger/calc(120% + 1.2em) sans-serif' PASS Property font value 'small-caps normal normal normal smaller cursive' PASS Property font value 'small-caps normal normal italic 10px/normal fantasy' PASS Property font value 'small-caps normal normal bolder 20%/1.2 monospace' PASS Property font value 'small-caps normal normal semi-expanded calc(30% - 40px)/calc(120% + 1.2em) Menu' -FAIL Property font value 'small-caps normal italic xx-small Non-Generic Example Family Name' assert_equals: expected "italic small-caps 9px Non-Generic Example Family Name" but got "italic small-caps 9px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps normal italic xx-small Non-Generic Example Family Name' PASS Property font value 'small-caps normal italic normal medium/normal serif' PASS Property font value 'small-caps normal italic lighter xx-large/1.2 sans-serif' PASS Property font value 'small-caps normal italic expanded larger/calc(120% + 1.2em) cursive' PASS Property font value 'small-caps normal 100 smaller fantasy' PASS Property font value 'small-caps normal 900 normal 10px/normal monospace' PASS Property font value 'small-caps normal bold italic 20%/1.2 Menu' -FAIL Property font value 'small-caps normal bolder extra-expanded calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "small-caps 900 extra-expanded 0px / 0px Non-Generic Example Family Name" but got "small-caps 900 extra-expanded 0px / 0px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps normal bolder extra-expanded calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'small-caps normal ultra-expanded xx-small serif' PASS Property font value 'small-caps normal ultra-condensed normal medium/normal sans-serif' PASS Property font value 'small-caps normal extra-condensed italic xx-large/1.2 cursive' PASS Property font value 'small-caps normal condensed lighter larger/calc(120% + 1.2em) fantasy' PASS Property font value 'small-caps italic smaller monospace' PASS Property font value 'small-caps italic normal 10px/normal Menu' -FAIL Property font value 'small-caps italic normal normal 20%/1.2 Non-Generic Example Family Name' assert_equals: expected "italic small-caps 8px / 9.6px Non-Generic Example Family Name" but got "italic small-caps 8px / 9.6px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps italic normal normal 20%/1.2 Non-Generic Example Family Name' PASS Property font value 'small-caps italic normal 100 calc(30% - 40px)/calc(120% + 1.2em) serif' PASS Property font value 'small-caps italic normal semi-condensed xx-small sans-serif' PASS Property font value 'small-caps italic 900 medium/normal cursive' PASS Property font value 'small-caps italic bold normal xx-large/1.2 fantasy' PASS Property font value 'small-caps italic bolder semi-expanded larger/calc(120% + 1.2em) monospace' PASS Property font value 'small-caps italic expanded smaller Menu' -FAIL Property font value 'small-caps italic extra-expanded normal 10px/normal Non-Generic Example Family Name' assert_equals: expected "italic small-caps extra-expanded 10px Non-Generic Example Family Name" but got "italic small-caps extra-expanded 10px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps italic extra-expanded normal 10px/normal Non-Generic Example Family Name' PASS Property font value 'small-caps italic ultra-expanded lighter 20%/1.2 serif' PASS Property font value 'small-caps 100 calc(30% - 40px)/calc(120% + 1.2em) sans-serif' PASS Property font value 'small-caps 900 normal xx-small cursive' PASS Property font value 'small-caps bold normal normal medium/normal fantasy' PASS Property font value 'small-caps bolder normal italic xx-large/1.2 monospace' PASS Property font value 'small-caps lighter normal ultra-condensed larger/calc(120% + 1.2em) Menu' -FAIL Property font value 'small-caps 100 italic smaller Non-Generic Example Family Name' assert_equals: expected "italic small-caps 100 33.333332px Non-Generic Example Family Name" but got "italic small-caps 100 33.333332px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps 100 italic smaller Non-Generic Example Family Name' PASS Property font value 'small-caps 900 italic normal 10px/normal serif' PASS Property font value 'small-caps bold italic extra-condensed 20%/1.2 sans-serif' PASS Property font value 'small-caps bolder condensed calc(30% - 40px)/calc(120% + 1.2em) cursive' PASS Property font value 'small-caps lighter semi-condensed normal xx-small fantasy' PASS Property font value 'small-caps 100 semi-expanded italic medium/normal monospace' PASS Property font value 'small-caps expanded xx-large/1.2 Menu' -FAIL Property font value 'small-caps extra-expanded normal larger/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "small-caps extra-expanded 48px / 115.199997px Non-Generic Example Family Name" but got "small-caps extra-expanded 48px / 115.199997px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps extra-expanded normal larger/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'small-caps ultra-expanded normal normal smaller serif' PASS Property font value 'small-caps ultra-condensed normal italic 10px/normal sans-serif' PASS Property font value 'small-caps extra-condensed normal 900 20%/1.2 cursive' PASS Property font value 'small-caps condensed italic calc(30% - 40px)/calc(120% + 1.2em) fantasy' PASS Property font value 'small-caps semi-condensed italic normal xx-small monospace' PASS Property font value 'small-caps semi-expanded italic bold medium/normal Menu' -FAIL Property font value 'small-caps expanded bolder xx-large/1.2 Non-Generic Example Family Name' assert_equals: expected "small-caps 900 expanded 32px / 38.400002px Non-Generic Example Family Name" but got "small-caps 900 expanded 32px / 38.400002px \"Non-Generic Example Family Name\"" +PASS Property font value 'small-caps expanded bolder xx-large/1.2 Non-Generic Example Family Name' PASS Property font value 'small-caps extra-expanded lighter normal larger/calc(120% + 1.2em) serif' PASS Property font value 'small-caps ultra-expanded 100 italic smaller sans-serif' PASS Property font value '900 10px/normal cursive' PASS Property font value 'bold normal 20%/1.2 fantasy' PASS Property font value 'bolder normal normal calc(30% - 40px)/calc(120% + 1.2em) monospace' PASS Property font value 'lighter normal normal normal xx-small Menu' -FAIL Property font value '100 normal normal italic medium/normal Non-Generic Example Family Name' assert_equals: expected "italic 100 16px Non-Generic Example Family Name" but got "italic 100 16px \"Non-Generic Example Family Name\"" +PASS Property font value '100 normal normal italic medium/normal Non-Generic Example Family Name' PASS Property font value '900 normal normal small-caps xx-large/1.2 serif' PASS Property font value 'bold normal normal ultra-condensed larger/calc(120% + 1.2em) sans-serif' PASS Property font value 'bolder normal italic smaller cursive' PASS Property font value 'lighter normal italic normal 10px/normal fantasy' PASS Property font value '100 normal italic small-caps 20%/1.2 monospace' PASS Property font value '900 normal italic extra-condensed calc(30% - 40px)/calc(120% + 1.2em) Menu' -FAIL Property font value 'bold normal small-caps xx-small Non-Generic Example Family Name' assert_equals: expected "small-caps 700 9px Non-Generic Example Family Name" but got "small-caps 700 9px \"Non-Generic Example Family Name\"" +PASS Property font value 'bold normal small-caps xx-small Non-Generic Example Family Name' PASS Property font value 'bolder normal small-caps normal medium/normal serif' PASS Property font value 'lighter normal small-caps italic xx-large/1.2 sans-serif' PASS Property font value '100 normal small-caps condensed larger/calc(120% + 1.2em) cursive' PASS Property font value '900 normal semi-condensed smaller fantasy' PASS Property font value 'bold normal semi-expanded normal 10px/normal monospace' PASS Property font value 'bolder normal expanded italic 20%/1.2 Menu' -FAIL Property font value 'lighter normal extra-expanded small-caps calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "small-caps 700 extra-expanded 0px / 0px Non-Generic Example Family Name" but got "small-caps 700 extra-expanded 0px / 0px \"Non-Generic Example Family Name\"" +PASS Property font value 'lighter normal extra-expanded small-caps calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value '100 italic xx-small serif' PASS Property font value '900 italic normal medium/normal sans-serif' PASS Property font value 'bold italic normal normal xx-large/1.2 cursive' PASS Property font value 'bolder italic normal small-caps larger/calc(120% + 1.2em) fantasy' PASS Property font value 'lighter italic normal ultra-expanded smaller monospace' PASS Property font value '100 italic small-caps 10px/normal Menu' -FAIL Property font value '900 italic small-caps normal 20%/1.2 Non-Generic Example Family Name' assert_equals: expected "italic small-caps 900 8px / 9.6px Non-Generic Example Family Name" but got "italic small-caps 900 8px / 9.6px \"Non-Generic Example Family Name\"" +PASS Property font value '900 italic small-caps normal 20%/1.2 Non-Generic Example Family Name' PASS Property font value 'bold italic small-caps ultra-condensed calc(30% - 40px)/calc(120% + 1.2em) serif' PASS Property font value 'bolder italic extra-condensed xx-small sans-serif' PASS Property font value 'lighter italic condensed normal medium/normal cursive' PASS Property font value '100 italic semi-condensed small-caps xx-large/1.2 fantasy' PASS Property font value '900 small-caps larger/calc(120% + 1.2em) monospace' PASS Property font value 'bold small-caps normal smaller Menu' -FAIL Property font value 'bolder small-caps normal normal 10px/normal Non-Generic Example Family Name' assert_equals: expected "small-caps 900 10px Non-Generic Example Family Name" but got "small-caps 900 10px \"Non-Generic Example Family Name\"" +PASS Property font value 'bolder small-caps normal normal 10px/normal Non-Generic Example Family Name' PASS Property font value 'lighter small-caps normal italic 20%/1.2 serif' PASS Property font value '100 small-caps normal semi-expanded calc(30% - 40px)/calc(120% + 1.2em) sans-serif' PASS Property font value '900 small-caps italic xx-small cursive' PASS Property font value 'bold small-caps italic normal medium/normal fantasy' PASS Property font value 'bolder small-caps italic expanded xx-large/1.2 monospace' PASS Property font value 'lighter small-caps extra-expanded larger/calc(120% + 1.2em) Menu' -FAIL Property font value '100 small-caps ultra-expanded normal smaller Non-Generic Example Family Name' assert_equals: expected "small-caps 100 ultra-expanded 33.333332px Non-Generic Example Family Name" but got "small-caps 100 ultra-expanded 33.333332px \"Non-Generic Example Family Name\"" +PASS Property font value '100 small-caps ultra-expanded normal smaller Non-Generic Example Family Name' PASS Property font value '900 small-caps ultra-condensed italic 10px/normal serif' PASS Property font value 'bold extra-condensed 20%/1.2 sans-serif' PASS Property font value 'bolder condensed normal calc(30% - 40px)/calc(120% + 1.2em) cursive' PASS Property font value 'lighter semi-condensed normal normal xx-small fantasy' PASS Property font value '100 semi-expanded normal italic medium/normal monospace' PASS Property font value '900 expanded normal small-caps xx-large/1.2 Menu' -FAIL Property font value 'bold extra-expanded italic larger/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "italic 700 extra-expanded 48px / 115.199997px Non-Generic Example Family Name" but got "italic 700 extra-expanded 48px / 115.199997px \"Non-Generic Example Family Name\"" +PASS Property font value 'bold extra-expanded italic larger/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'bolder ultra-expanded italic normal smaller serif' PASS Property font value 'lighter ultra-condensed italic small-caps 10px/normal sans-serif' PASS Property font value '100 extra-condensed small-caps 20%/1.2 cursive' PASS Property font value '900 condensed small-caps normal calc(30% - 40px)/calc(120% + 1.2em) fantasy' PASS Property font value 'bold semi-condensed small-caps italic xx-small monospace' PASS Property font value 'semi-expanded medium/normal Menu' -FAIL Property font value 'expanded normal xx-large/1.2 Non-Generic Example Family Name' assert_equals: expected "expanded 32px / 38.400002px Non-Generic Example Family Name" but got "expanded 32px / 38.400002px \"Non-Generic Example Family Name\"" +PASS Property font value 'expanded normal xx-large/1.2 Non-Generic Example Family Name' PASS Property font value 'extra-expanded normal normal larger/calc(120% + 1.2em) serif' PASS Property font value 'ultra-expanded normal normal normal smaller sans-serif' PASS Property font value 'ultra-condensed normal normal italic 10px/normal cursive' PASS Property font value 'extra-condensed normal normal small-caps 20%/1.2 fantasy' PASS Property font value 'condensed normal normal bolder calc(30% - 40px)/calc(120% + 1.2em) monospace' PASS Property font value 'semi-condensed normal italic xx-small Menu' -FAIL Property font value 'semi-expanded normal italic normal medium/normal Non-Generic Example Family Name' assert_equals: expected "italic semi-expanded 16px Non-Generic Example Family Name" but got "italic semi-expanded 16px \"Non-Generic Example Family Name\"" +PASS Property font value 'semi-expanded normal italic normal medium/normal Non-Generic Example Family Name' PASS Property font value 'expanded normal italic small-caps xx-large/1.2 serif' PASS Property font value 'extra-expanded normal italic lighter larger/calc(120% + 1.2em) sans-serif' PASS Property font value 'ultra-expanded normal small-caps smaller cursive' PASS Property font value 'ultra-condensed normal small-caps normal 10px/normal fantasy' PASS Property font value 'extra-condensed normal small-caps italic 20%/1.2 monospace' PASS Property font value 'condensed normal small-caps 100 calc(30% - 40px)/calc(120% + 1.2em) Menu' -FAIL Property font value 'semi-condensed normal 900 xx-small Non-Generic Example Family Name' assert_equals: expected "900 semi-condensed 9px Non-Generic Example Family Name" but got "900 semi-condensed 9px \"Non-Generic Example Family Name\"" +PASS Property font value 'semi-condensed normal 900 xx-small Non-Generic Example Family Name' PASS Property font value 'semi-expanded normal bold normal medium/normal serif' PASS Property font value 'expanded normal bolder italic xx-large/1.2 sans-serif' PASS Property font value 'extra-expanded normal lighter small-caps larger/calc(120% + 1.2em) cursive' PASS Property font value 'ultra-expanded italic smaller fantasy' PASS Property font value 'ultra-condensed italic normal 10px/normal monospace' PASS Property font value 'extra-condensed italic normal normal 20%/1.2 Menu' -FAIL Property font value 'condensed italic normal small-caps calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "italic small-caps condensed 0px / 0px Non-Generic Example Family Name" but got "italic small-caps condensed 0px / 0px \"Non-Generic Example Family Name\"" +PASS Property font value 'condensed italic normal small-caps calc(30% - 40px)/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'semi-condensed italic normal 100 xx-small serif' PASS Property font value 'semi-expanded italic small-caps medium/normal sans-serif' PASS Property font value 'expanded italic small-caps normal xx-large/1.2 cursive' PASS Property font value 'extra-expanded italic small-caps 900 larger/calc(120% + 1.2em) fantasy' PASS Property font value 'ultra-expanded italic bold smaller monospace' PASS Property font value 'ultra-condensed italic bolder normal 10px/normal Menu' -FAIL Property font value 'extra-condensed italic lighter small-caps 20%/1.2 Non-Generic Example Family Name' assert_equals: expected "italic small-caps 700 extra-condensed 8px / 9.6px Non-Generic Example Family Name" but got "italic small-caps 700 extra-condensed 8px / 9.6px \"Non-Generic Example Family Name\"" +PASS Property font value 'extra-condensed italic lighter small-caps 20%/1.2 Non-Generic Example Family Name' PASS Property font value 'condensed small-caps calc(30% - 40px)/calc(120% + 1.2em) serif' PASS Property font value 'semi-condensed small-caps normal xx-small sans-serif' PASS Property font value 'semi-expanded small-caps normal normal medium/normal cursive' PASS Property font value 'expanded small-caps normal italic xx-large/1.2 fantasy' PASS Property font value 'extra-expanded small-caps normal 100 larger/calc(120% + 1.2em) monospace' PASS Property font value 'ultra-expanded small-caps italic smaller Menu' -FAIL Property font value 'ultra-condensed small-caps italic normal 10px/normal Non-Generic Example Family Name' assert_equals: expected "italic small-caps ultra-condensed 10px Non-Generic Example Family Name" but got "italic small-caps ultra-condensed 10px \"Non-Generic Example Family Name\"" +PASS Property font value 'ultra-condensed small-caps italic normal 10px/normal Non-Generic Example Family Name' PASS Property font value 'extra-condensed small-caps italic 900 20%/1.2 serif' PASS Property font value 'condensed small-caps bold calc(30% - 40px)/calc(120% + 1.2em) sans-serif' PASS Property font value 'semi-condensed small-caps bolder normal xx-small cursive' PASS Property font value 'semi-expanded small-caps lighter italic medium/normal fantasy' PASS Property font value 'expanded 100 xx-large/1.2 monospace' PASS Property font value 'extra-expanded 900 normal larger/calc(120% + 1.2em) Menu' -FAIL Property font value 'ultra-expanded bold normal normal smaller Non-Generic Example Family Name' assert_equals: expected "700 ultra-expanded 33.333332px Non-Generic Example Family Name" but got "700 ultra-expanded 33.333332px \"Non-Generic Example Family Name\"" +PASS Property font value 'ultra-expanded bold normal normal smaller Non-Generic Example Family Name' PASS Property font value 'ultra-condensed bolder normal italic 10px/normal serif' PASS Property font value 'extra-condensed lighter normal small-caps 20%/1.2 sans-serif' PASS Property font value 'condensed 100 italic calc(30% - 40px)/calc(120% + 1.2em) cursive' PASS Property font value 'semi-condensed 900 italic normal xx-small fantasy' PASS Property font value 'semi-expanded bold italic small-caps medium/normal monospace' PASS Property font value 'expanded bolder small-caps xx-large/1.2 Menu' -FAIL Property font value 'extra-expanded lighter small-caps normal larger/calc(120% + 1.2em) Non-Generic Example Family Name' assert_equals: expected "small-caps 700 extra-expanded 48px / 115.199997px Non-Generic Example Family Name" but got "small-caps 700 extra-expanded 48px / 115.199997px \"Non-Generic Example Family Name\"" +PASS Property font value 'extra-expanded lighter small-caps normal larger/calc(120% + 1.2em) Non-Generic Example Family Name' PASS Property font value 'ultra-expanded 100 small-caps italic smaller serif' diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-computed-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-computed-expected.txt index 2b6cff76a785..2ef151907a95 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-computed-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-computed-expected.txt @@ -6,7 +6,7 @@ PASS Property font-family value 'fantasy' PASS Property font-family value 'monospace' PASS Property font-family value 'serif, sans-serif, cursive, fantasy, monospace' PASS Property font-family value 'Helvetica, Verdana, sans-serif' -FAIL Property font-family value '"New Century Schoolbook", serif' assert_equals: expected "New Century Schoolbook, serif" but got "\"New Century Schoolbook\", serif" +PASS Property font-family value '"New Century Schoolbook", serif' PASS Property font-family value '"21st Century", fantasy' PASS Property font-family value '"inherit", "serif"' diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-valid-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-valid-expected.txt index 4a4f3933f5c3..0e37180cbed6 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-valid-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-family-valid-expected.txt @@ -7,7 +7,7 @@ PASS e.style['font-family'] = "Monospace" should set the property value PASS e.style['font-family'] = "System-UI" should set the property value PASS e.style['font-family'] = "serif, sans-serif, cursive, fantasy, monospace, system-ui" should set the property value PASS e.style['font-family'] = "Helvetica, Verdana, sans-serif" should set the property value -FAIL e.style['font-family'] = "\"New Century Schoolbook\", serif" should set the property value assert_equals: serialization should be canonical expected "New Century Schoolbook, serif" but got "\"New Century Schoolbook\", serif" +PASS e.style['font-family'] = "\"New Century Schoolbook\", serif" should set the property value PASS e.style['font-family'] = "'21st Century', fantasy" should set the property value PASS e.style['font-family'] = "\"inherit\", \"serif\"" should set the property value diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-valid-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-valid-expected.txt index c1a5fcd5d325..305045c7a0de 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-valid-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/parsing/font-valid-expected.txt @@ -11,307 +11,307 @@ PASS e.style['font'] = "normal normal xx-large/1.2 cursive" should set the prope PASS e.style['font'] = "normal normal normal larger/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "normal normal normal normal smaller monospace" should set the property value PASS e.style['font'] = "normal normal normal italic 10px/normal Menu" should set the property value -FAIL e.style['font'] = "normal normal normal small-caps 20%/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps 20% / 1.2 FB Armada" but got "small-caps 20% / 1.2 \"FB Armada\"" +PASS e.style['font'] = "normal normal normal small-caps 20%/1.2 FB Armada" should set the property value PASS e.style['font'] = "normal normal normal bold calc(30% - 40px)/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "normal normal normal ultra-condensed xx-small sans-serif" should set the property value PASS e.style['font'] = "normal normal oblique medium/normal cursive" should set the property value PASS e.style['font'] = "normal normal italic normal xx-large/1.2 fantasy" should set the property value PASS e.style['font'] = "normal normal oblique small-caps larger/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "normal normal italic bolder smaller Menu" should set the property value -FAIL e.style['font'] = "normal normal oblique extra-condensed 10px/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique extra-condensed 10px FB Armada" but got "oblique extra-condensed 10px \"FB Armada\"" +PASS e.style['font'] = "normal normal oblique extra-condensed 10px/normal FB Armada" should set the property value PASS e.style['font'] = "normal normal small-caps 20%/1.2 serif" should set the property value PASS e.style['font'] = "normal normal small-caps normal calc(30% - 40px)/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "normal normal small-caps italic xx-small cursive" should set the property value PASS e.style['font'] = "normal normal small-caps lighter medium/normal fantasy" should set the property value PASS e.style['font'] = "normal normal small-caps condensed xx-large/1.2 monospace" should set the property value PASS e.style['font'] = "normal normal 100 larger/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "normal normal 900 normal smaller FB Armada" should set the property value assert_equals: serialization should be canonical expected "900 smaller FB Armada" but got "900 smaller \"FB Armada\"" +PASS e.style['font'] = "normal normal 900 normal smaller FB Armada" should set the property value PASS e.style['font'] = "normal normal bold oblique 10px/normal serif" should set the property value PASS e.style['font'] = "normal normal bolder small-caps 20%/1.2 sans-serif" should set the property value PASS e.style['font'] = "normal normal lighter semi-condensed calc(30% - 40px)/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "normal normal semi-expanded xx-small fantasy" should set the property value PASS e.style['font'] = "normal normal expanded normal medium/normal monospace" should set the property value PASS e.style['font'] = "normal normal extra-expanded italic xx-large/1.2 Menu" should set the property value -FAIL e.style['font'] = "normal normal ultra-expanded small-caps larger/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps ultra-expanded larger / calc(120% + 1.2em) FB Armada" but got "small-caps ultra-expanded larger / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "normal normal ultra-expanded small-caps larger/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "normal normal ultra-condensed 100 smaller serif" should set the property value PASS e.style['font'] = "normal oblique 10px/normal sans-serif" should set the property value PASS e.style['font'] = "normal italic normal 20%/1.2 cursive" should set the property value PASS e.style['font'] = "normal oblique normal normal calc(30% - 40px)/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "normal italic normal small-caps xx-small monospace" should set the property value PASS e.style['font'] = "normal oblique normal 900 medium/normal Menu" should set the property value -FAIL e.style['font'] = "normal italic normal extra-condensed xx-large/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic extra-condensed xx-large / 1.2 FB Armada" but got "italic extra-condensed xx-large / 1.2 \"FB Armada\"" +PASS e.style['font'] = "normal italic normal extra-condensed xx-large/1.2 FB Armada" should set the property value PASS e.style['font'] = "normal oblique small-caps larger/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "normal italic small-caps normal smaller sans-serif" should set the property value PASS e.style['font'] = "normal oblique small-caps bold 10px/normal cursive" should set the property value PASS e.style['font'] = "normal italic small-caps condensed 20%/1.2 fantasy" should set the property value PASS e.style['font'] = "normal oblique bolder calc(30% - 40px)/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "normal italic lighter normal xx-small Menu" should set the property value -FAIL e.style['font'] = "normal oblique 100 small-caps medium/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique small-caps 100 medium FB Armada" but got "oblique small-caps 100 medium \"FB Armada\"" +PASS e.style['font'] = "normal oblique 100 small-caps medium/normal FB Armada" should set the property value PASS e.style['font'] = "normal italic 900 semi-condensed xx-large/1.2 serif" should set the property value PASS e.style['font'] = "normal oblique semi-expanded larger/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "normal italic expanded normal smaller cursive" should set the property value PASS e.style['font'] = "normal oblique extra-expanded small-caps 10px/normal fantasy" should set the property value PASS e.style['font'] = "normal italic ultra-expanded bold 20%/1.2 monospace" should set the property value PASS e.style['font'] = "normal small-caps calc(30% - 40px)/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "normal small-caps normal xx-small FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps xx-small FB Armada" but got "small-caps xx-small \"FB Armada\"" +PASS e.style['font'] = "normal small-caps normal xx-small FB Armada" should set the property value PASS e.style['font'] = "normal small-caps normal normal medium/normal serif" should set the property value PASS e.style['font'] = "normal small-caps normal oblique xx-large/1.2 sans-serif" should set the property value PASS e.style['font'] = "normal small-caps normal bolder larger/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "normal small-caps normal ultra-condensed smaller fantasy" should set the property value PASS e.style['font'] = "normal small-caps italic 10px/normal monospace" should set the property value PASS e.style['font'] = "normal small-caps oblique normal 20%/1.2 Menu" should set the property value -FAIL e.style['font'] = "normal small-caps italic lighter calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic small-caps lighter calc(30% - 40px) / calc(120% + 1.2em) FB Armada" but got "italic small-caps lighter calc(30% - 40px) / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "normal small-caps italic lighter calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "normal small-caps oblique extra-condensed xx-small serif" should set the property value PASS e.style['font'] = "normal small-caps 100 medium/normal sans-serif" should set the property value PASS e.style['font'] = "normal small-caps 900 normal xx-large/1.2 cursive" should set the property value PASS e.style['font'] = "normal small-caps bold italic larger/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "normal small-caps bolder condensed smaller monospace" should set the property value PASS e.style['font'] = "normal small-caps semi-condensed 10px/normal Menu" should set the property value -FAIL e.style['font'] = "normal small-caps semi-expanded normal 20%/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps semi-expanded 20% / 1.2 FB Armada" but got "small-caps semi-expanded 20% / 1.2 \"FB Armada\"" +PASS e.style['font'] = "normal small-caps semi-expanded normal 20%/1.2 FB Armada" should set the property value PASS e.style['font'] = "normal small-caps expanded oblique calc(30% - 40px)/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "normal small-caps extra-expanded lighter xx-small sans-serif" should set the property value PASS e.style['font'] = "normal 100 medium/normal cursive" should set the property value PASS e.style['font'] = "normal 900 normal xx-large/1.2 fantasy" should set the property value PASS e.style['font'] = "normal bold normal normal larger/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "normal bolder normal italic smaller Menu" should set the property value -FAIL e.style['font'] = "normal lighter normal small-caps 10px/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps lighter 10px FB Armada" but got "small-caps lighter 10px \"FB Armada\"" +PASS e.style['font'] = "normal lighter normal small-caps 10px/normal FB Armada" should set the property value PASS e.style['font'] = "normal 100 normal ultra-expanded 20%/1.2 serif" should set the property value PASS e.style['font'] = "normal 900 oblique calc(30% - 40px)/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "normal bold italic normal xx-small cursive" should set the property value PASS e.style['font'] = "normal bolder oblique small-caps medium/normal fantasy" should set the property value PASS e.style['font'] = "normal lighter italic ultra-condensed xx-large/1.2 monospace" should set the property value PASS e.style['font'] = "normal 100 small-caps larger/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "normal 900 small-caps normal smaller FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps 900 smaller FB Armada" but got "small-caps 900 smaller \"FB Armada\"" +PASS e.style['font'] = "normal 900 small-caps normal smaller FB Armada" should set the property value PASS e.style['font'] = "normal bold small-caps oblique 10px/normal serif" should set the property value PASS e.style['font'] = "normal bolder small-caps extra-condensed 20%/1.2 sans-serif" should set the property value PASS e.style['font'] = "normal lighter condensed calc(30% - 40px)/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "normal 100 semi-condensed normal xx-small fantasy" should set the property value PASS e.style['font'] = "normal 900 semi-expanded italic medium/normal monospace" should set the property value PASS e.style['font'] = "normal bold expanded small-caps xx-large/1.2 Menu" should set the property value -FAIL e.style['font'] = "normal extra-expanded larger/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "extra-expanded larger / calc(120% + 1.2em) FB Armada" but got "extra-expanded larger / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "normal extra-expanded larger/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "normal ultra-expanded normal smaller serif" should set the property value PASS e.style['font'] = "normal ultra-condensed normal normal 10px/normal sans-serif" should set the property value PASS e.style['font'] = "normal extra-condensed normal oblique 20%/1.2 cursive" should set the property value PASS e.style['font'] = "normal condensed normal small-caps calc(30% - 40px)/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "normal semi-condensed normal bolder xx-small monospace" should set the property value PASS e.style['font'] = "normal semi-expanded italic medium/normal Menu" should set the property value -FAIL e.style['font'] = "normal expanded oblique normal xx-large/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique expanded xx-large / 1.2 FB Armada" but got "oblique expanded xx-large / 1.2 \"FB Armada\"" +PASS e.style['font'] = "normal expanded oblique normal xx-large/1.2 FB Armada" should set the property value PASS e.style['font'] = "normal extra-expanded italic small-caps larger/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "normal ultra-expanded oblique lighter smaller sans-serif" should set the property value PASS e.style['font'] = "normal ultra-condensed small-caps 10px/normal cursive" should set the property value PASS e.style['font'] = "normal extra-condensed small-caps normal 20%/1.2 fantasy" should set the property value PASS e.style['font'] = "normal condensed small-caps italic calc(30% - 40px)/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "normal semi-condensed small-caps 100 xx-small Menu" should set the property value -FAIL e.style['font'] = "normal semi-expanded 900 medium/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "900 semi-expanded medium FB Armada" but got "900 semi-expanded medium \"FB Armada\"" +PASS e.style['font'] = "normal semi-expanded 900 medium/normal FB Armada" should set the property value PASS e.style['font'] = "normal expanded bold normal xx-large/1.2 serif" should set the property value PASS e.style['font'] = "normal extra-expanded bolder oblique larger/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "normal ultra-expanded lighter small-caps smaller cursive" should set the property value PASS e.style['font'] = "italic 10px/normal fantasy" should set the property value PASS e.style['font'] = "oblique normal 20%/1.2 monospace" should set the property value PASS e.style['font'] = "italic normal normal calc(30% - 40px)/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "oblique normal normal normal xx-small FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique xx-small FB Armada" but got "oblique xx-small \"FB Armada\"" +PASS e.style['font'] = "oblique normal normal normal xx-small FB Armada" should set the property value PASS e.style['font'] = "italic normal normal small-caps medium/normal serif" should set the property value PASS e.style['font'] = "oblique normal normal 100 xx-large/1.2 sans-serif" should set the property value PASS e.style['font'] = "italic normal normal ultra-condensed larger/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "oblique normal small-caps smaller fantasy" should set the property value PASS e.style['font'] = "italic normal small-caps normal 10px/normal monospace" should set the property value PASS e.style['font'] = "oblique normal small-caps 900 20%/1.2 Menu" should set the property value -FAIL e.style['font'] = "italic normal small-caps extra-condensed calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic small-caps extra-condensed calc(30% - 40px) / calc(120% + 1.2em) FB Armada" but got "italic small-caps extra-condensed calc(30% - 40px) / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "italic normal small-caps extra-condensed calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "oblique normal bold xx-small serif" should set the property value PASS e.style['font'] = "italic normal bolder normal medium/normal sans-serif" should set the property value PASS e.style['font'] = "oblique normal lighter small-caps xx-large/1.2 cursive" should set the property value PASS e.style['font'] = "italic normal 100 condensed larger/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "oblique normal semi-condensed smaller monospace" should set the property value PASS e.style['font'] = "italic normal semi-expanded normal 10px/normal Menu" should set the property value -FAIL e.style['font'] = "oblique normal expanded small-caps 20%/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique small-caps expanded 20% / 1.2 FB Armada" but got "oblique small-caps expanded 20% / 1.2 \"FB Armada\"" +PASS e.style['font'] = "oblique normal expanded small-caps 20%/1.2 FB Armada" should set the property value PASS e.style['font'] = "italic normal extra-expanded 900 calc(30% - 40px)/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "oblique small-caps xx-small sans-serif" should set the property value PASS e.style['font'] = "italic small-caps normal medium/normal cursive" should set the property value PASS e.style['font'] = "oblique small-caps normal normal xx-large/1.2 fantasy" should set the property value PASS e.style['font'] = "italic small-caps normal bold larger/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "oblique small-caps normal ultra-expanded smaller Menu" should set the property value -FAIL e.style['font'] = "italic small-caps bolder 10px/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic small-caps bolder 10px FB Armada" but got "italic small-caps bolder 10px \"FB Armada\"" +PASS e.style['font'] = "italic small-caps bolder 10px/normal FB Armada" should set the property value PASS e.style['font'] = "oblique small-caps lighter normal 20%/1.2 serif" should set the property value PASS e.style['font'] = "italic small-caps 100 ultra-condensed calc(30% - 40px)/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "oblique small-caps extra-condensed xx-small cursive" should set the property value PASS e.style['font'] = "italic small-caps condensed normal medium/normal fantasy" should set the property value PASS e.style['font'] = "oblique small-caps semi-condensed 900 xx-large/1.2 monospace" should set the property value PASS e.style['font'] = "italic bold larger/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "oblique bolder normal smaller FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique bolder smaller FB Armada" but got "oblique bolder smaller \"FB Armada\"" +PASS e.style['font'] = "oblique bolder normal smaller FB Armada" should set the property value PASS e.style['font'] = "italic lighter normal normal 10px/normal serif" should set the property value PASS e.style['font'] = "oblique 100 normal small-caps 20%/1.2 sans-serif" should set the property value PASS e.style['font'] = "italic 900 normal semi-expanded calc(30% - 40px)/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "oblique bold small-caps xx-small fantasy" should set the property value PASS e.style['font'] = "italic bolder small-caps normal medium/normal monospace" should set the property value PASS e.style['font'] = "oblique lighter small-caps expanded xx-large/1.2 Menu" should set the property value -FAIL e.style['font'] = "italic 100 extra-expanded larger/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic 100 extra-expanded larger / calc(120% + 1.2em) FB Armada" but got "italic 100 extra-expanded larger / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "italic 100 extra-expanded larger/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "oblique 900 ultra-expanded normal smaller serif" should set the property value PASS e.style['font'] = "italic bold ultra-condensed small-caps 10px/normal sans-serif" should set the property value PASS e.style['font'] = "oblique extra-condensed 20%/1.2 cursive" should set the property value PASS e.style['font'] = "italic condensed normal calc(30% - 40px)/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "oblique semi-condensed normal normal xx-small monospace" should set the property value PASS e.style['font'] = "italic semi-expanded normal small-caps medium/normal Menu" should set the property value -FAIL e.style['font'] = "oblique expanded normal bolder xx-large/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique bolder expanded xx-large / 1.2 FB Armada" but got "oblique bolder expanded xx-large / 1.2 \"FB Armada\"" +PASS e.style['font'] = "oblique expanded normal bolder xx-large/1.2 FB Armada" should set the property value PASS e.style['font'] = "italic extra-expanded small-caps larger/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "oblique ultra-expanded small-caps normal smaller sans-serif" should set the property value PASS e.style['font'] = "italic ultra-condensed small-caps lighter 10px/normal cursive" should set the property value PASS e.style['font'] = "oblique extra-condensed 100 20%/1.2 fantasy" should set the property value PASS e.style['font'] = "italic condensed 900 normal calc(30% - 40px)/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "oblique semi-condensed bold small-caps xx-small Menu" should set the property value -FAIL e.style['font'] = "small-caps medium/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps medium FB Armada" but got "small-caps medium \"FB Armada\"" +PASS e.style['font'] = "small-caps medium/normal FB Armada" should set the property value PASS e.style['font'] = "small-caps normal xx-large/1.2 serif" should set the property value PASS e.style['font'] = "small-caps normal normal larger/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "small-caps normal normal normal smaller cursive" should set the property value PASS e.style['font'] = "small-caps normal normal italic 10px/normal fantasy" should set the property value PASS e.style['font'] = "small-caps normal normal bolder 20%/1.2 monospace" should set the property value PASS e.style['font'] = "small-caps normal normal semi-expanded calc(30% - 40px)/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "small-caps normal oblique xx-small FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique small-caps xx-small FB Armada" but got "oblique small-caps xx-small \"FB Armada\"" +PASS e.style['font'] = "small-caps normal oblique xx-small FB Armada" should set the property value PASS e.style['font'] = "small-caps normal italic normal medium/normal serif" should set the property value PASS e.style['font'] = "small-caps normal oblique lighter xx-large/1.2 sans-serif" should set the property value PASS e.style['font'] = "small-caps normal italic expanded larger/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "small-caps normal 100 smaller fantasy" should set the property value PASS e.style['font'] = "small-caps normal 900 normal 10px/normal monospace" should set the property value PASS e.style['font'] = "small-caps normal bold oblique 20%/1.2 Menu" should set the property value -FAIL e.style['font'] = "small-caps normal bolder extra-expanded calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps bolder extra-expanded calc(30% - 40px) / calc(120% + 1.2em) FB Armada" but got "small-caps bolder extra-expanded calc(30% - 40px) / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "small-caps normal bolder extra-expanded calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "small-caps normal ultra-expanded xx-small serif" should set the property value PASS e.style['font'] = "small-caps normal ultra-condensed normal medium/normal sans-serif" should set the property value PASS e.style['font'] = "small-caps normal extra-condensed italic xx-large/1.2 cursive" should set the property value PASS e.style['font'] = "small-caps normal condensed lighter larger/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "small-caps oblique smaller monospace" should set the property value PASS e.style['font'] = "small-caps italic normal 10px/normal Menu" should set the property value -FAIL e.style['font'] = "small-caps oblique normal normal 20%/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique small-caps 20% / 1.2 FB Armada" but got "oblique small-caps 20% / 1.2 \"FB Armada\"" +PASS e.style['font'] = "small-caps oblique normal normal 20%/1.2 FB Armada" should set the property value PASS e.style['font'] = "small-caps italic normal 100 calc(30% - 40px)/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "small-caps oblique normal semi-condensed xx-small sans-serif" should set the property value PASS e.style['font'] = "small-caps italic 900 medium/normal cursive" should set the property value PASS e.style['font'] = "small-caps oblique bold normal xx-large/1.2 fantasy" should set the property value PASS e.style['font'] = "small-caps italic bolder semi-expanded larger/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "small-caps oblique expanded smaller Menu" should set the property value -FAIL e.style['font'] = "small-caps italic extra-expanded normal 10px/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic small-caps extra-expanded 10px FB Armada" but got "italic small-caps extra-expanded 10px \"FB Armada\"" +PASS e.style['font'] = "small-caps italic extra-expanded normal 10px/normal FB Armada" should set the property value PASS e.style['font'] = "small-caps oblique ultra-expanded lighter 20%/1.2 serif" should set the property value PASS e.style['font'] = "small-caps 100 calc(30% - 40px)/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "small-caps 900 normal xx-small cursive" should set the property value PASS e.style['font'] = "small-caps bold normal normal medium/normal fantasy" should set the property value PASS e.style['font'] = "small-caps bolder normal italic xx-large/1.2 monospace" should set the property value PASS e.style['font'] = "small-caps lighter normal ultra-condensed larger/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "small-caps 100 oblique smaller FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique small-caps 100 smaller FB Armada" but got "oblique small-caps 100 smaller \"FB Armada\"" +PASS e.style['font'] = "small-caps 100 oblique smaller FB Armada" should set the property value PASS e.style['font'] = "small-caps 900 italic normal 10px/normal serif" should set the property value PASS e.style['font'] = "small-caps bold oblique extra-condensed 20%/1.2 sans-serif" should set the property value PASS e.style['font'] = "small-caps bolder condensed calc(30% - 40px)/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "small-caps lighter semi-condensed normal xx-small fantasy" should set the property value PASS e.style['font'] = "small-caps 100 semi-expanded italic medium/normal monospace" should set the property value PASS e.style['font'] = "small-caps expanded xx-large/1.2 Menu" should set the property value -FAIL e.style['font'] = "small-caps extra-expanded normal larger/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps extra-expanded larger / calc(120% + 1.2em) FB Armada" but got "small-caps extra-expanded larger / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "small-caps extra-expanded normal larger/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "small-caps ultra-expanded normal normal smaller serif" should set the property value PASS e.style['font'] = "small-caps ultra-condensed normal oblique 10px/normal sans-serif" should set the property value PASS e.style['font'] = "small-caps extra-condensed normal 900 20%/1.2 cursive" should set the property value PASS e.style['font'] = "small-caps condensed italic calc(30% - 40px)/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "small-caps semi-condensed oblique normal xx-small monospace" should set the property value PASS e.style['font'] = "small-caps semi-expanded italic bold medium/normal Menu" should set the property value -FAIL e.style['font'] = "small-caps expanded bolder xx-large/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps bolder expanded xx-large / 1.2 FB Armada" but got "small-caps bolder expanded xx-large / 1.2 \"FB Armada\"" +PASS e.style['font'] = "small-caps expanded bolder xx-large/1.2 FB Armada" should set the property value PASS e.style['font'] = "small-caps extra-expanded lighter normal larger/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "small-caps ultra-expanded 100 oblique smaller sans-serif" should set the property value PASS e.style['font'] = "900 10px/normal cursive" should set the property value PASS e.style['font'] = "bold normal 20%/1.2 fantasy" should set the property value PASS e.style['font'] = "bolder normal normal calc(30% - 40px)/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "lighter normal normal normal xx-small Menu" should set the property value -FAIL e.style['font'] = "100 normal normal italic medium/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic 100 medium FB Armada" but got "italic 100 medium \"FB Armada\"" +PASS e.style['font'] = "100 normal normal italic medium/normal FB Armada" should set the property value PASS e.style['font'] = "900 normal normal small-caps xx-large/1.2 serif" should set the property value PASS e.style['font'] = "bold normal normal ultra-condensed larger/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "bolder normal oblique smaller cursive" should set the property value PASS e.style['font'] = "lighter normal italic normal 10px/normal fantasy" should set the property value PASS e.style['font'] = "100 normal oblique small-caps 20%/1.2 monospace" should set the property value PASS e.style['font'] = "900 normal italic extra-condensed calc(30% - 40px)/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "bold normal small-caps xx-small FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps bold xx-small FB Armada" but got "small-caps bold xx-small \"FB Armada\"" +PASS e.style['font'] = "bold normal small-caps xx-small FB Armada" should set the property value PASS e.style['font'] = "bolder normal small-caps normal medium/normal serif" should set the property value PASS e.style['font'] = "lighter normal small-caps oblique xx-large/1.2 sans-serif" should set the property value PASS e.style['font'] = "100 normal small-caps condensed larger/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "900 normal semi-condensed smaller fantasy" should set the property value PASS e.style['font'] = "bold normal semi-expanded normal 10px/normal monospace" should set the property value PASS e.style['font'] = "bolder normal expanded italic 20%/1.2 Menu" should set the property value -FAIL e.style['font'] = "lighter normal extra-expanded small-caps calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps lighter extra-expanded calc(30% - 40px) / calc(120% + 1.2em) FB Armada" but got "small-caps lighter extra-expanded calc(30% - 40px) / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "lighter normal extra-expanded small-caps calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "100 oblique xx-small serif" should set the property value PASS e.style['font'] = "900 italic normal medium/normal sans-serif" should set the property value PASS e.style['font'] = "bold oblique normal normal xx-large/1.2 cursive" should set the property value PASS e.style['font'] = "bolder italic normal small-caps larger/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "lighter oblique normal ultra-expanded smaller monospace" should set the property value PASS e.style['font'] = "100 italic small-caps 10px/normal Menu" should set the property value -FAIL e.style['font'] = "900 oblique small-caps normal 20%/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique small-caps 900 20% / 1.2 FB Armada" but got "oblique small-caps 900 20% / 1.2 \"FB Armada\"" +PASS e.style['font'] = "900 oblique small-caps normal 20%/1.2 FB Armada" should set the property value PASS e.style['font'] = "bold italic small-caps ultra-condensed calc(30% - 40px)/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "bolder oblique extra-condensed xx-small sans-serif" should set the property value PASS e.style['font'] = "lighter italic condensed normal medium/normal cursive" should set the property value PASS e.style['font'] = "100 oblique semi-condensed small-caps xx-large/1.2 fantasy" should set the property value PASS e.style['font'] = "900 small-caps larger/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "bold small-caps normal smaller Menu" should set the property value -FAIL e.style['font'] = "bolder small-caps normal normal 10px/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps bolder 10px FB Armada" but got "small-caps bolder 10px \"FB Armada\"" +PASS e.style['font'] = "bolder small-caps normal normal 10px/normal FB Armada" should set the property value PASS e.style['font'] = "lighter small-caps normal italic 20%/1.2 serif" should set the property value PASS e.style['font'] = "100 small-caps normal semi-expanded calc(30% - 40px)/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "900 small-caps oblique xx-small cursive" should set the property value PASS e.style['font'] = "bold small-caps italic normal medium/normal fantasy" should set the property value PASS e.style['font'] = "bolder small-caps oblique expanded xx-large/1.2 monospace" should set the property value PASS e.style['font'] = "lighter small-caps extra-expanded larger/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "100 small-caps ultra-expanded normal smaller FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps 100 ultra-expanded smaller FB Armada" but got "small-caps 100 ultra-expanded smaller \"FB Armada\"" +PASS e.style['font'] = "100 small-caps ultra-expanded normal smaller FB Armada" should set the property value PASS e.style['font'] = "900 small-caps ultra-condensed italic 10px/normal serif" should set the property value PASS e.style['font'] = "bold extra-condensed 20%/1.2 sans-serif" should set the property value PASS e.style['font'] = "bolder condensed normal calc(30% - 40px)/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "lighter semi-condensed normal normal xx-small fantasy" should set the property value PASS e.style['font'] = "100 semi-expanded normal oblique medium/normal monospace" should set the property value PASS e.style['font'] = "900 expanded normal small-caps xx-large/1.2 Menu" should set the property value -FAIL e.style['font'] = "bold extra-expanded italic larger/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic bold extra-expanded larger / calc(120% + 1.2em) FB Armada" but got "italic bold extra-expanded larger / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "bold extra-expanded italic larger/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "bolder ultra-expanded oblique normal smaller serif" should set the property value PASS e.style['font'] = "lighter ultra-condensed italic small-caps 10px/normal sans-serif" should set the property value PASS e.style['font'] = "100 extra-condensed small-caps 20%/1.2 cursive" should set the property value PASS e.style['font'] = "900 condensed small-caps normal calc(30% - 40px)/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "bold semi-condensed small-caps oblique xx-small monospace" should set the property value PASS e.style['font'] = "semi-expanded medium/normal Menu" should set the property value -FAIL e.style['font'] = "expanded normal xx-large/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "expanded xx-large / 1.2 FB Armada" but got "expanded xx-large / 1.2 \"FB Armada\"" +PASS e.style['font'] = "expanded normal xx-large/1.2 FB Armada" should set the property value PASS e.style['font'] = "extra-expanded normal normal larger/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "ultra-expanded normal normal normal smaller sans-serif" should set the property value PASS e.style['font'] = "ultra-condensed normal normal italic 10px/normal cursive" should set the property value PASS e.style['font'] = "extra-condensed normal normal small-caps 20%/1.2 fantasy" should set the property value PASS e.style['font'] = "condensed normal normal bolder calc(30% - 40px)/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "semi-condensed normal oblique xx-small Menu" should set the property value -FAIL e.style['font'] = "semi-expanded normal italic normal medium/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic semi-expanded medium FB Armada" but got "italic semi-expanded medium \"FB Armada\"" +PASS e.style['font'] = "semi-expanded normal italic normal medium/normal FB Armada" should set the property value PASS e.style['font'] = "expanded normal oblique small-caps xx-large/1.2 serif" should set the property value PASS e.style['font'] = "extra-expanded normal italic lighter larger/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "ultra-expanded normal small-caps smaller cursive" should set the property value PASS e.style['font'] = "ultra-condensed normal small-caps normal 10px/normal fantasy" should set the property value PASS e.style['font'] = "extra-condensed normal small-caps oblique 20%/1.2 monospace" should set the property value PASS e.style['font'] = "condensed normal small-caps 100 calc(30% - 40px)/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "semi-condensed normal 900 xx-small FB Armada" should set the property value assert_equals: serialization should be canonical expected "900 semi-condensed xx-small FB Armada" but got "900 semi-condensed xx-small \"FB Armada\"" +PASS e.style['font'] = "semi-condensed normal 900 xx-small FB Armada" should set the property value PASS e.style['font'] = "semi-expanded normal bold normal medium/normal serif" should set the property value PASS e.style['font'] = "expanded normal bolder italic xx-large/1.2 sans-serif" should set the property value PASS e.style['font'] = "extra-expanded normal lighter small-caps larger/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "ultra-expanded oblique smaller fantasy" should set the property value PASS e.style['font'] = "ultra-condensed italic normal 10px/normal monospace" should set the property value PASS e.style['font'] = "extra-condensed oblique normal normal 20%/1.2 Menu" should set the property value -FAIL e.style['font'] = "condensed italic normal small-caps calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic small-caps condensed calc(30% - 40px) / calc(120% + 1.2em) FB Armada" but got "italic small-caps condensed calc(30% - 40px) / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "condensed italic normal small-caps calc(30% - 40px)/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "semi-condensed oblique normal 100 xx-small serif" should set the property value PASS e.style['font'] = "semi-expanded italic small-caps medium/normal sans-serif" should set the property value PASS e.style['font'] = "expanded oblique small-caps normal xx-large/1.2 cursive" should set the property value PASS e.style['font'] = "extra-expanded italic small-caps 900 larger/calc(120% + 1.2em) fantasy" should set the property value PASS e.style['font'] = "ultra-expanded oblique bold smaller monospace" should set the property value PASS e.style['font'] = "ultra-condensed italic bolder normal 10px/normal Menu" should set the property value -FAIL e.style['font'] = "extra-condensed oblique lighter small-caps 20%/1.2 FB Armada" should set the property value assert_equals: serialization should be canonical expected "oblique small-caps lighter extra-condensed 20% / 1.2 FB Armada" but got "oblique small-caps lighter extra-condensed 20% / 1.2 \"FB Armada\"" +PASS e.style['font'] = "extra-condensed oblique lighter small-caps 20%/1.2 FB Armada" should set the property value PASS e.style['font'] = "condensed small-caps calc(30% - 40px)/calc(120% + 1.2em) serif" should set the property value PASS e.style['font'] = "semi-condensed small-caps normal xx-small sans-serif" should set the property value PASS e.style['font'] = "semi-expanded small-caps normal normal medium/normal cursive" should set the property value PASS e.style['font'] = "expanded small-caps normal italic xx-large/1.2 fantasy" should set the property value PASS e.style['font'] = "extra-expanded small-caps normal 100 larger/calc(120% + 1.2em) monospace" should set the property value PASS e.style['font'] = "ultra-expanded small-caps oblique smaller Menu" should set the property value -FAIL e.style['font'] = "ultra-condensed small-caps italic normal 10px/normal FB Armada" should set the property value assert_equals: serialization should be canonical expected "italic small-caps ultra-condensed 10px FB Armada" but got "italic small-caps ultra-condensed 10px \"FB Armada\"" +PASS e.style['font'] = "ultra-condensed small-caps italic normal 10px/normal FB Armada" should set the property value PASS e.style['font'] = "extra-condensed small-caps oblique 900 20%/1.2 serif" should set the property value PASS e.style['font'] = "condensed small-caps bold calc(30% - 40px)/calc(120% + 1.2em) sans-serif" should set the property value PASS e.style['font'] = "semi-condensed small-caps bolder normal xx-small cursive" should set the property value PASS e.style['font'] = "semi-expanded small-caps lighter italic medium/normal fantasy" should set the property value PASS e.style['font'] = "expanded 100 xx-large/1.2 monospace" should set the property value PASS e.style['font'] = "extra-expanded 900 normal larger/calc(120% + 1.2em) Menu" should set the property value -FAIL e.style['font'] = "ultra-expanded bold normal normal smaller FB Armada" should set the property value assert_equals: serialization should be canonical expected "bold ultra-expanded smaller FB Armada" but got "bold ultra-expanded smaller \"FB Armada\"" +PASS e.style['font'] = "ultra-expanded bold normal normal smaller FB Armada" should set the property value PASS e.style['font'] = "ultra-condensed bolder normal oblique 10px/normal serif" should set the property value PASS e.style['font'] = "extra-condensed lighter normal small-caps 20%/1.2 sans-serif" should set the property value PASS e.style['font'] = "condensed 100 italic calc(30% - 40px)/calc(120% + 1.2em) cursive" should set the property value PASS e.style['font'] = "semi-condensed 900 oblique normal xx-small fantasy" should set the property value PASS e.style['font'] = "semi-expanded bold italic small-caps medium/normal monospace" should set the property value PASS e.style['font'] = "expanded bolder small-caps xx-large/1.2 Menu" should set the property value -FAIL e.style['font'] = "extra-expanded lighter small-caps normal larger/calc(120% + 1.2em) FB Armada" should set the property value assert_equals: serialization should be canonical expected "small-caps lighter extra-expanded larger / calc(120% + 1.2em) FB Armada" but got "small-caps lighter extra-expanded larger / calc(120% + 1.2em) \"FB Armada\"" +PASS e.style['font'] = "extra-expanded lighter small-caps normal larger/calc(120% + 1.2em) FB Armada" should set the property value PASS e.style['font'] = "ultra-expanded 100 small-caps oblique smaller serif" should set the property value diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt index 3b88957521ee..a8374a0fada6 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt @@ -1,10 +1,10 @@ A A A A A A A A2 A3 A4 A5 A6 A7 -FAIL Test native font matching on "CSSTest Weights W2569" for weight 375 assert_approx_equals: @font-face should be mapped to "CSSTest Weights W2569". expected 90 +/- 2 but got 78.21875 -FAIL Test native font matching on "CSSTest Weights Full" for weight 375 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78.21875 -FAIL Test native font matching on "CSSTest Weights W1479" for weight 475 assert_approx_equals: @font-face should be mapped to "CSSTest Weights W1479". expected 90 +/- 2 but got 78.21875 -FAIL Test native font matching on "CSSTest Weights Full" for weight 425 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78.21875 -FAIL Test native font matching on "CSSTest Weights Full" for weight 525 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78.21875 -FAIL Test native font matching on "CSSTest Weights Full" for weight 675 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78.21875 +FAIL Test native font matching on CSSTest Weights W2569 for weight 375 assert_approx_equals: @font-face should be mapped to CSSTest Weights W2569. expected 90 +/- 2 but got 78.21875 +FAIL Test native font matching on CSSTest Weights Full for weight 375 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78.21875 +FAIL Test native font matching on CSSTest Weights W1479 for weight 475 assert_approx_equals: @font-face should be mapped to CSSTest Weights W1479. expected 90 +/- 2 but got 78.21875 +FAIL Test native font matching on CSSTest Weights Full for weight 425 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78.21875 +FAIL Test native font matching on CSSTest Weights Full for weight 525 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78.21875 +FAIL Test native font matching on CSSTest Weights Full for weight 675 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78.21875 diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/serialize-values-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/serialize-values-expected.txt index cb45b535afcf..0b882f37050b 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/serialize-values-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/serialize-values-expected.txt @@ -377,7 +377,7 @@ PASS float: right PASS float: none PASS float: inherit PASS font-family: Arial -PASS font-family: 'Lucida Grande' +FAIL font-family: 'Lucida Grande' assert_equals: font-family raw inline style declaration expected "\"Lucida Grande\"" but got "Lucida Grande" PASS font-family: serif PASS font-family: sans-serif PASS font-family: inherit diff --git a/LayoutTests/platform/glib/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt b/LayoutTests/platform/glib/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt index 0069c15b3b65..729d0b42cb1a 100644 --- a/LayoutTests/platform/glib/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt +++ b/LayoutTests/platform/glib/imported/w3c/web-platform-tests/css/css-fonts/variations/font-weight-matching-installed-fonts-expected.txt @@ -1,10 +1,10 @@ A A A A A A A A2 A3 A4 A5 A6 A7 -FAIL Test native font matching on "CSSTest Weights W2569" for weight 375 assert_approx_equals: @font-face should be mapped to "CSSTest Weights W2569". expected 90 +/- 2 but got 78 -FAIL Test native font matching on "CSSTest Weights Full" for weight 375 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78 -FAIL Test native font matching on "CSSTest Weights W1479" for weight 475 assert_approx_equals: @font-face should be mapped to "CSSTest Weights W1479". expected 90 +/- 2 but got 78 -FAIL Test native font matching on "CSSTest Weights Full" for weight 425 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78 -FAIL Test native font matching on "CSSTest Weights Full" for weight 525 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78 -FAIL Test native font matching on "CSSTest Weights Full" for weight 675 assert_approx_equals: @font-face should be mapped to "CSSTest Weights Full". expected 90 +/- 2 but got 78 +FAIL Test native font matching on CSSTest Weights W2569 for weight 375 assert_approx_equals: @font-face should be mapped to CSSTest Weights W2569. expected 90 +/- 2 but got 78 +FAIL Test native font matching on CSSTest Weights Full for weight 375 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78 +FAIL Test native font matching on CSSTest Weights W1479 for weight 475 assert_approx_equals: @font-face should be mapped to CSSTest Weights W1479. expected 90 +/- 2 but got 78 +FAIL Test native font matching on CSSTest Weights Full for weight 425 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78 +FAIL Test native font matching on CSSTest Weights Full for weight 525 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78 +FAIL Test native font matching on CSSTest Weights Full for weight 675 assert_approx_equals: @font-face should be mapped to CSSTest Weights Full. expected 90 +/- 2 but got 78 diff --git a/LayoutTests/platform/gtk/fast/css/css2-system-fonts-expected.txt b/LayoutTests/platform/gtk/fast/css/css2-system-fonts-expected.txt index 7b919fc0f7e9..04759b5c006d 100644 --- a/LayoutTests/platform/gtk/fast/css/css2-system-fonts-expected.txt +++ b/LayoutTests/platform/gtk/fast/css/css2-system-fonts-expected.txt @@ -1,10 +1,10 @@ This tests platform specific system font styles. If any of the styles appear in monospace the test fails. -caption: 16px "Liberation Sans" -icon: 16px "Liberation Sans" -menu: 16px "Liberation Sans" -message-box: 16px "Liberation Sans" -small-caption: 16px "Liberation Sans" -status-bar: 16px "Liberation Sans" --webkit-mini-control: 16px "Liberation Sans" --webkit-small-control: 16px "Liberation Sans" --webkit-control: 16px "Liberation Sans" +caption: 16px Liberation Sans +icon: 16px Liberation Sans +menu: 16px Liberation Sans +message-box: 16px Liberation Sans +small-caption: 16px Liberation Sans +status-bar: 16px Liberation Sans +-webkit-mini-control: 16px Liberation Sans +-webkit-small-control: 16px Liberation Sans +-webkit-control: 16px Liberation Sans diff --git a/LayoutTests/platform/gtk/imported/w3c/web-platform-tests/css/css-fonts/animations/system-fonts-expected.txt b/LayoutTests/platform/gtk/imported/w3c/web-platform-tests/css/css-fonts/animations/system-fonts-expected.txt index 8e5ae4899960..33414d0b86f3 100644 --- a/LayoutTests/platform/gtk/imported/w3c/web-platform-tests/css/css-fonts/animations/system-fonts-expected.txt +++ b/LayoutTests/platform/gtk/imported/w3c/web-platform-tests/css/css-fonts/animations/system-fonts-expected.txt @@ -33,21 +33,21 @@ PASS CSS Animations: property from [italic 100 small-caps ultra-expanded PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [140%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [49.6px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [400] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [100%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [16px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [550] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [50%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [0px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (-2) should be [italic] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (-2) should be [1] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (-2) should be [small-caps] for @@ -82,21 +82,21 @@ PASS Web Animations: property from [italic 100 small-caps ultra-expanded PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [140%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [49.6px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (0.6) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [400] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [100%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [16px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [550] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [50%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [0px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [caption] at (1.5) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (-2) should be [italic] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (-2) should be [1] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (-2) should be [small-caps] for @@ -131,21 +131,21 @@ PASS CSS Animations: property from [italic 100 small-caps ultra-expanded PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [140%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [49.6px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [400] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [100%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [16px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [550] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [50%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [0px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (-2) should be [italic] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (-2) should be [1] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (-2) should be [small-caps] for @@ -180,21 +180,21 @@ PASS Web Animations: property from [italic 100 small-caps ultra-expanded PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [140%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [49.6px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (0.6) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [400] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [100%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [16px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [550] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [50%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [0px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [icon] at (1.5) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (-2) should be [italic] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (-2) should be [1] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (-2) should be [small-caps] for @@ -229,21 +229,21 @@ PASS CSS Animations: property from [italic 100 small-caps ultra-expanded PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [140%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [49.6px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [400] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [100%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [16px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [550] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [50%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [0px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (-2) should be [italic] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (-2) should be [1] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (-2) should be [small-caps] for @@ -278,21 +278,21 @@ PASS Web Animations: property from [italic 100 small-caps ultra-expanded PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [140%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [49.6px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (0.6) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [400] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [100%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [16px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [550] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [50%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [0px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [menu] at (1.5) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (-2) should be [italic] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (-2) should be [1] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (-2) should be [small-caps] for @@ -327,21 +327,21 @@ PASS CSS Animations: property from [italic 100 small-caps ultra-expanded PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [140%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [49.6px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [400] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [100%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [16px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [550] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [50%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [0px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (-2) should be [italic] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (-2) should be [1] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (-2) should be [small-caps] for @@ -376,21 +376,21 @@ PASS Web Animations: property from [italic 100 small-caps ultra-expanded PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [140%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [49.6px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (0.6) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [400] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [100%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [16px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [550] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [50%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [0px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [message-box] at (1.5) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (-2) should be [italic] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (-2) should be [1] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (-2) should be [small-caps] for @@ -425,21 +425,21 @@ PASS CSS Animations: property from [italic 100 small-caps ultra-expanded PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [140%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [49.6px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [400] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [100%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [16px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [550] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [50%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [0px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (-2) should be [italic] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (-2) should be [1] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (-2) should be [small-caps] for @@ -474,21 +474,21 @@ PASS Web Animations: property from [italic 100 small-caps ultra-expanded PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [140%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [49.6px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (0.6) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [400] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [100%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [16px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [550] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [50%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [0px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [small-caption] at (1.5) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (-2) should be [italic] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (-2) should be [1] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (-2) should be [small-caps] for @@ -523,21 +523,21 @@ PASS CSS Animations: property from [italic 100 small-caps ultra-expanded PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [140%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [49.6px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [400] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [100%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [16px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [Liberation Sans] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [550] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [normal] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [50%] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [0px] for PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [normal] for -PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be ["Liberation Sans"] for +PASS CSS Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (-2) should be [italic] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (-2) should be [1] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (-2) should be [small-caps] for @@ -572,19 +572,19 @@ PASS Web Animations: property from [italic 100 small-caps ultra-expanded PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [140%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [49.6px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (0.6) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [400] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [100%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [16px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1) should be [Liberation Sans] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [550] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [normal] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [50%] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [0px] for PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [normal] for -PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be ["Liberation Sans"] for +PASS Web Animations: property from [italic 100 small-caps ultra-expanded 100px / 100px Ahem] to [status-bar] at (1.5) should be [Liberation Sans] for diff --git a/Source/WebCore/css/CSSMarkup.cpp b/Source/WebCore/css/CSSMarkup.cpp index f89f8bcbed3a..b5d6205b464a 100644 --- a/Source/WebCore/css/CSSMarkup.cpp +++ b/Source/WebCore/css/CSSMarkup.cpp @@ -154,21 +154,44 @@ String serializeString(StringView string) static bool shouldQuoteFontFamily(StringView string) { - if (!isCSSTokenizerIdentifier(string)) - return true; - // Font family names that match CSS-wide keywords, 'default', or generic // family keywords must be quoted to prevent confusion with the keywords // of the same names. // https://www.w3.org/TR/css-fonts-4/#family-name-syntax // + // FIXME: We also quote when the first word of a multi-word name is a + // generic keyword, matching parser behavior. See webkit.org/b/314837. + // // Note: system-ui is excluded from quoting because the parser always // stores it as a CSS_FONT_FAMILY string (not a CSSValueID), even when // used as a generic keyword. Since we cannot distinguish the generic // keyword from a quoted family name at this point, and the generic // keyword is the common case, we leave it unquoted. - auto valueID = cssValueKeywordID(string); - return valueID != CSSValueSystemUi && (!isValidCustomIdentifier(valueID) || isGenericFontFamilyKeyword(valueID)); + auto stringID = cssValueKeywordID(string); + if (stringID == CSSValueSystemUi) + return false; + if (isGenericFontFamilyKeyword(stringID)) + return true; + + // Leading, trailing, or consecutive spaces are collapsed on re-parse, + // so names containing them must be quoted to round-trip. + if (string.startsWith(' ') || string.endsWith(' ') || string.contains(" "_s)) + return true; + + bool isFirstWord = true; + bool hasWord = false; + for (auto word : string.split(' ')) { + if (!isCSSTokenizerIdentifier(word)) + return true; + auto valueID = cssValueKeywordID(word); + if (!isValidCustomIdentifier(valueID)) + return true; + if (isFirstWord && isGenericFontFamilyKeyword(valueID)) + return true; + hasWord = true; + isFirstWord = false; + } + return !hasWord; } void serializeFontFamily(StringBuilder& builder, StringView string) diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebViewEditActions.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebViewEditActions.mm index c34337725176..b81ad52e7575 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebViewEditActions.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebViewEditActions.mm @@ -341,7 +341,7 @@ - (void)_translate:(id)sender; EXPECT_WK_STREQ("normal", [webView stylePropertyAtSelectionStart:@"font-style"]); [webView _setFont:[UIFont fontWithName:@"TimesNewRomanPS-BoldMT" size:12] sender:nil]; - EXPECT_WK_STREQ("\"Times New Roman\"", [webView stylePropertyAtSelectionStart:@"font-family"]); + EXPECT_WK_STREQ("Times New Roman", [webView stylePropertyAtSelectionStart:@"font-family"]); EXPECT_WK_STREQ("12px", [webView stylePropertyAtSelectionStart:@"font-size"]); EXPECT_WK_STREQ("700", [webView stylePropertyAtSelectionStart:@"font-weight"]); EXPECT_WK_STREQ("normal", [webView stylePropertyAtSelectionStart:@"font-style"]); diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/FontManagerTests.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/FontManagerTests.mm index 3f11963c684c..6da56e9f6503 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/FontManagerTests.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/FontManagerTests.mm @@ -242,7 +242,7 @@ static void setOverrideSelectedFaceName(NSFontPanel *panel, NSString *name) [fontManager modifyFontViaPanel:fontPanel]; EXPECT_WK_STREQ("bar", [webView selectedText]); EXPECT_WK_STREQ("bar", [webView stringByEvaluatingJavaScript:@"bar.outerHTML"]); - EXPECT_WK_STREQ("\"Times New Roman\"", [webView stringByEvaluatingJavaScript:@"getComputedStyle(bar.firstChild.firstChild)['font-family']"]); + EXPECT_WK_STREQ("Times New Roman", [webView stringByEvaluatingJavaScript:@"getComputedStyle(bar.firstChild.firstChild)['font-family']"]); EXPECT_WK_STREQ("10px", [webView stringByEvaluatingJavaScript:@"getComputedStyle(bar.firstChild.firstChild)['font-size']"]); EXPECT_WK_STREQ("700", [webView stringByEvaluatingJavaScript:@"getComputedStyle(bar.firstChild.firstChild)['font-weight']"]); expectSameAttributes(smallBoldTimesFont, fontManager.selectedFont); From 5086d4e7339386e9754a971718430d57c4f0280b Mon Sep 17 00:00:00 2001 From: Ahmad Saleem Date: Thu, 14 May 2026 15:48:01 -0700 Subject: [PATCH 046/424] Use for-range loop in SVGFEConvolveMatrixElement::createFilterEffect https://bugs.webkit.org/show_bug.cgi?id=314826 rdar://177083269 Reviewed by Brent Fulgham. The default-divisor loop re-evaluated kernelMatrix.length() and indexed kernelMatrix.items()[i] on every iteration. Switch to a for-range loop over items() so the bounds and backing vector are resolved once. * Source/WebCore/svg/SVGFEConvolveMatrixElement.cpp: (WebCore::SVGFEConvolveMatrixElement::createFilterEffect const): Canonical link: https://commits.webkit.org/313272@main --- Source/WebCore/svg/SVGFEConvolveMatrixElement.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/WebCore/svg/SVGFEConvolveMatrixElement.cpp b/Source/WebCore/svg/SVGFEConvolveMatrixElement.cpp index c17d12eaf472..21711b5d1a0c 100644 --- a/Source/WebCore/svg/SVGFEConvolveMatrixElement.cpp +++ b/Source/WebCore/svg/SVGFEConvolveMatrixElement.cpp @@ -248,8 +248,8 @@ RefPtr SVGFEConvolveMatrixElement::createFilterEffect(const Filter float filterDivisor = 0; // The spec says the default value is the sum of all values in kernelMatrix. - for (unsigned i = 0; i < kernelMatrix.length(); ++i) - filterDivisor += kernelMatrix.items()[i]->value(); + for (auto& item : kernelMatrix.items()) + filterDivisor += item->value(); // The spec says if the sum is zero, then the divisor is set to the initial value. return filterDivisor ? filterDivisor : initialDivisorValue; From e01a74ffaab3600aea2da3667edd8dd0da20a9e8 Mon Sep 17 00:00:00 2001 From: Qianlang Chen Date: Thu, 14 May 2026 15:49:04 -0700 Subject: [PATCH 047/424] Web Inspector: `TestHarness.expect(Not)Null` should account for `undefined` https://bugs.webkit.org/show_bug.cgi?id=314687 Reviewed by Devin Rousso. We recently wasted significant debugging time on a bot failure where a protocol agent was undefined rather than null, but expectNotNull for that agent happily passed while subsequent test cases crashed. expectNotNull(value) was implemented as `value !== null` (strict), which means expectNotNull(undefined) silently passes. This should've never been the intent: callers use expectNotNull to assert they have a real value, and undefined is just as invalid as null in that context. Change both checks to use loose equality: - expectNotNull: `value != null` (fails for both null and undefined) - expectNull: `value == null` (passes for both null and undefined) This makes them behave like "has a value" / "has no value" checks, which matches how they're used in practice throughout the test suite. This should not regress any existing tests. One caveat is expectNull now becomes more permissive (accepts undefined in addition to null). In case someone specifically needs to assert `=== null` and not undefined, they'll need to use expectEqual(value, null), though that scenario shouldn't arise often in practice. * Source/WebInspectorUI/UserInterface/Test/TestHarness.js: (TestHarness.prototype.expectNull): (TestHarness.prototype.expectNotNull): Linter disallows loose equality (== and !=), so we explicitly compare against both null and undefined to achieve the same result. * LayoutTests/inspector/unit-tests/test-harness-expect-functions-expected.txt: * LayoutTests/inspector/unit-tests/test-harness-expect-functions.html: Canonical link: https://commits.webkit.org/313273@main --- .../unit-tests/test-harness-expect-functions-expected.txt | 8 ++++---- .../unit-tests/test-harness-expect-functions.html | 4 ++-- Source/WebInspectorUI/UserInterface/Test/TestHarness.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LayoutTests/inspector/unit-tests/test-harness-expect-functions-expected.txt b/LayoutTests/inspector/unit-tests/test-harness-expect-functions-expected.txt index dbf6351e9a09..19b53919fc21 100644 --- a/LayoutTests/inspector/unit-tests/test-harness-expect-functions-expected.txt +++ b/LayoutTests/inspector/unit-tests/test-harness-expect-functions-expected.txt @@ -125,6 +125,7 @@ FAIL: expectNotEmpty should not be called with a non-object: -- Running test case: InspectorTest.expectNull Expected to PASS PASS: expectNull(null) +PASS: expectNull(undefined) Expected to FAIL FAIL: expectNull(true) Expected: null @@ -138,9 +139,6 @@ FAIL: expectNull(1) FAIL: expectNull("") Expected: null Actual: "" -FAIL: expectNull(undefined) - Expected: null - Actual: undefined FAIL: expectNull({}) Expected: null Actual: {} @@ -154,13 +152,15 @@ PASS: expectNotNull(true) PASS: expectNotNull(false) PASS: expectNotNull(1) PASS: expectNotNull("") -PASS: expectNotNull(undefined) PASS: expectNotNull({}) PASS: expectNotNull([]) Expected to FAIL FAIL: expectNotNull(null) Expected: not null Actual: null +FAIL: expectNotNull(undefined) + Expected: not null + Actual: undefined -- Running test case: InspectorTest.expectEqual Expected to PASS diff --git a/LayoutTests/inspector/unit-tests/test-harness-expect-functions.html b/LayoutTests/inspector/unit-tests/test-harness-expect-functions.html index e3864a03c4e0..e8a7112e604b 100644 --- a/LayoutTests/inspector/unit-tests/test-harness-expect-functions.html +++ b/LayoutTests/inspector/unit-tests/test-harness-expect-functions.html @@ -64,8 +64,8 @@ let expectNullTestCase = { functionName: "expectNull", - passingInputs: [null], - failingInputs: [true, false, 1, "", undefined, {}, []] + passingInputs: [null, undefined], + failingInputs: [true, false, 1, "", {}, []], }; addTestCase(expectNullTestCase); addInverseTestCase("expectNotNull", expectNullTestCase); diff --git a/Source/WebInspectorUI/UserInterface/Test/TestHarness.js b/Source/WebInspectorUI/UserInterface/Test/TestHarness.js index 158bc4bb40bc..3140c88af3c0 100644 --- a/Source/WebInspectorUI/UserInterface/Test/TestHarness.js +++ b/Source/WebInspectorUI/UserInterface/Test/TestHarness.js @@ -173,12 +173,12 @@ TestHarness = class TestHarness extends WI.Object expectNull(actual, message) { - this._expect(TestHarness.ExpectationType.Null, actual === null, message, actual, null); + this._expect(TestHarness.ExpectationType.Null, actual === null || actual === undefined, message, actual, null); } expectNotNull(actual, message) { - this._expect(TestHarness.ExpectationType.NotNull, actual !== null, message, actual); + this._expect(TestHarness.ExpectationType.NotNull, actual !== null && actual !== undefined, message, actual); } expectEqual(actual, expected, message) From a735fd3063f79481584a963d7de114ea0dd2f089 Mon Sep 17 00:00:00 2001 From: Chris Dumez Date: Thu, 14 May 2026 16:07:26 -0700 Subject: [PATCH 048/424] WKBackForwardList.BackForwardNavigationSkipsItemsWithoutUserGesture* API tests are flaky https://bugs.webkit.org/show_bug.cgi?id=314785 rdar://176046782 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Brady Eidson. The shared helper runBackForwardNavigationSkipsItemsWithoutUserGestureTest runs ~12 sequential navigations in a single TEST(), and is shared across three variants (PushState, Fragment, PushStateAfterEvaluateJS). On loaded bots this exceeds the per-test timeout, producing flaky failures across all three variants — including failures at the very first loadRequest on a fresh WKWebView. A previous attempt (313126@main) added a 10-second inner timeout to each wait. That made things worse: 10 seconds is too tight for slow bots that previously recovered within the per-test framework timeout, so the inner timeout converted slow steps into hard fails rather than catching genuine hangs. Reduce per-test work by splitting the helper into a setup function and two phase checks: - Phase 1: the goBack/goForward navigations that verify items without user gesture are skipped (idempotent — leaves state unchanged). - Phase 2: the JS history.back() navigations that verify the JS API does not skip those items (mutating — must run after Phase 1). Use a templated testing::Test base WKBackForwardListSkipItemsTestBase parameterized by a per-variant navigate function, so the back/forward list setup is built once via SetUpTestSuite and shared across both phase checks within the suite. Each variant becomes a one-line subclass with two TEST_F entries (six tests in total). Gtest registration (source) order ensures Phase 1 runs before Phase 2 within each suite. Make the setup more resilient to slow / I/O-saturated bots: - Switch from file:// URLs to a custom URL scheme handler serving in-memory HTML for a "test://" scheme. Avoids the file scheme handler path in NetworkProcess, which can be slow under disk pressure. - Share a single WKProcessPool across all fixtures (and across --gtest_repeat iterations) so WebProcesses get cached and reused instead of cold-launched per WebView. - Share a single ephemeral WKWebsiteDataStore so cookies/cache stay in memory and we avoid disk I/O entirely. - Call _launchInitialProcessIfNecessary right after WKWebView creation so the WebProcess launch overlaps with the rest of test setup rather than blocking the first loadRequest. Also restore the unbounded waitForDidFinishNavigationOrDidSameDocumentNavigation, matching what the other tests in this file use, and re-enable BackForwardNavigationSkipsItemsWithoutUserGestureFragment on macOS. * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKBackForwardListTests.mm: (-[WKBackForwardNavigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigationWithTimeout:]): Deleted. (setupBackForwardListWithItemsWithoutUserGesture): (runBackForwardNavigationSkipsItemsWithoutUserGestureCheck): (runJSHistoryBackDoesNotSkipItemsWithoutUserGestureCheck): (pushStateNavigate): (fragmentNavigate): (pushStateAfterEvaluateJSNavigate): (WKBackForwardListSkipItemsTestBase::SetUpTestSuite): (WKBackForwardListSkipItemsTestBase::TearDownTestSuite): (TEST_F(WKBackForwardListSkipItemsPushStateTest, BackForwardNavigationSkipsItemsWithoutUserGesture)): (TEST_F(WKBackForwardListSkipItemsPushStateTest, JSHistoryBackDoesNotSkipItemsWithoutUserGesture)): (TEST_F(WKBackForwardListSkipItemsFragmentTest, BackForwardNavigationSkipsItemsWithoutUserGesture)): (TEST_F(WKBackForwardListSkipItemsFragmentTest, JSHistoryBackDoesNotSkipItemsWithoutUserGesture)): (TEST_F(WKBackForwardListSkipItemsPushStateAfterEvaluateJSTest, BackForwardNavigationSkipsItemsWithoutUserGesture)): (TEST_F(WKBackForwardListSkipItemsPushStateAfterEvaluateJSTest, JSHistoryBackDoesNotSkipItemsWithoutUserGesture)): Canonical link: https://commits.webkit.org/313274@main --- .../WKWebView/WKBackForwardListTests.mm | 272 +++++++++++------- 1 file changed, 166 insertions(+), 106 deletions(-) diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKBackForwardListTests.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKBackForwardListTests.mm index 7a6f9d70e73a..9c67d70a4423 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKBackForwardListTests.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKBackForwardListTests.mm @@ -32,6 +32,7 @@ #import "Helpers/cocoa/TestNavigationDelegate.h" #import "Helpers/cocoa/TestUIDelegate.h" #import "Helpers/cocoa/TestWKWebView.h" +#import "TestURLSchemeHandler.h" #import #import #import @@ -43,6 +44,7 @@ #import #import #import +#import #import #import #import @@ -392,7 +394,6 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati @interface WKBackForwardNavigationDelegate : NSObject - (void)waitForDidFinishNavigationOrDidSameDocumentNavigation; -- (BOOL)waitForDidFinishNavigationOrDidSameDocumentNavigationWithTimeout:(NSTimeInterval)seconds; @end static RetainPtr lastNavigation; @@ -435,12 +436,6 @@ - (void)waitForDidFinishNavigationOrDidSameDocumentNavigation TestWebKitAPI::Util::run(&_navigated); } -- (BOOL)waitForDidFinishNavigationOrDidSameDocumentNavigationWithTimeout:(NSTimeInterval)seconds -{ - _navigated = false; - return TestWebKitAPI::Util::runFor(&_navigated, Seconds { seconds }); -} - - (void)waitForDidFinishNavigation { _didFinishNavigation = false; @@ -529,170 +524,235 @@ - (void)waitForDidFinishNavigation EXPECT_EQ([webView backForwardList].forwardList.count, 1U); } -static void runBackForwardNavigationSkipsItemsWithoutUserGestureTest(Function&& navigate) +struct SkipItemsBackForwardListFixture { + RetainPtr webView; + RetainPtr navigationDelegate; + RetainPtr url1; + RetainPtr url2; + RetainPtr url3; +}; + +// Builds the back/forward list: +// url1 -> url2 -> url2#a (no user gesture) -> url2#b (no user gesture) -> url2#c (no user gesture) -> url3 ** +static SkipItemsBackForwardListFixture setupBackForwardListWithItemsWithoutUserGesture(NOESCAPE Function&& navigate) { - RetainPtr webView = adoptNS([[WKWebView alloc] init]); + static RetainPtr url1 = adoptNS([[NSURL alloc] initWithString:@"test://example/simple.html"]); + static RetainPtr url2 = adoptNS([[NSURL alloc] initWithString:@"test://example/simple2.html"]); + static RetainPtr url3 = adoptNS([[NSURL alloc] initWithString:@"test://example/simple3.html"]); + + // Build a single shared WKWebViewConfiguration once: the URL scheme handler (with + // pre-built per-URL responses), a shared WKProcessPool so WebProcesses get cached + // across fixtures and --gtest_repeat iterations, and a shared ephemeral + // WKWebsiteDataStore so cookies/cache stay in memory (avoiding disk I/O entirely). + static RetainPtr configuration = ([] { + static constexpr auto htmlBytes = "simple"_s; + RetainPtr data = toNSDataNoCopy(htmlBytes.span8(), FreeWhenDone::No); + auto makeResponse = [&data](NSURL *url) { + return adoptNS([[NSURLResponse alloc] initWithURL:url MIMEType:@"text/html" expectedContentLength:[data length] textEncodingName:nil]); + }; + RetainPtr responseByURL = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys: + makeResponse(url1.get()).get(), url1.get(), + makeResponse(url2.get()).get(), url2.get(), + makeResponse(url3.get()).get(), url3.get(), + nil]); + + RetainPtr schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); + [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { + [task didReceiveResponse:[responseByURL objectForKey:[[task request] URL]]]; + [task didReceiveData:data.get()]; + [task didFinish]; + }]; + + RetainPtr config = adoptNS([[WKWebViewConfiguration alloc] init]); + [config setURLSchemeHandler:schemeHandler.get() forURLScheme:@"test"]; + [config setProcessPool:adoptNS([WKProcessPool new]).get()]; + [config setWebsiteDataStore:[WKWebsiteDataStore nonPersistentDataStore]]; + return config; + }()); + RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]); + [webView _launchInitialProcessIfNecessary]; RetainPtr navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]); webView.get().navigationDelegate = navigationDelegate.get(); - RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"]; - RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"]; - RetainPtr url3 = [NSBundle.test_resourcesBundle URLForResource:@"simple3" withExtension:@"html"]; - - // Waits for the navigation delegate to see didFinishNavigation or didSameDocumentNavigation, - // with a finite timeout so that a rare hang (webkit.org/b/313844) surfaces as a fast EXPECT - // failure with the context we were waiting for rather than a silent test-framework timeout. - // Returns false on timeout so callers can bail out before the next wait also times out, - // keeping total runtime bounded. - auto waitForNavigation = [&](const char* context) -> bool { - if ([navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigationWithTimeout:10]) - return true; - NSLog(@"WKBackForwardNavigationDelegate wait timed out (%s); webView.URL = %@", context, [webView URL]); - EXPECT_TRUE(false); - return false; - }; - [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]]; - if (!waitForNavigation("loadRequest url1")) - return; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]]; - if (!waitForNavigation("loadRequest url2")) - return; - - // Test case: - // url1 -> url2 -> url2#a (no user gesture) -> url2#b (no user gesture) -> url2#c (no user gesture) -> url3. + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; // Add back/forward list items without user gestures. navigate(webView.get(), "location.pathname + '#a'"_s); - if (!waitForNavigation("navigate to url2#a")) - return; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; EXPECT_FALSE([lastNavigation _isUserInitiated]); EXPECT_TRUE(webView.get().backForwardList.currentItem._wasCreatedByJSWithoutUserInteraction); RetainPtr expectedURLString = makeString(String([url2 absoluteString]), "#a"_s).createNSString(); EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String); navigate(webView.get(), "location.pathname + '#b'"_s); - if (!waitForNavigation("navigate to url2#b")) - return; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; EXPECT_FALSE([lastNavigation _isUserInitiated]); EXPECT_TRUE(webView.get().backForwardList.currentItem._wasCreatedByJSWithoutUserInteraction); expectedURLString = makeString(String([url2 absoluteString]), "#b"_s).createNSString(); EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String); navigate(webView.get(), "location.pathname + '#c'"_s); - if (!waitForNavigation("navigate to url2#c")) - return; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; EXPECT_FALSE([lastNavigation _isUserInitiated]); EXPECT_TRUE(webView.get().backForwardList.currentItem._wasCreatedByJSWithoutUserInteraction); expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString(); EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String); [webView loadRequest:[NSURLRequest requestWithURL:url3.get()]]; - if (!waitForNavigation("loadRequest url3")) - return; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; EXPECT_FALSE([webView backForwardList].currentItem._wasCreatedByJSWithoutUserInteraction); - // url1 -> url2 -> url2#a (no user gesture) -> url2#b (no user gesture) -> url2#c (no user gesture) -> url3 ** EXPECT_EQ([webView backForwardList].backList.count, 2U); EXPECT_EQ([webView backForwardList].forwardList.count, 0U); + SkipItemsBackForwardListFixture fixture; + fixture.webView = WTF::move(webView); + fixture.navigationDelegate = WTF::move(navigationDelegate); + fixture.url1 = url1; + fixture.url2 = url2; + fixture.url3 = url3; + return fixture; +} + +// Phase 1 check: idempotent — leaves state unchanged (back to url3, backList=2, forwardList=0). +static void runBackForwardNavigationSkipsItemsWithoutUserGestureCheck(const SkipItemsBackForwardListFixture& fixture) +{ // We are now on url3. Let's go back. - [webView goBack]; - if (!waitForNavigation("goBack from url3")) - return; + [fixture.webView goBack]; + [fixture.navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; // url1 -> url2 -> url2#a (no user gesture) -> url2#b (no user gesture) -> url2#c (no user gesture) ** -> url3 - expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString(); - EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); - EXPECT_EQ([webView backForwardList].backList.count, 1U); - EXPECT_EQ([webView backForwardList].forwardList.count, 1U); + RetainPtr expectedURLString = makeString(String([fixture.url2 absoluteString]), "#c"_s).createNSString(); + EXPECT_STREQ([fixture.webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); + EXPECT_EQ([fixture.webView backForwardList].backList.count, 1U); + EXPECT_EQ([fixture.webView backForwardList].forwardList.count, 1U); // Let's go back again. - [webView goBack]; - if (!waitForNavigation("goBack from url2#c (skipping)")) - return; + [fixture.webView goBack]; + [fixture.navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; // We should have skipped over url2#b, url2#a and url2, to end up on url1. // url1 ** -> url2 -> url2#a (no user gesture) -> url2#b (no user gesture) -> url2#c (no user gesture) -> url3 - EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url1 absoluteString].UTF8String); - EXPECT_EQ([webView backForwardList].backList.count, 0U); - EXPECT_EQ([webView backForwardList].forwardList.count, 2U); + EXPECT_STREQ([fixture.webView URL].absoluteString.UTF8String, [fixture.url1 absoluteString].UTF8String); + EXPECT_EQ([fixture.webView backForwardList].backList.count, 0U); + EXPECT_EQ([fixture.webView backForwardList].forwardList.count, 2U); // Now let's go forward. - [webView goForward]; - if (!waitForNavigation("goForward from url1")) - return; + [fixture.webView goForward]; + [fixture.navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; // We should get to the latest url2 URL, that is url2#c. // url1 -> url2 -> url2#a (no user gesture) -> url2#b (no user gesture) -> url2#c (no user gesture) ** -> url3 - expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString(); - EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); - EXPECT_EQ([webView backForwardList].backList.count, 1U); - EXPECT_EQ([webView backForwardList].forwardList.count, 1U); + expectedURLString = makeString(String([fixture.url2 absoluteString]), "#c"_s).createNSString(); + EXPECT_STREQ([fixture.webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); + EXPECT_EQ([fixture.webView backForwardList].backList.count, 1U); + EXPECT_EQ([fixture.webView backForwardList].forwardList.count, 1U); // Let's go forward again. - [webView goForward]; - if (!waitForNavigation("goForward from url2#c")) - return; + [fixture.webView goForward]; + [fixture.navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; // We should now be on url3. - EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url3 absoluteString].UTF8String); + EXPECT_STREQ([fixture.webView URL].absoluteString.UTF8String, [fixture.url3 absoluteString].UTF8String); - EXPECT_EQ([webView backForwardList].backList.count, 2U); - EXPECT_EQ([webView backForwardList].forwardList.count, 0U); + EXPECT_EQ([fixture.webView backForwardList].backList.count, 2U); + EXPECT_EQ([fixture.webView backForwardList].forwardList.count, 0U); +} +// Phase 2 check: mutating — leaves state on url2#b. Must run after the Phase 1 check. +static void runJSHistoryBackDoesNotSkipItemsWithoutUserGestureCheck(const SkipItemsBackForwardListFixture& fixture) +{ // Navigating via the JS API shouldn't skip those back/forward list items. - [webView _evaluateJavaScriptWithoutUserGesture:@"history.back();" completionHandler:^(id, NSError *) { }]; - if (!waitForNavigation("history.back() from url3")) - return; + [fixture.webView _evaluateJavaScriptWithoutUserGesture:@"history.back();" completionHandler:^(id, NSError *) { }]; + [fixture.navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + RetainPtr expectedURLString = makeString(String([fixture.url2 absoluteString]), "#c"_s).createNSString(); + EXPECT_STREQ([fixture.webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); + EXPECT_EQ([fixture.webView backForwardList].backList.count, 1U); + EXPECT_EQ([fixture.webView backForwardList].forwardList.count, 1U); + + [fixture.webView _evaluateJavaScriptWithoutUserGesture:@"history.back();" completionHandler:^(id, NSError *) { }]; + [fixture.navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + expectedURLString = makeString(String([fixture.url2 absoluteString]), "#b"_s).createNSString(); + EXPECT_STREQ([fixture.webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); + EXPECT_EQ([fixture.webView backForwardList].backList.count, 1U); + EXPECT_EQ([fixture.webView backForwardList].forwardList.count, 1U); +} - expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString(); - EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); - EXPECT_EQ([webView backForwardList].backList.count, 1U); - EXPECT_EQ([webView backForwardList].forwardList.count, 1U); +// Test fixtures: the back/forward list setup is built once per fixture (via SetUpTestSuite) +// and reused across both Phase 1 (skip checks) and Phase 2 (JS history.back checks). Phase 1 +// is idempotent, so it must come first; Phase 2 mutates and runs last. Tests within a fixture +// are run in source order. - [webView _evaluateJavaScriptWithoutUserGesture:@"history.back();" completionHandler:^(id, NSError *) { }]; - if (!waitForNavigation("history.back() from url2#c (same-document)")) - return; - expectedURLString = makeString(String([url2 absoluteString]), "#b"_s).createNSString(); - EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); - EXPECT_EQ([webView backForwardList].backList.count, 1U); - EXPECT_EQ([webView backForwardList].forwardList.count, 1U); +static void pushStateNavigate(WKWebView *webView, ASCIILiteral destination) +{ + [webView _evaluateJavaScriptWithoutUserGesture:makeString("history.pushState(null, document.title, "_s, destination, ");"_s).createNSString().get() completionHandler:nil]; } -TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGesturePushState) +static void fragmentNavigate(WKWebView *webView, ASCIILiteral destination) { - runBackForwardNavigationSkipsItemsWithoutUserGestureTest([](WKWebView* webView, ASCIILiteral destination) { - [webView _evaluateJavaScriptWithoutUserGesture:makeString("history.pushState(null, document.title, "_s, destination, ");"_s).createNSString().get() completionHandler:nil]; - }); + [webView _evaluateJavaScriptWithoutUserGesture:makeString("location.href = "_s, destination, ";"_s).createNSString().get() completionHandler:nil]; } -// FIXME when webkit.org/b/313844 is resolved. -#if PLATFORM(MAC) -TEST(WKBackForwardList, DISABLED_BackForwardNavigationSkipsItemsWithoutUserGestureFragment) -#else -TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGestureFragment) -#endif +static void pushStateAfterEvaluateJSNavigate(WKWebView *webView, ASCIILiteral destination) { - runBackForwardNavigationSkipsItemsWithoutUserGestureTest([](WKWebView* webView, ASCIILiteral destination) { - [webView _evaluateJavaScriptWithoutUserGesture:makeString("location.href = "_s, destination, ";"_s).createNSString().get() completionHandler:nil]; - }); + // Do a call to evaluateJavaScript (with user gesture) *BEFORE* the pushState and make sure it doesn't count + // as a user gesture for the pushState(). + __block bool didRunScript = false; + [webView evaluateJavaScript:@"window.foo = 1;" completionHandler:^(id, NSError *) { + didRunScript = true; + }]; + TestWebKitAPI::Util::run(&didRunScript); + pushStateNavigate(webView, destination); } -TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGesturePushStateAfterEvaluateJS) +template +class WKBackForwardListSkipItemsTestBase : public testing::Test { +public: + static void SetUpTestSuite() { s_fixture = setupBackForwardListWithItemsWithoutUserGesture(navigate); } + static void TearDownTestSuite() { s_fixture.reset(); } + static std::optional s_fixture; +}; +template +std::optional WKBackForwardListSkipItemsTestBase::s_fixture; + +class WKBackForwardListSkipItemsPushStateTest : public WKBackForwardListSkipItemsTestBase { }; +class WKBackForwardListSkipItemsFragmentTest : public WKBackForwardListSkipItemsTestBase { }; +class WKBackForwardListSkipItemsPushStateAfterEvaluateJSTest : public WKBackForwardListSkipItemsTestBase { }; + +TEST_F(WKBackForwardListSkipItemsPushStateTest, BackForwardNavigationSkipsItemsWithoutUserGesture) { - runBackForwardNavigationSkipsItemsWithoutUserGestureTest([](WKWebView* webView, ASCIILiteral destination) { - // Do a call to evaluateJavaScript (with user gesture) *BEFORE* the pushState and make sure it doesn't count - // as a user gesture for the pushState(). - __block bool didRunScript = false; - [webView evaluateJavaScript:@"window.foo = 1;" completionHandler:^(id, NSError *) { - didRunScript = true; - }]; - TestWebKitAPI::Util::run(&didRunScript); - [webView _evaluateJavaScriptWithoutUserGesture:makeString("history.pushState(null, document.title, "_s, destination, ");"_s).createNSString().get() completionHandler:nil]; - }); + runBackForwardNavigationSkipsItemsWithoutUserGestureCheck(*s_fixture); +} + +TEST_F(WKBackForwardListSkipItemsPushStateTest, JSHistoryBackDoesNotSkipItemsWithoutUserGesture) +{ + runJSHistoryBackDoesNotSkipItemsWithoutUserGestureCheck(*s_fixture); +} + +TEST_F(WKBackForwardListSkipItemsFragmentTest, BackForwardNavigationSkipsItemsWithoutUserGesture) +{ + runBackForwardNavigationSkipsItemsWithoutUserGestureCheck(*s_fixture); +} + +TEST_F(WKBackForwardListSkipItemsFragmentTest, JSHistoryBackDoesNotSkipItemsWithoutUserGesture) +{ + runJSHistoryBackDoesNotSkipItemsWithoutUserGestureCheck(*s_fixture); +} + +TEST_F(WKBackForwardListSkipItemsPushStateAfterEvaluateJSTest, BackForwardNavigationSkipsItemsWithoutUserGesture) +{ + runBackForwardNavigationSkipsItemsWithoutUserGestureCheck(*s_fixture); +} + +TEST_F(WKBackForwardListSkipItemsPushStateAfterEvaluateJSTest, JSHistoryBackDoesNotSkipItemsWithoutUserGesture) +{ + runJSHistoryBackDoesNotSkipItemsWithoutUserGestureCheck(*s_fixture); } TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGestureSubframe) From 7c6ce8fd9c0a1105701414aeee63f466e8454f11 Mon Sep 17 00:00:00 2001 From: Nikolas Zimmermann Date: Thu, 14 May 2026 16:09:20 -0700 Subject: [PATCH 049/424] [GTK][WPE][Tools] Make wkdev-build container ephemeral https://bugs.webkit.org/show_bug.cgi?id=314841 Reviewed by Carlos Alberto Lopez Perez. The persistent container required manual `podman rm` whenever its create arguments changed, paid the first-run recursive chown cost, and needed a `podman start` step after host reboots. Switch to `podman run --rm --init` per host wrapper invocation. Run as $UID:$GID with `--userns keep-id`; /run/user/ comes from a `--mount type=tmpfs,chown=true` whose ownership is set at mount time, which removes the privileged init phase and the recursive chown. An inline entrypoint refreshes the Wayland / PipeWire socket symlinks and flatpak helper dirs in $XDG_RUNTIME_DIR, then execs the user command. Add podman/catatonit/crun as required packages to the dependencies files. Also fix coredumps: Mount /var/lib/systemd/coredump and /var/log/journal (read-only) into the wkdev-build container so that coredumpctl works inside it. Crashes inside the container are handled by the host's core_pattern / systemd-coredump, which writes to /var/lib/systemd/coredump; without these mounts the files and their journal metadata are invisible to coredumpctl running in the container. * Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py: (_translate_host_path_to_container): (_build_podman_run_args): (maybe_enter_webkit_container_sdk): (_podman_container_info): Deleted. (_build_podman_create_args): Deleted. (_init_and_sync_container_runtime): Deleted. (_stop_and_remove_container): Deleted. (_create_container): Deleted. (_ensure_container_ready): Deleted. * Tools/glib/dependencies/apt: * Tools/glib/dependencies/dnf: * Tools/glib/dependencies/pacman: Canonical link: https://commits.webkit.org/313275@main --- .../port/linux_container_sdk_utils.py | 240 ++++++++---------- Tools/glib/dependencies/apt | 9 + Tools/glib/dependencies/dnf | 9 + Tools/glib/dependencies/pacman | 9 + 4 files changed, 126 insertions(+), 141 deletions(-) diff --git a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py index 95604b59f568..663cd2d60acc 100644 --- a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py +++ b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py @@ -30,7 +30,6 @@ WKDEV_CONTAINER_NAME = 'wkdev-build' WKDEV_SDK_VERSION_FILENAME = '.wkdev-sdk-version' WKDEV_SDK_IMAGE_REPOSITORY = 'ghcr.io/igalia/wkdev-sdk' -WKDEV_INIT_DONE_FILE = '/run/.wkdev-init-done' WEBKIT_CONTAINER_SDK_DOCS_URL = 'https://github.com/Igalia/webkit-container-sdk' # Signals to Perl callers (via container-sdk-autoenter) that auto-enter @@ -148,23 +147,6 @@ def _translate_host_path_to_container(host_path): return host_path -def _podman_container_info(name): - """Returns (image, status) for an existing container, or (None, None) if missing.""" - try: - result = subprocess.run( - ['podman', 'inspect', '--type', 'container', - '--format', '{{.ImageName}}|{{.State.Status}}', name], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - if result.returncode != 0: - return None, None - parts = result.stdout.decode().strip().split('|', 1) - if len(parts) != 2: - return None, None - return parts[0], parts[1] - except FileNotFoundError: - return None, None - - def _xdg_runtime_dir(): return os.environ.get('XDG_RUNTIME_DIR') or '/run/user/{}'.format(os.getuid()) @@ -192,19 +174,39 @@ def _container_hostname(): return hostname -def _build_podman_create_args(pinned_version): - image_ref = '{}:{}'.format(WKDEV_SDK_IMAGE_REPOSITORY, pinned_version) +def _build_podman_run_args(pinned_version): + """Static `podman run` flags for an ephemeral wkdev-build container. + + The caller appends env-var forwards, --workdir, --tty/--interactive, the + image ref, the inline init wrapper, and the user command.""" container_home = _container_home_path() user = os.environ.get('USER') or os.environ.get('LOGNAME') uid = os.getuid() + gid = os.getgid() xdg = _xdg_runtime_dir() args = [ - '--name', WKDEV_CONTAINER_NAME, + # Ephemeral: container is removed as soon as the user command exits. + '--rm', + # Name the container after the invoking process so `podman ps` shows + # `wkdev-build-` instead of a random `tender_bhabha`-style name. + # We `execvp` into podman, so this PID is also podman's PID for the + # container's full lifetime -- collision-free across concurrent + # invocations, and `--rm` frees the name when the command exits. + '--name', '{}-{}'.format(WKDEV_CONTAINER_NAME, os.getpid()), + # tini-style PID 1 forwards signals and reaps zombies, so the user + # command does not have to play PID 1 itself. `--init` requires the + # `catatonit` helper on the host and only works with `crun`, not + # `runc`, so pin the runtime here rather than relying on the host + # default. + '--init', + '--runtime', 'crun', '--hostname', _container_hostname(), - '--workdir', '/home/{}'.format(user), '--userns', 'keep-id', - '--user', 'root:root', + # Run as the invoking host user directly. The tmpfs mount for + # /run/user/ below is created with the right ownership at mount + # time, so we no longer need a privileged init phase to chown it. + '--user', '{}:{}'.format(uid, gid), '--security-opt', 'label=disable', '--security-opt', 'unmask=ALL', '--security-opt', 'seccomp=unconfined', @@ -217,14 +219,19 @@ def _build_podman_create_args(pinned_version): '--ulimit', 'host', '--pids-limit', '-1', '--tmpfs', '/tmp', - # No --pid host: crun rejects sharing the host PID namespace when the - # container is not creating its own cgroup ("containers not creating - # Cgroups must create a private PID namespace"). The build container - # runs `sleep infinity` as its own PID 1 with no systemd inside, so a - # private PID namespace is fine. + # XDG_RUNTIME_DIR as a per-uid tmpfs with the correct ownership and + # mode set at mount time. `chown=true` makes podman chown the mount + # point to the container's user (our invoking host UID via keep-id), + # so the entrypoint can populate it (Wayland / PipeWire / flatpak + # symlinks) without a privileged setup step. --tmpfs's option syntax + # rejects uid=/gid=, which is why we use --mount type=tmpfs here. + '--mount', 'type=tmpfs,destination=/run/user/{},tmpfs-mode=0700,chown=true'.format(uid), '--ipc', 'host', '--network', 'host', - '--pull=newer', + # Pull only when the tag is not already cached. .wkdev-sdk-version + # uses immutable tags, so a registry round-trip per host-wrapper + # invocation would be wasted work; new tags still pull on first use. + '--pull=missing', '--label', 'io.webkit.container={}'.format(WKDEV_CONTAINER_NAME), '--label', 'org.opencontainers.image.version={}'.format(pinned_version), '--env', 'WEBKIT_CONTAINER_SDK=1', @@ -295,116 +302,56 @@ def _build_podman_create_args(pinned_version): if os.path.isdir(dconf_dir): args += _bind_mount(dconf_dir, dconf_dir) + # coredumpctl: share the coredump store and journal so `coredumpctl` works + # inside the container. Crashes inside the container are caught by the + # host's core_pattern handler (systemd-coredump), which writes to + # /var/lib/systemd/coredump; without these mounts the files and their + # journal metadata are invisible to tools running in the container. + if os.path.isdir('/var/lib/systemd/coredump'): + args += _bind_mount('/var/lib/systemd/coredump', '/var/lib/systemd/coredump') + if os.path.isdir('/var/log/journal'): + args += _bind_mount('/var/log/journal', '/var/log/journal', options='ro,rslave') + # Host runtime dir is exposed as /host/run so Wayland / PipeWire / flatpak # sockets can be symlinked into the container's XDG_RUNTIME_DIR per exec. if os.path.isdir(xdg): args += _bind_mount(xdg, '/host/run', options='bind-propagation=rslave') - args += [image_ref, 'sleep', 'infinity'] return args -def _init_and_sync_container_runtime(): - # Fused into one `podman exec` to avoid an extra round-trip per host - # wrapper invocation. The init step is idempotent via a /run sentinel - # (tmpfs, so it naturally resets on container recreation); the sync step - # mirrors webkit-container-sdk's .wkdev-sync-runtime-state and runs every - # invocation to symlink the live host Wayland/PipeWire sockets and flatpak - # helper dirs from /host/run into the container's XDG_RUNTIME_DIR. - uid = os.getuid() - gid = os.getgid() - wayland = os.environ.get('WAYLAND_DISPLAY', 'wayland-0') - pipewire = os.environ.get('PIPEWIRE_REMOTE', 'pipewire-0') - # WAYLAND_DISPLAY / PIPEWIRE_REMOTE are forwarded as positional args ($1, $2) - # instead of interpolated into the script body, so shell metacharacters in - # their values cannot break out of the quoting. - script = ( - 'set +e\n' - 'init_rc=0\n' - 'if [ ! -f {done} ]; then\n' - ' (set -e\n' - ' mkdir -p /run/user/{uid}\n' - ' chmod 700 /run/user/{uid}\n' - ' chown {uid}:{gid} /run/user/{uid}\n' - ' for d in /jhbuild /opt/rust /sdk; do\n' - ' [ -d "$d" ] && chown -R {uid}:{gid} "$d"\n' - ' done\n' - ' touch {done})\n' - ' init_rc=$?\n' - 'fi\n' - # Wayland/PipeWire sockets are addressed by name (wayland-0, pipewire-0) - # but the target inode can change across compositor restarts, so always - # refresh the symlink. The flatpak helper dirs below are stable; guard - # those with [ -L ] to skip the mkdir+ln on the steady-state path. - 'for s in "$1" "$2"; do\n' - ' [ -e "/host/run/$s" ] && ln -sfn "/host/run/$s" "$XDG_RUNTIME_DIR/$s"\n' - ' [ -e "/host/run/$s.lock" ] && ln -sfn "/host/run/$s.lock" "$XDG_RUNTIME_DIR/$s.lock"\n' - 'done\n' - 'for d in .flatpak .flatpak-helper doc; do\n' - ' if [ ! -L "$XDG_RUNTIME_DIR/$d" ]; then\n' - ' mkdir -p "/host/run/$d" 2>/dev/null\n' - ' ln -sfn "/host/run/$d" "$XDG_RUNTIME_DIR/$d" 2>/dev/null\n' - ' fi\n' - 'done\n' - 'exit $init_rc\n' - ).format(uid=uid, gid=gid, done=WKDEV_INIT_DONE_FILE) - rc = subprocess.call(['podman', 'exec', '--user', 'root', WKDEV_CONTAINER_NAME, - 'sh', '-c', script, 'wkdev-init', wayland, pipewire], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - if rc != 0: - _print_prominent_warning([ - "WARNING: First-run init for '{}' exited with status {}.".format(WKDEV_CONTAINER_NAME, rc), - ' Some build paths inside the container may have wrong ownership.', - ]) - - -def _stop_and_remove_container(): - subprocess.call(['podman', 'rm', '--force', WKDEV_CONTAINER_NAME], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - -def _create_container(pinned_version): - os.makedirs(_container_home_path(), mode=0o750, exist_ok=True) - - print("Creating WebKit Container SDK container named '{}' (this may take a while)...".format(WKDEV_CONTAINER_NAME), - file=sys.stderr) - create_cmd = ['podman', 'create'] + _build_podman_create_args(pinned_version) - rc = subprocess.call(create_cmd) - if rc != 0: - print("ERROR: Failed to create WebKit Container SDK container named '{}'.".format(WKDEV_CONTAINER_NAME), - file=sys.stderr) - sys.exit(rc) - - -def _ensure_container_ready(pinned_version): - """Bring the wkdev-build container into 'running' state at the pinned version.""" - image, status = _podman_container_info(WKDEV_CONTAINER_NAME) - expected_image = '{}:{}'.format(WKDEV_SDK_IMAGE_REPOSITORY, pinned_version) - if image is None: - _create_container(pinned_version) - status = 'created' - elif image != expected_image: - _print_prominent_warning([ - "Existing '{}' container uses image '{}',".format(WKDEV_CONTAINER_NAME, image), - "but {} pins '{}'. Recreating the container.".format(WKDEV_SDK_VERSION_FILENAME, pinned_version), - "Note: any changes inside the container filesystem will be lost;", - "the container home directory survives.", - ]) - _stop_and_remove_container() - _create_container(pinned_version) - status = 'created' - - if status != 'running': - rc = subprocess.call(['podman', 'start', WKDEV_CONTAINER_NAME], stdout=subprocess.DEVNULL) - if rc != 0: - print("ERROR: Failed to start container '{}'.".format(WKDEV_CONTAINER_NAME), file=sys.stderr) - sys.exit(rc) +# The tmpfs mount in `_build_podman_run_args` prepares $XDG_RUNTIME_DIR with +# the right ownership before this script runs, so we can populate it from the +# user without a privileged setup step. WAYLAND_DISPLAY and PIPEWIRE_REMOTE +# come in as positional args ($1, $2) so shell metacharacters in their values +# cannot break out of the quoting. +# +# Kept as a single-line script (`;` between statements, no embedded newlines) +# so that `podman ps` shows it as one truncated `sh -c ...` cell instead of +# wrapping across multiple lines. +_EPHEMERAL_ENTRYPOINT_SCRIPT = ( + 'set -e; ' + 'wayland_name=$1; pipewire_name=$2; shift 2; ' + # Wayland/PipeWire sockets are addressed by name but the target inode can + # change across compositor restarts, so always refresh the symlink. + 'for s in "$wayland_name" "$pipewire_name"; do ' + '[ -e "/host/run/$s" ] && ln -sfn "/host/run/$s" "$XDG_RUNTIME_DIR/$s"; ' + '[ -e "/host/run/$s.lock" ] && ln -sfn "/host/run/$s.lock" "$XDG_RUNTIME_DIR/$s.lock"; ' + 'done; ' + 'for d in .flatpak .flatpak-helper doc; do ' + 'mkdir -p "/host/run/$d" 2>/dev/null || true; ' + 'ln -sfn "/host/run/$d" "$XDG_RUNTIME_DIR/$d"; ' + 'done; ' + 'exec "$@"' +) def maybe_enter_webkit_container_sdk(argv=None): """If invoked on the host (outside a wkdev-sdk container), re-execute the - current command inside the 'wkdev-build' container, creating the container - on first use using the version pinned in .wkdev-sdk-version. + current command inside an ephemeral wkdev-build container at the SDK + version pinned in .wkdev-sdk-version. The container is removed (--rm) as + soon as the command exits, so there is no persistent state to keep in + sync, no recreate-on-arg-change problem, and no reboot recovery to do. When already running inside a wkdev-sdk container, verify the running SDK version matches the pinned one and warn loudly if not, but continue. The @@ -447,7 +394,7 @@ def maybe_enter_webkit_container_sdk(argv=None): ' Running container SDK version: {}'.format(running_version), ' Pinned by .wkdev-sdk-version: {}'.format(pinned_version), ' Re-run any wrapper script (build-webkit, run-webkit-tests, ...)', - ' from the host to recreate the container at the pinned version.', + ' from the host to launch a fresh container at the pinned version.', ' Continuing with the current container.', ]) return @@ -471,32 +418,43 @@ def maybe_enter_webkit_container_sdk(argv=None): ]) return - _ensure_container_ready(pinned_version) - _init_and_sync_container_runtime() + # Container-internal home is bind-mounted from a host directory that must + # exist before `podman run` starts the container. + os.makedirs(_container_home_path(), mode=0o750, exist_ok=True) + image_ref = '{}:{}'.format(WKDEV_SDK_IMAGE_REPOSITORY, pinned_version) container_cwd = _translate_host_path_to_container(os.getcwd()) host_command = os.path.realpath(argv[0]) container_command = _translate_host_path_to_container(host_command) - exec_cmd = [ - 'podman', 'exec', - '--user', '{}:{}'.format(os.getuid(), os.getgid()), - '--workdir', container_cwd, - '--detach-keys=', - ] + run_cmd = ['podman', 'run'] + _build_podman_run_args(pinned_version) + run_cmd += ['--workdir', container_cwd, '--detach-keys='] if sys.stdin.isatty() and sys.stdout.isatty(): - exec_cmd += ['--tty', '--interactive'] + run_cmd += ['--tty', '--interactive'] else: - exec_cmd += ['--interactive'] + run_cmd += ['--interactive'] for var in sorted(os.environ): if _env_var_should_be_forwarded(var): - exec_cmd += ['--env', '{}={}'.format(var, os.environ[var])] - exec_cmd += [WKDEV_CONTAINER_NAME, container_command] + argv[1:] + run_cmd += ['--env', '{}={}'.format(var, os.environ[var])] + + wayland = os.environ.get('WAYLAND_DISPLAY', 'wayland-0') + pipewire = os.environ.get('PIPEWIRE_REMOTE', 'pipewire-0') + # Image, then the inline entrypoint (`sh -c diff --git a/LayoutTests/media/video-timeupdate-before-seeking-expected.txt b/LayoutTests/media/video-timeupdate-before-seeking-expected.txt new file mode 100644 index 000000000000..b15593e1bd1a --- /dev/null +++ b/LayoutTests/media/video-timeupdate-before-seeking-expected.txt @@ -0,0 +1,4 @@ + + +PASS timeupdate queued before an explicit seek must not dispatch ahead of 'seeking' + diff --git a/LayoutTests/media/video-timeupdate-before-seeking.html b/LayoutTests/media/video-timeupdate-before-seeking.html new file mode 100644 index 000000000000..089f776c5e93 --- /dev/null +++ b/LayoutTests/media/video-timeupdate-before-seeking.html @@ -0,0 +1,76 @@ + + +A timeupdate event queued before an explicit seek must not dispatch ahead of 'seeking' + + + + + + + + diff --git a/Source/WebCore/html/HTMLMediaElement.cpp b/Source/WebCore/html/HTMLMediaElement.cpp index f9c25bc652f6..952f6186ad4f 100644 --- a/Source/WebCore/html/HTMLMediaElement.cpp +++ b/Source/WebCore/html/HTMLMediaElement.cpp @@ -3936,6 +3936,19 @@ void HTMLMediaElement::seekWithTolerance(const SeekTarget& target, bool fromDOM) // 4 - Set the seeking IDL attribute to true. // The flag will be cleared when the engine tells us the time has actually changed. setSeeking(true); + + // Drop any queued timeupdate task before the seekTask (or any other task) + // has a chance to dispatch, per the HTML spec time-marches-on algorithm + // (https://html.spec.whatwg.org/multipage/media.html#time-marches-on, step 6): + // "(In the other cases, such as explicit seeks, relevant events get fired + // as part of the overall process of changing the current playback + // position.)" + // This must happen synchronously from the DOM-side entry point: the seek + // path below enqueues seekTask onto the same task queue where a pending + // timeupdate may already be waiting, and the event loop would otherwise + // dispatch the timeupdate first. + m_timeupdateCancellationGroup.cancel(); + if (m_playing) { if (m_lastSeekTime < now) addPlayedRange(m_lastSeekTime, now); @@ -4026,7 +4039,8 @@ void HTMLMediaElement::seekTask() ALWAYS_LOG(LOGIDENTIFIER, "ignored seek to ", time); if (time == now) { scheduleEvent(eventNames().seekingEvent); - scheduleTimeupdateEvent(false); + // Emit directly: scheduleTimeupdateEvent's m_seeking guard would drop it. + scheduleEvent(eventNames().timeupdateEvent); scheduleEvent(eventNames().seekedEvent); if (protect(document())->quirks().needsCanPlayAfterSeekedQuirk() && m_readyState > HAVE_CURRENT_DATA) @@ -4042,13 +4056,6 @@ void HTMLMediaElement::seekTask() m_pendingSeekType = thisSeekType; setSeeking(true); - // Before scheduling the 'seeking' event, drop any queued periodic timeupdate - // task. Without this, a periodic timeupdate queued by playbackProgressTimerFired - // just before setCurrentTime could dispatch ahead of 'seeking', producing the - // spec-incorrect event ordering observed in mediasource-duration.html. The - // seek-completion timeupdate is queued separately by finishSeek and survives. - m_periodicTimeupdateCancellationGroup.cancel(); - // 10 - Queue a task to fire a simple event named seeking at the element. scheduleEvent(eventNames().seekingEvent); @@ -5066,9 +5073,9 @@ void HTMLMediaElement::scheduleTimeupdateEvent(bool periodicEvent) // Per HTML spec, the periodic timeupdate is only for "the time reached through the // usual monotonic increase of the current playback position during normal playback". // During an active seek, the seek algorithm's own events (seeking -> timeupdate -> - // seeked via finishSeek) are responsible for notifying the page. Suppress periodic - // timeupdates while seeking so they don't interleave with the seek-driven ordering. - if (periodicEvent && m_seeking) + // seeked via finishSeek) are responsible for notifying the page. Suppress timeupdates + // while seeking so they don't interleave with the seek-driven ordering. + if (m_seeking) return; MonotonicTime now = MonotonicTime::now(); @@ -5085,14 +5092,14 @@ void HTMLMediaElement::scheduleTimeupdateEvent(bool periodicEvent) // event at a given time so filter here MediaTime movieTime = currentMediaTime(); if (movieTime != m_lastTimeUpdateEventMovieTime) { - if (periodicEvent) { - // Periodic timeupdates are cancellable by the seek path — if a seek starts - // before this task dispatches, the pending timeupdate would race ahead of - // the 'seeking' event, producing spec-incorrect event ordering that fails - // mediasource-duration.html. - queueCancellableTaskToDispatchEvent(*this, TaskSource::MediaElement, m_periodicTimeupdateCancellationGroup, Event::create(eventNames().timeupdateEvent, Event::CanBubble::No, Event::IsCancelable::Yes)); - } else - scheduleEvent(eventNames().timeupdateEvent); + // Route through the cancellation group so seek() can drop the pending + // timeupdate before queueing 'seeking', per the HTML spec + // time-marches-on algorithm + // (https://html.spec.whatwg.org/multipage/media.html#time-marches-on, step 6): + // "(In the other cases, such as explicit seeks, relevant events get + // fired as part of the overall process of changing the current + // playback position.)" + queueCancellableTaskToDispatchEvent(*this, TaskSource::MediaElement, m_timeupdateCancellationGroup, Event::create(eventNames().timeupdateEvent, Event::CanBubble::No, Event::IsCancelable::Yes)); m_clockTimeAtLastUpdateEvent = now; m_lastTimeUpdateEventMovieTime = movieTime; } diff --git a/Source/WebCore/html/HTMLMediaElement.h b/Source/WebCore/html/HTMLMediaElement.h index e18817dbb168..a4c6bd85bd46 100644 --- a/Source/WebCore/html/HTMLMediaElement.h +++ b/Source/WebCore/html/HTMLMediaElement.h @@ -1215,7 +1215,7 @@ class HTMLMediaElement TaskCancellationGroup m_updateShouldAutoplayTaskCancellationGroup; RefPtr m_playedTimeRanges; TaskCancellationGroup m_asyncEventsCancellationGroup; - TaskCancellationGroup m_periodicTimeupdateCancellationGroup; + TaskCancellationGroup m_timeupdateCancellationGroup; TaskCancellationGroup m_volumeRevertTaskCancellationGroup; PlayPromiseVector m_pendingPlayPromises; From 5e57a5812ef4218e611a1f7601bf20544267394c Mon Sep 17 00:00:00 2001 From: Yusuke Suzuki Date: Thu, 14 May 2026 17:47:07 -0700 Subject: [PATCH 058/424] [JSC] Cache result of Symbol.prototype.toString https://bugs.webkit.org/show_bug.cgi?id=314842 rdar://177099930 Reviewed by Yijia Huang. We found that this is relatively frequently called and we are generating JSString each time. This patch adds some fields to cache the idempotent string results from Symbol. As we already hash-consing Symbols, this cache works well for the same Symbol globally. We also added SymbolToString DFG node to handle this efficiently, and the fast path is just loading a value from the cached field of Symbol. Test: JSTests/stress/symbol-prototype-to-string-intrinsic.js * JSTests/stress/symbol-prototype-to-string-intrinsic.js: Added. (shouldBe): (shouldThrow): (test): (viaString): (viaToString): (desc): (const.descriptor.toString): * Source/JavaScriptCore/b3/B3AbstractHeapRepository.h: * Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h: (JSC::DFG::AbstractInterpreter::executeEffects): * Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp: (JSC::DFG::ByteCodeParser::handleIntrinsicCall): * Source/JavaScriptCore/dfg/DFGClobberize.h: (JSC::DFG::clobberize): * Source/JavaScriptCore/dfg/DFGDoesGC.cpp: (JSC::DFG::doesGC): * Source/JavaScriptCore/dfg/DFGFixupPhase.cpp: (JSC::DFG::FixupPhase::fixupNode): * Source/JavaScriptCore/dfg/DFGNodeType.h: * Source/JavaScriptCore/dfg/DFGOperations.cpp: (JSC::DFG::JSC_DEFINE_JIT_OPERATION): * Source/JavaScriptCore/dfg/DFGOperations.h: * Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp: * Source/JavaScriptCore/dfg/DFGSafeToExecute.h: (JSC::DFG::safeToExecute): * Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp: * Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h: * Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp: (JSC::DFG::SpeculativeJIT::compile): * Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp: (JSC::DFG::SpeculativeJIT::compile): * Source/JavaScriptCore/ftl/FTLCapabilities.cpp: (JSC::FTL::canCompile): * Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp: (JSC::FTL::DFG::LowerDFGToB3::compileNode): (JSC::FTL::DFG::LowerDFGToB3::compileSymbolToString): * Source/JavaScriptCore/runtime/Intrinsic.h: * Source/JavaScriptCore/runtime/StringConstructor.cpp: (JSC::stringConstructor): * Source/JavaScriptCore/runtime/Symbol.cpp: (JSC::Symbol::Symbol): (JSC::Symbol::toString): (JSC::Symbol::visitChildrenImpl): (JSC::Symbol::description): (JSC::Symbol::createWithDescription): (JSC::Symbol::description const): Deleted. * Source/JavaScriptCore/runtime/Symbol.h: * Source/JavaScriptCore/runtime/SymbolConstructor.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): * Source/JavaScriptCore/runtime/SymbolPrototype.cpp: (JSC::JSC_DEFINE_CUSTOM_GETTER): (JSC::JSC_DEFINE_HOST_FUNCTION): Canonical link: https://commits.webkit.org/313284@main --- .../symbol-prototype-to-string-intrinsic.js | 160 ++++++++++++++++++ .../b3/B3AbstractHeapRepository.h | 2 + .../dfg/DFGAbstractInterpreterInlines.h | 5 + .../JavaScriptCore/dfg/DFGByteCodeParser.cpp | 15 +- Source/JavaScriptCore/dfg/DFGClobberize.h | 1 + Source/JavaScriptCore/dfg/DFGDoesGC.cpp | 1 + Source/JavaScriptCore/dfg/DFGFixupPhase.cpp | 5 + Source/JavaScriptCore/dfg/DFGNodeType.h | 1 + Source/JavaScriptCore/dfg/DFGOperations.cpp | 22 ++- Source/JavaScriptCore/dfg/DFGOperations.h | 1 + .../dfg/DFGPredictionPropagationPhase.cpp | 4 + Source/JavaScriptCore/dfg/DFGSafeToExecute.h | 1 + .../JavaScriptCore/dfg/DFGSpeculativeJIT.cpp | 19 +++ Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h | 1 + .../dfg/DFGSpeculativeJIT32_64.cpp | 5 + .../dfg/DFGSpeculativeJIT64.cpp | 5 + Source/JavaScriptCore/ftl/FTLCapabilities.cpp | 1 + Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp | 24 +++ Source/JavaScriptCore/runtime/Intrinsic.h | 1 + .../runtime/StringConstructor.cpp | 16 +- Source/JavaScriptCore/runtime/Symbol.cpp | 55 +++++- Source/JavaScriptCore/runtime/Symbol.h | 17 +- .../runtime/SymbolConstructor.cpp | 8 +- .../runtime/SymbolPrototype.cpp | 15 +- 24 files changed, 347 insertions(+), 38 deletions(-) create mode 100644 JSTests/stress/symbol-prototype-to-string-intrinsic.js diff --git a/JSTests/stress/symbol-prototype-to-string-intrinsic.js b/JSTests/stress/symbol-prototype-to-string-intrinsic.js new file mode 100644 index 000000000000..c3dbe20d2a4f --- /dev/null +++ b/JSTests/stress/symbol-prototype-to-string-intrinsic.js @@ -0,0 +1,160 @@ +function shouldBe(actual, expected) +{ + if (actual !== expected) + throw new Error('bad value: ' + actual + ' (expected: ' + expected + ')'); +} + +function shouldThrow(func, errorType) +{ + let threw = false; + try { + func(); + } catch (e) { + threw = true; + if (!(e instanceof errorType)) + throw new Error('unexpected error: ' + e); + } + if (!threw) + throw new Error('did not throw'); +} + +// 1. SymbolUse fast path: repeated .toString() on a Symbol with a description. +(function () { + function test(sym) { + return sym.toString(); + } + noInline(test); + + const sym = Symbol("cocoa"); + for (let i = 0; i < testLoopCount; ++i) + shouldBe(test(sym), "Symbol(cocoa)"); +}()); + +// 2. SymbolUse fast path: Symbol with no description must stringify as "Symbol()". +(function () { + function test(sym) { + return sym.toString(); + } + noInline(test); + + const sym = Symbol(); + for (let i = 0; i < testLoopCount; ++i) + shouldBe(test(sym), "Symbol()"); +}()); + +// 3. Well-known symbols should tier up and return "Symbol(Symbol.iterator)" etc. +(function () { + function test(sym) { + return sym.toString(); + } + noInline(test); + + for (let i = 0; i < testLoopCount; ++i) { + shouldBe(test(Symbol.iterator), "Symbol(Symbol.iterator)"); + shouldBe(test(Symbol.asyncIterator), "Symbol(Symbol.asyncIterator)"); + shouldBe(test(Symbol.hasInstance), "Symbol(Symbol.hasInstance)"); + } +}()); + +// 4. Cached result must be stable across many invocations (same observable value). +(function () { + function test(sym) { + return sym.toString(); + } + noInline(test); + + const sym = Symbol("stable"); + const first = test(sym); + for (let i = 0; i < testLoopCount; ++i) + shouldBe(test(sym), first); +}()); + +// 5. String(symbol) goes through stringConstructor's symbol fast path and should +// match Symbol.prototype.toString.call(symbol). +(function () { + function viaString(sym) { + return String(sym); + } + function viaToString(sym) { + return sym.toString(); + } + noInline(viaString); + noInline(viaToString); + + const sym = Symbol("matcha"); + for (let i = 0; i < testLoopCount; ++i) { + shouldBe(viaString(sym), "Symbol(matcha)"); + shouldBe(viaString(sym), viaToString(sym)); + } +}()); + +// 6. Symbol.prototype.description must return undefined (not null) for a +// descriptionless Symbol, per ECMA-262 20.4.3.2. +(function () { + function desc(sym) { + return sym.description; + } + noInline(desc); + + const withDesc = Symbol("rize"); + const withoutDesc = Symbol(); + for (let i = 0; i < testLoopCount; ++i) { + shouldBe(desc(withDesc), "rize"); + shouldBe(desc(withoutDesc), undefined); + } +}()); + +// 7. Symbol.prototype.toString.call on a non-Symbol must throw TypeError. +// Exercises the generic (non-intrinsified) path and its OSR behavior. +(function () { + const toString = Symbol.prototype.toString; + function test(receiver) { + return toString.call(receiver); + } + noInline(test); + + for (let i = 0; i < testLoopCount; ++i) { + shouldThrow(() => test({}), TypeError); + shouldThrow(() => test("not a symbol"), TypeError); + shouldThrow(() => test(42), TypeError); + shouldThrow(() => test(undefined), TypeError); + } +}()); + +// 8. Symbol created from a non-string description (coerced via toString) should +// preserve the coerced description across repeated toString() calls. +(function () { + function test(sym) { + return sym.toString(); + } + noInline(test); + + const descriptor = { toString() { return "coerced"; } }; + const sym = Symbol(descriptor); + for (let i = 0; i < testLoopCount; ++i) + shouldBe(test(sym), "Symbol(coerced)"); +}()); + +// 9. OSR-exit path: feed a non-Symbol to a function that has tiered up on +// Symbols. The SymbolUse speculation must fail cleanly and the call must +// fall back to the generic toString (which throws TypeError for non-Symbol +// receivers when invoked via Symbol.prototype.toString.call). +(function () { + const toString = Symbol.prototype.toString; + function test(receiver) { + return toString.call(receiver); + } + noInline(test); + + const sym = Symbol("kilimanjaro"); + // Warm up on Symbol to encourage speculation. + for (let i = 0; i < testLoopCount; ++i) + shouldBe(test(sym), "Symbol(kilimanjaro)"); + + // Now hit the non-Symbol path. + shouldThrow(() => test({}), TypeError); + + // And make sure the Symbol path still works after OSR exit. + for (let i = 0; i < testLoopCount; ++i) + shouldBe(test(sym), "Symbol(kilimanjaro)"); +}()); diff --git a/Source/JavaScriptCore/b3/B3AbstractHeapRepository.h b/Source/JavaScriptCore/b3/B3AbstractHeapRepository.h index 1a8b2fd9937a..281788fb8d15 100644 --- a/Source/JavaScriptCore/b3/B3AbstractHeapRepository.h +++ b/Source/JavaScriptCore/b3/B3AbstractHeapRepository.h @@ -209,7 +209,9 @@ namespace JSC::B3 { macro(WebAssemblyFunctionBase_targetInstance, WebAssemblyFunctionBase::offsetOfTargetInstance(), Mutability::Immutable) \ macro(WebAssemblyGCStructure_rtt, WebAssemblyGCStructure::offsetOfRTT(), Mutability::Immutable) \ macro(WebAssemblyModuleRecord_exportsObject, WebAssemblyModuleRecord::offsetOfExportsObject(), Mutability::Mutable) \ + macro(Symbol_description, Symbol::offsetOfDescription(), Mutability::Mutable) \ macro(Symbol_symbolImpl, Symbol::offsetOfSymbolImpl(), Mutability::Immutable) \ + macro(Symbol_string, Symbol::offsetOfString(), Mutability::Mutable) \ #define FOR_EACH_INDEXED_ABSTRACT_HEAP(macro) \ macro(ArrayStorage_vector, ArrayStorage::vectorOffset(), sizeof(WriteBarrier)) \ diff --git a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h index f90713a51f3f..1dab7021a710 100644 --- a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h +++ b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h @@ -4129,6 +4129,11 @@ bool AbstractInterpreter::executeEffects(unsigned clobberLimi break; } + case SymbolToString: { + setTypeForNode(node, SpecStringResolved); + break; + } + case ToObject: case CallObjectConstructor: { AbstractValue& source = forNode(node->child1()); diff --git a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp index d0baa9981ee2..8667c1b50f04 100644 --- a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp +++ b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp @@ -3683,7 +3683,20 @@ auto ByteCodeParser::handleIntrinsicCall(Node* callee, Operand resultOperand, Ca setResult(resultNode); return CallOptimizationResult::Inlined; } - + + case SymbolPrototypeToStringIntrinsic: { + if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadType)) + return CallOptimizationResult::DidNothing; + if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadConstantValue)) + return CallOptimizationResult::DidNothing; + if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadCache)) + return CallOptimizationResult::DidNothing; + + insertChecks(); + setResult(addToGraph(SymbolToString, Edge(get(virtualRegisterForArgumentIncludingThis(0, registerOffset)), SymbolUse))); + return CallOptimizationResult::Inlined; + } + case RoundIntrinsic: case FloorIntrinsic: case CeilIntrinsic: diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.h index 56d37ce3462c..210a2a9632b7 100644 --- a/Source/JavaScriptCore/dfg/DFGClobberize.h +++ b/Source/JavaScriptCore/dfg/DFGClobberize.h @@ -271,6 +271,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu case GetExecutable: case BottomValue: case TypeOf: + case SymbolToString: def(PureValue(node)); return; diff --git a/Source/JavaScriptCore/dfg/DFGDoesGC.cpp b/Source/JavaScriptCore/dfg/DFGDoesGC.cpp index ed3ca960c5e6..d90c7d4b1b3f 100644 --- a/Source/JavaScriptCore/dfg/DFGDoesGC.cpp +++ b/Source/JavaScriptCore/dfg/DFGDoesGC.cpp @@ -409,6 +409,7 @@ bool doesGC(Graph& graph, Node* node) case ObjectGetOwnPropertyNames: case ObjectGetOwnPropertySymbols: case ObjectToString: + case SymbolToString: case ReflectOwnKeys: case AllocatePropertyStorage: case ReallocatePropertyStorage: diff --git a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp index 65b4a485303f..6dd33c681bb1 100644 --- a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp +++ b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp @@ -2494,6 +2494,11 @@ class FixupPhase : public Phase { break; } + case SymbolToString: { + fixEdge(node->child1()); + break; + } + case CheckIdent: { if (node->uidOperand()->isSymbol()) fixEdge(node->child1()); diff --git a/Source/JavaScriptCore/dfg/DFGNodeType.h b/Source/JavaScriptCore/dfg/DFGNodeType.h index c28ed4fbfd55..5e0bb4cec8ce 100644 --- a/Source/JavaScriptCore/dfg/DFGNodeType.h +++ b/Source/JavaScriptCore/dfg/DFGNodeType.h @@ -327,6 +327,7 @@ namespace JSC { namespace DFG { macro(ObjectGetOwnPropertySymbols, NodeMustGenerate | NodeResultJS) \ macro(ObjectToString, NodeMustGenerate | NodeResultJS) \ macro(ReflectOwnKeys, NodeMustGenerate | NodeResultJS) \ + macro(SymbolToString, NodeResultJS) \ \ /* Atomics object functions. */\ macro(AtomicsAdd, NodeResultJS | NodeMustGenerate | NodeHasVarArgs) \ diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp index 0929a092aeb0..1feeda1a8062 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.cpp +++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp @@ -4202,11 +4202,9 @@ JSC_DEFINE_JIT_OPERATION(operationNewSymbolWithStringDescription, Symbol*, (JSGl CallFrame* callFrame = DECLARE_CALL_FRAME(vm); JITOperationPrologueCallFrameTracer tracer(vm, callFrame); auto scope = DECLARE_THROW_SCOPE(vm); - - auto string = description->value(globalObject); + auto value = description->value(globalObject); OPERATION_RETURN_IF_EXCEPTION(scope, nullptr); - - OPERATION_RETURN(scope, Symbol::createWithDescription(vm, WTF::move(string))); + OPERATION_RETURN(scope, Symbol::createWithDescription(vm, value, description)); } JSC_DEFINE_JIT_OPERATION(operationNewSymbolWithDescription, Symbol*, (JSGlobalObject* globalObject, EncodedJSValue encodedDescription)) @@ -4220,10 +4218,22 @@ JSC_DEFINE_JIT_OPERATION(operationNewSymbolWithDescription, Symbol*, (JSGlobalOb if (description.isUndefined()) OPERATION_RETURN(scope, Symbol::create(vm)); - String string = description.toWTFString(globalObject); + auto* string = description.toString(globalObject); + OPERATION_RETURN_IF_EXCEPTION(scope, nullptr); + + auto value = string->value(globalObject); OPERATION_RETURN_IF_EXCEPTION(scope, nullptr); - OPERATION_RETURN(scope, Symbol::createWithDescription(vm, string)); + OPERATION_RETURN(scope, Symbol::createWithDescription(vm, value, string)); +} + +JSC_DEFINE_JIT_OPERATION(operationSymbolToString, JSString*, (JSGlobalObject* globalObject, Symbol* symbol)) +{ + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + auto scope = DECLARE_THROW_SCOPE(vm); + OPERATION_RETURN(scope, symbol->toString(globalObject)); } JSC_DEFINE_JIT_OPERATION(operationNewStringObject, JSCell*, (VM* vmPointer, JSString* string, Structure* structure)) diff --git a/Source/JavaScriptCore/dfg/DFGOperations.h b/Source/JavaScriptCore/dfg/DFGOperations.h index 073007f08859..60a7c25c67e9 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.h +++ b/Source/JavaScriptCore/dfg/DFGOperations.h @@ -374,6 +374,7 @@ JSC_DECLARE_NOEXCEPT_JIT_OPERATION(operationPerformPromiseThenOneHandler, void, JSC_DECLARE_JIT_OPERATION(operationNewSymbol, Symbol*, (VM*)); JSC_DECLARE_JIT_OPERATION(operationNewSymbolWithStringDescription, Symbol*, (JSGlobalObject*, JSString*)); JSC_DECLARE_JIT_OPERATION(operationNewSymbolWithDescription, Symbol*, (JSGlobalObject*, EncodedJSValue)); +JSC_DECLARE_JIT_OPERATION(operationSymbolToString, JSString*, (JSGlobalObject*, Symbol*)); JSC_DECLARE_JIT_OPERATION(operationNewStringObject, JSCell*, (VM*, JSString*, Structure*)); JSC_DECLARE_JIT_OPERATION(operationToStringOnCell, JSString*, (JSGlobalObject*, JSCell*)); JSC_DECLARE_JIT_OPERATION(operationToString, JSString*, (JSGlobalObject*, EncodedJSValue)); diff --git a/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp b/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp index 709b6858d82a..095660de0d56 100644 --- a/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp +++ b/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp @@ -1413,6 +1413,10 @@ class PredictionPropagationPhase : public Phase { setPrediction(SpecString); break; + case SymbolToString: + setPrediction(SpecStringResolved); + break; + case Spread: setPrediction(SpecCellOther); break; diff --git a/Source/JavaScriptCore/dfg/DFGSafeToExecute.h b/Source/JavaScriptCore/dfg/DFGSafeToExecute.h index c6d22794b31a..3c0377c47303 100644 --- a/Source/JavaScriptCore/dfg/DFGSafeToExecute.h +++ b/Source/JavaScriptCore/dfg/DFGSafeToExecute.h @@ -582,6 +582,7 @@ bool safeToExecute(AbstractStateType& state, Graph& graph, Node* node, bool igno case ObjectGetOwnPropertyNames: case ObjectGetOwnPropertySymbols: case ObjectToString: + case SymbolToString: case ReflectOwnKeys: case SetLocal: case SetCallee: diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp index e2547ca119d7..65e39b8e7f51 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp @@ -15993,6 +15993,25 @@ void SpeculativeJIT::compileObjectToString(Node* node) } } +void SpeculativeJIT::compileSymbolToString(Node* node) +{ + SpeculateCellOperand argument(this, node->child1()); + GPRTemporary result(this); + + GPRReg argumentGPR = argument.gpr(); + GPRReg resultGPR = result.gpr(); + + speculateSymbol(node->child1(), argumentGPR); + + JumpList slowCases; + loadPtr(Address(argumentGPR, Symbol::offsetOfString()), resultGPR); + slowCases.append(branchTestPtr(Zero, resultGPR)); + + addSlowPathGenerator(slowPathCall(slowCases, this, operationSymbolToString, resultGPR, LinkableConstant::globalObject(*this, node), argumentGPR)); + + cellResult(resultGPR, node); +} + void SpeculativeJIT::compileCreateThis(Node* node) { // Note that there is not so much profit to speculate here. The only things we diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h index e946aab6467d..9eaa78fd7969 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h @@ -1779,6 +1779,7 @@ class SpeculativeJIT : public JITCompiler { void compileObjectAssign(Node*); void compileObjectCreate(Node*); void compileObjectToString(Node*); + void compileSymbolToString(Node*); void compileCreateThis(Node*); void compileCreatePromise(Node*); void compileCreateGenerator(Node*); diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp index 0c643da4a2ad..11124ef34af1 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp @@ -3313,6 +3313,11 @@ void SpeculativeJIT::compile(Node* node) break; } + case SymbolToString: { + compileSymbolToString(node); + break; + } + case CreateThis: { compileCreateThis(node); break; diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp index 0eb1b39b97c8..e8fc15927c4f 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp @@ -4685,6 +4685,11 @@ void SpeculativeJIT::compile(Node* node) break; } + case SymbolToString: { + compileSymbolToString(node); + break; + } + case CreateThis: { compileCreateThis(node); break; diff --git a/Source/JavaScriptCore/ftl/FTLCapabilities.cpp b/Source/JavaScriptCore/ftl/FTLCapabilities.cpp index 48b9e43f469c..5a3a38959049 100644 --- a/Source/JavaScriptCore/ftl/FTLCapabilities.cpp +++ b/Source/JavaScriptCore/ftl/FTLCapabilities.cpp @@ -250,6 +250,7 @@ inline CapabilityLevel canCompile(Node* node) case ObjectGetOwnPropertyNames: case ObjectGetOwnPropertySymbols: case ObjectToString: + case SymbolToString: case ReflectOwnKeys: case MakeRope: case MakeAtomString: diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp index c8078effd054..1667cdd41e65 100644 --- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp +++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp @@ -1239,6 +1239,9 @@ class LowerDFGToB3 { case ObjectToString: compileObjectToString(); break; + case SymbolToString: + compileSymbolToString(); + break; case NewObject: compileNewObject(); break; @@ -9514,6 +9517,27 @@ IGNORE_CLANG_WARNINGS_END } } + void compileSymbolToString() + { + JSGlobalObject* globalObject = m_graph.globalObjectFor(m_origin.semantic); + + LBasicBlock slowCase = m_out.newBlock(); + LBasicBlock continuation = m_out.newBlock(); + + auto symbol = lowSymbol(m_node->child1()); + auto cached = m_out.loadPtr(symbol, m_heaps.Symbol_string); + ValueFromBlock fastResult = m_out.anchor(cached); + m_out.branch(m_out.notNull(cached), usually(continuation), rarely(slowCase)); + + LBasicBlock lastNext = m_out.appendTo(slowCase, continuation); + auto slowResultValue = vmCall(pointerType(), operationSymbolToString, weakPointer(globalObject), symbol); + ValueFromBlock slowResult = m_out.anchor(slowResultValue); + m_out.jump(continuation); + + m_out.appendTo(continuation, lastNext); + setJSValue(m_out.phi(pointerType(), fastResult, slowResult)); + } + void compileObjectAssign() { JSGlobalObject* globalObject = m_graph.globalObjectFor(m_origin.semantic); diff --git a/Source/JavaScriptCore/runtime/Intrinsic.h b/Source/JavaScriptCore/runtime/Intrinsic.h index 899bef9f6da2..492e444b10e0 100644 --- a/Source/JavaScriptCore/runtime/Intrinsic.h +++ b/Source/JavaScriptCore/runtime/Intrinsic.h @@ -137,6 +137,7 @@ namespace JSC { macro(StringPrototypeSubstringIntrinsic) \ macro(StringPrototypeToLowerCaseIntrinsic) \ macro(StringPrototypeToUpperCaseIntrinsic) \ + macro(SymbolPrototypeToStringIntrinsic) \ macro(NumberPrototypeToStringIntrinsic) \ macro(NumberIsFiniteIntrinsic) \ macro(NumberIsNaNIntrinsic) \ diff --git a/Source/JavaScriptCore/runtime/StringConstructor.cpp b/Source/JavaScriptCore/runtime/StringConstructor.cpp index 08eb3e5895ca..437f4588fa27 100644 --- a/Source/JavaScriptCore/runtime/StringConstructor.cpp +++ b/Source/JavaScriptCore/runtime/StringConstructor.cpp @@ -163,19 +163,9 @@ JSC_DEFINE_HOST_FUNCTION(constructWithStringConstructor, (JSGlobalObject* global JSString* stringConstructor(JSGlobalObject* globalObject, JSValue argument) { - VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - if (argument.isSymbol()) { - auto description = asSymbol(argument)->tryGetDescriptiveString(); - if (!description) [[unlikely]] { - ASSERT(description.error() == ErrorTypeWithExtension::OutOfMemoryError); - throwOutOfMemoryError(globalObject, scope); - return nullptr; - } - return jsNontrivialString(vm, description.value()); - } - RELEASE_AND_RETURN(scope, argument.toString(globalObject)); + if (argument.isSymbol()) + return asSymbol(argument)->toString(globalObject); + return argument.toString(globalObject); } JSC_DEFINE_HOST_FUNCTION(callStringConstructor, (JSGlobalObject* globalObject, CallFrame* callFrame)) diff --git a/Source/JavaScriptCore/runtime/Symbol.cpp b/Source/JavaScriptCore/runtime/Symbol.cpp index 0fcee6634eac..62b8a5d6efc6 100644 --- a/Source/JavaScriptCore/runtime/Symbol.cpp +++ b/Source/JavaScriptCore/runtime/Symbol.cpp @@ -53,6 +53,13 @@ Symbol::Symbol(VM& vm, SymbolImpl& uid) { } +Symbol::Symbol(VM& vm, const String& string, JSString* description) + : Base(vm, vm.symbolStructure.get()) + , m_privateName(PrivateName::Description, string) + , m_description(description, WriteBarrierEarlyInit) +{ +} + void Symbol::finishCreation(VM& vm) { Base::finishCreation(vm); @@ -79,11 +86,40 @@ double Symbol::toNumber(JSGlobalObject* globalObject) const return 0.0; } +JSString* Symbol::toString(JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (auto* string = m_string.get()) + return string; + + auto description = tryGetDescriptiveString(); + if (!description) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + auto* string = jsNontrivialString(vm, description.value()); + m_string.set(vm, this, string); + ASSERT(!string->isRope()); + return string; +} + void Symbol::destroy(JSCell* cell) { static_cast(cell)->Symbol::~Symbol(); } +template +void Symbol::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisCell = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisCell, info()); + Base::visitChildren(thisCell, visitor); + visitor.append(thisCell->m_description); + visitor.append(thisCell->m_string); +} +DEFINE_VISIT_CHILDREN(Symbol); + Expected Symbol::tryGetDescriptiveString() const { String description = tryMakeString("Symbol("_s, StringView(m_privateName.uid()), ')'); @@ -92,10 +128,18 @@ Expected Symbol::tryGetDescriptiveString() const return description; } -String Symbol::description() const +JSString* Symbol::description(VM& vm) { + if (auto* string = m_description.get()) + return string; + auto& uid = m_privateName.uid(); - return uid.isNullSymbol() ? String() : uid; + if (uid.isNullSymbol()) + return nullptr; + + auto* string = jsString(vm, String(uid)); + m_description.set(vm, this, string); + return string; } Symbol* Symbol::create(VM& vm) @@ -112,6 +156,13 @@ Symbol* Symbol::createWithDescription(VM& vm, const String& description) return symbol; } +Symbol* Symbol::createWithDescription(VM& vm, const String& description, JSString* string) +{ + Symbol* symbol = new (NotNull, allocateCell(vm)) Symbol(vm, description, string); + symbol->finishCreation(vm); + return symbol; +} + Symbol* Symbol::create(VM& vm, SymbolImpl& uid) { if (Symbol* symbol = vm.symbolImplToSymbolMap.get(&uid)) diff --git a/Source/JavaScriptCore/runtime/Symbol.h b/Source/JavaScriptCore/runtime/Symbol.h index 85f85db5cefa..b31d9818715e 100644 --- a/Source/JavaScriptCore/runtime/Symbol.h +++ b/Source/JavaScriptCore/runtime/Symbol.h @@ -53,33 +53,42 @@ class Symbol final : public JSCell { static Symbol* create(VM&); static Symbol* createWithDescription(VM&, const String&); + static Symbol* createWithDescription(VM&, const String&, JSString*); JS_EXPORT_PRIVATE static Symbol* create(VM&, SymbolImpl& uid); SymbolImpl& uid() const { return m_privateName.uid(); } PrivateName privateName() const { return m_privateName; } - String NODELETE description() const; - Expected tryGetDescriptiveString() const; + JSString* description(VM&); JSValue NODELETE toPrimitive(JSGlobalObject*, PreferredPrimitiveType) const; JSObject* toObject(JSGlobalObject*) const; double toNumber(JSGlobalObject*) const; + JSString* toString(JSGlobalObject*); static constexpr ptrdiff_t offsetOfSymbolImpl() { // PrivateName is just a Ref which can just be used as a SymbolImpl*. return OBJECT_OFFSETOF(Symbol, m_privateName); } + static constexpr ptrdiff_t offsetOfString() { return OBJECT_OFFSETOF(Symbol, m_string); } + static constexpr ptrdiff_t offsetOfDescription() { return OBJECT_OFFSETOF(Symbol, m_description); } -private: - static void destroy(JSCell*); + DECLARE_VISIT_CHILDREN; + Expected tryGetDescriptiveString() const; + +private: Symbol(VM&); Symbol(VM&, const String&); + Symbol(VM&, const String&, JSString*); Symbol(VM&, SymbolImpl& uid); + static void destroy(JSCell*); void finishCreation(VM&); PrivateName m_privateName; + WriteBarrier m_description; + WriteBarrier m_string; }; Symbol* asSymbol(JSValue); diff --git a/Source/JavaScriptCore/runtime/SymbolConstructor.cpp b/Source/JavaScriptCore/runtime/SymbolConstructor.cpp index d7dab0a86038..472073642e0e 100644 --- a/Source/JavaScriptCore/runtime/SymbolConstructor.cpp +++ b/Source/JavaScriptCore/runtime/SymbolConstructor.cpp @@ -86,9 +86,13 @@ JSC_DEFINE_HOST_FUNCTION(callSymbol, (JSGlobalObject* globalObject, CallFrame* c if (description.isUndefined()) return JSValue::encode(Symbol::create(vm)); - String string = description.toWTFString(globalObject); + auto* string = description.toString(globalObject); RETURN_IF_EXCEPTION(scope, { }); - return JSValue::encode(Symbol::createWithDescription(vm, string)); + + auto value = string->value(globalObject); + RETURN_IF_EXCEPTION(scope, { }); + + return JSValue::encode(Symbol::createWithDescription(vm, value, string)); } JSC_DEFINE_HOST_FUNCTION(constructSymbol, (JSGlobalObject* globalObject, CallFrame* callFrame)) diff --git a/Source/JavaScriptCore/runtime/SymbolPrototype.cpp b/Source/JavaScriptCore/runtime/SymbolPrototype.cpp index f9695e08f286..95fcec275a6c 100644 --- a/Source/JavaScriptCore/runtime/SymbolPrototype.cpp +++ b/Source/JavaScriptCore/runtime/SymbolPrototype.cpp @@ -48,7 +48,7 @@ const ClassInfo SymbolPrototype::s_info = { "Symbol"_s, &Base::s_info, &symbolPr /* Source for SymbolPrototype.lut.h @begin symbolPrototypeTable description symbolProtoGetterDescription DontEnum|ReadOnly|CustomAccessor - toString symbolProtoFuncToString DontEnum|Function 0 + toString symbolProtoFuncToString DontEnum|Function 0 SymbolPrototypeToStringIntrinsic valueOf symbolProtoFuncValueOf DontEnum|Function 0 @end */ @@ -98,8 +98,9 @@ JSC_DEFINE_CUSTOM_GETTER(symbolProtoGetterDescription, (JSGlobalObject* globalOb return throwVMTypeError(globalObject, scope, SymbolDescriptionTypeError); scope.release(); Integrity::auditStructureID(symbol->structureID()); - auto description = symbol->description(); - return JSValue::encode(description.isNull() ? jsUndefined() : jsString(vm, WTF::move(description))); + if (auto* string = symbol->description(vm)) + return JSValue::encode(string); + return JSValue::encode(jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(symbolProtoFuncToString, (JSGlobalObject* globalObject, CallFrame* callFrame)) @@ -111,13 +112,7 @@ JSC_DEFINE_HOST_FUNCTION(symbolProtoFuncToString, (JSGlobalObject* globalObject, if (!symbol) return throwVMTypeError(globalObject, scope, SymbolToStringTypeError); Integrity::auditStructureID(symbol->structureID()); - auto description = symbol->tryGetDescriptiveString(); - if (!description) [[unlikely]] { - ASSERT(description.error() == ErrorTypeWithExtension::OutOfMemoryError); - throwOutOfMemoryError(globalObject, scope); - return { }; - } - RELEASE_AND_RETURN(scope, JSValue::encode(jsNontrivialString(vm, description.value()))); + RELEASE_AND_RETURN(scope, JSValue::encode(symbol->toString(globalObject))); } JSC_DEFINE_HOST_FUNCTION(symbolProtoFuncValueOf, (JSGlobalObject* globalObject, CallFrame* callFrame)) From 068029b0161f94ce1ddfedfb286109851e9a606c Mon Sep 17 00:00:00 2001 From: Fady Farag Date: Thu, 14 May 2026 18:10:54 -0700 Subject: [PATCH 059/424] [Gardening][WPE] Update outdated `imported/w3c/web-platform-tests/xhr/send-redirect-bogus.htm` name after 312947@main https://bugs.webkit.org/show_bug.cgi?id=314860 Unreviewed. * LayoutTests/platform/wpe/TestExpectations: Canonical link: https://commits.webkit.org/313285@main --- LayoutTests/platform/wpe/TestExpectations | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LayoutTests/platform/wpe/TestExpectations b/LayoutTests/platform/wpe/TestExpectations index 0b21a3b8e600..c63493ada5d7 100644 --- a/LayoutTests/platform/wpe/TestExpectations +++ b/LayoutTests/platform/wpe/TestExpectations @@ -657,7 +657,7 @@ webkit.org/b/186262 imported/w3c/web-platform-tests/css/css-text/word-break/word webkit.org/b/180645 imported/w3c/web-platform-tests/html/semantics/forms/the-input-element/valueMode.html [ Failure ] -webkit.org/b/127676 imported/w3c/web-platform-tests/xhr/send-redirect-bogus.htm [ Skip ] +webkit.org/b/127676 imported/w3c/web-platform-tests/xhr/send-redirect-bogus.sub.htm [ Skip ] webkit.org/b/127676 imported/w3c/web-platform-tests/xhr/send-redirect-to-cors.htm [ Skip ] webkit.org/b/127676 imported/w3c/web-platform-tests/xhr/send-redirect-to-non-cors.htm [ Skip ] From 4951276cc765d82514a1a3d7d6ef10e363ea7d18 Mon Sep 17 00:00:00 2001 From: Ryosuke Niwa Date: Thu, 14 May 2026 18:11:38 -0700 Subject: [PATCH 060/424] REGRESSION(310826@main): Vietnamese & Korean keyboard gets out of modeless input method in Mail Compose https://bugs.webkit.org/show_bug.cgi?id=314823 rdar://176847897 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Wenson Hsieh. InputMethodUsesCorrectKeyEventOrder (turned on by default in 310826@main, originally added in 297270@main to fix Devanagari typing on docs.google.com) defers setMarkedText: and insertText: calls from the input method's handleEventByInputMethod: callback into a queue (m_collectedKeypressCommands). The queue is replayed in the web process's keydown default handler, ensuring keydown fires before downstream events such as compositionstart and input. There was an unintended consequence from this change: while the input method is still inside its callback, it polls selectedRange to verify that its insertText: took effect. Because the queued command hasn't reached the web process yet, the poll returns the pre-keystroke cursor, which is logically stale by the inserted text length. Modeless input methods such as Vietnamese Simple Telex and 2-Set Korean interpret that as "my insertion didn't stick" and abandon the modeless path, switching back to the mode which uses setMarkedText:-based for the rest of the session in applications such as Mail and Notes. There were also two other related issues of the same root cause: - Once an input method is confident enough to keep going in modeless mode, it commits via insertText: with a non-NSNotFound replacementRange (e.g. insertText:"vi" replacementRange:(0,1) to extend 'v' into "vi"). The queue's KeypressCommand constructor ignored replacementRange, and the queue path asserted it was NSNotFound. - When the IME returned handled=YES for a purely-insertText: keystroke with no prior composition, handleEditingKeyboardEvent's event.handledByInputMethod() branch (added by 313212@main) executed the command during the keydown default handler. That dispatched a TextInput event with underlyingEvent set to keydown, which trips the assertion in EventHandler::handleTextInputEvent requiring a keypress as the underlying event instead. This PR implements three narrow changes in Source/WebKit/UIProcess/mac/WebViewImpl.mm: 1. Modeless commit routing in interpretKeyEvent's completion handler: when the input method's commands are purely insertText: (no setMarkedText:, no other) and editorState().hasComposition is false, force handled=NO. The keypress flow then runs and executeKeypressCommandsInternal fires insertText: during the Char phase with underlyingEvent=keypress — no assertion. The !hasComposition guard preserves 313212@main's Japanese Enter-commit fix: insertText:"k" with prior composition stays handled=YES, keydown gets keyCode 229, and google.com doesn't misread it as a real Enter. 2. selectedRange staging in selectedRangeWithCompletionHandler: when every queued keypress command is insertText:, add the cumulative text length to the cursor returned to the input method. The input method observes its own insertion reflected and stays in modeless mode. If any setMarkedText: is in the queue, the input method is already composing and the unstaged value is correct. 3. Replacement-range queueing in WebViewImpl::insertText: the queue path uses the existing 6-arg KeypressCommand constructor (already used by setMarkedText:) so it carries replacementRange. The IME's modeless continuation goes through the same queue as plain insertText:, executed in the web process's keydown default handler with the same event ordering. In addition, this PR implements one fix in Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm 4. replacementRange execution in executeKeypressCommandsInternal for insertText:: when the queued command has a non-NSNotFound replacementRange, set the selection to that range first via frame->selection().setSelection(VisibleSelection(replacementRange)) then call editor->insertText. This mirrors what WebPage::insertTextAsync already does for the same case. Test: TestWebKitAPI.WKWebViewMacEditingTests.KeyDownForModelessInputMethodInsertUsesRealKeyCode TestWebKitAPI.WKWebViewMacEditingTests.ModelessInputMethodInsertTextWithReplacementRangeInsertsCorrectly TestWebKitAPI.WKWebViewMacEditingTests.ModelessInputMethodEventOrderingMatchesNormalTyping * Source/WebKit/UIProcess/mac/WebViewImpl.mm: (WebKit::WebViewImpl::interpretKeyEvent): (WebKit::WebViewImpl::insertText): (WebKit::WebViewImpl::selectedRangeWithCompletionHandler): * Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm: (WebKit::WebPage::executeKeypressCommandsInternal): * Tools/Scripts/webkitpy/api_tests/allowlist.txt: * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm: (TestWebKitAPI::TEST(WKWebViewMacEditingTests, KeyDownForModelessInputMethodInsertUsesRealKeyCode)): (TestWebKitAPI::TEST(WKWebViewMacEditingTests, ModelessInputMethodInsertTextWithReplacementRangeInsertsCorrectly)): (TestWebKitAPI::TEST(WKWebViewMacEditingTests, ModelessInputMethodEventOrderingMatchesNormalTyping)): Canonical link: https://commits.webkit.org/313286@main --- Source/WebKit/UIProcess/mac/WebViewImpl.mm | 55 +++++++++- .../WebProcess/WebPage/mac/WebPageMac.mm | 9 ++ .../Scripts/webkitpy/api_tests/allowlist.txt | 6 + .../WKWebView/mac/WKWebViewMacEditingTests.mm | 103 ++++++++++++++++++ 4 files changed, 170 insertions(+), 3 deletions(-) diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.mm b/Source/WebKit/UIProcess/mac/WebViewImpl.mm index af7739e5b8df..d6fd7aa34da0 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.mm +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.mm @@ -5604,11 +5604,26 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) #endif bool hasInsertText = false; + bool hasOnlyInsertText = !commands.isEmpty(); for (auto& command : commands) { if (command.commandName == "insertText:"_s) hasInsertText = true; + else + hasOnlyInsertText = false; } + // If the input method only produced insertText: commands (no setMarkedText:, no other + // commands) AND there is no existing composition, this is a modeless insertion — e.g. + // Simple Telex or Korean Hangul typing a character that commits inline. Route it through + // the keypress flow (handled=NO) so the keydown reports the real keyCode, a keypress + // fires, and executeKeypressCommandsInternal runs the insertText: during the Char phase + // with underlyingEvent=keypress. Composition activity (setMarkedText:), and insertText: + // that commits an existing composition (e.g. Japanese Enter on a marked candidate), keep + // handled=YES so the keydown reports keyCode 229 and google.com's keyCode==13 listener + // doesn't treat it as a real Enter. + if (handled && hasOnlyInsertText && !checkedThis->m_page->editorState().hasComposition) + handled = NO; + LOG(TextInput, "... handleEventByInputMethod%s handled", handled ? "" : " not"); if (handled) { capturedBlock(YES, WTF::move(commands)); @@ -5690,8 +5705,13 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) // - If it's sent outside of keyboard event processing (e.g. from Character Viewer, or when confirming an inline input area with a mouse), // then we also execute it immediately, as there will be no other chance. if (m_collectedKeypressCommands && !m_isTextInsertionReplacingSoftSpace) { - ASSERT(replacementRange.location == NSNotFound); - WebCore::KeypressCommand command("insertText:"_s, text.get()); + // Modeless input methods (Vietnamese Simple Telex, Korean Hangul) call insertText: with a + // replacementRange during their handleEventByInputMethod: callback to commit a previously + // inserted character into a longer sequence (e.g. replace 'v' with 'vi'). Queue it with + // the replacement range preserved so executeKeypressCommandsInternal can replay it during + // the keydown default handler, after keydown has fired but before composition/input — + // matching the event ordering 297270@main set up for plain insertText:. + WebCore::KeypressCommand command("insertText:"_s, text.get(), { }, { }, { }, EditingRange { replacementRange }.toCharacterRange()); m_collectedKeypressCommands->append(command); LOG(TextInput, "...stored"); m_page->registerKeypressCommandName(command.commandName); @@ -5718,7 +5738,32 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) auto completionHandler = adoptNS([completionHandlerPtr copy]); LOG(TextInput, "selectedRange"); - m_page->getSelectedRangeAsync([completionHandler, stagedSelectedRange = m_stagedMarkedRange](const EditingRange& editingRangeResult, const EditingRange& compositionRange) { + + // When the IME calls insertText: during its handleEventByInputMethod: callback and then polls + // selectedRange to verify, the queued insertText: hasn't reached the web process yet, so the + // cursor appears not to have advanced. Modeless input methods (Vietnamese Simple Telex, + // Korean Hangul) interpret that stale cursor as "my insertion didn't stick" and fall back to + // setMarkedText: for the rest of the session. Surface the cumulative insertText: advance from + // the queue so the IME observes the cursor it expects, while leaving the actual execution + // (and 310826's event ordering) untouched. Only applies when every queued command is + // insertText:; if setMarkedText: is in the mix, the IME is already in composition mode and + // the unstaged selectedRange is the right answer. + size_t stagedInsertLength = 0; + if (m_collectedKeypressCommands && !m_collectedKeypressCommands->isEmpty()) { + bool onlyInsertText = true; + size_t total = 0; + for (auto& command : *m_collectedKeypressCommands) { + if (command.commandName != "insertText:"_s) { + onlyInsertText = false; + break; + } + total += command.text.length(); + } + if (onlyInsertText) + stagedInsertLength = total; + } + + m_page->getSelectedRangeAsync([completionHandler, stagedSelectedRange = m_stagedMarkedRange, stagedInsertLength](const EditingRange& editingRangeResult, const EditingRange& compositionRange) { void (^completionHandlerBlock)(NSRange) = (void (^)(NSRange))completionHandler.get(); if (stagedSelectedRange) { @@ -5727,6 +5772,10 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) } NSRange result = editingRangeResult; + if (stagedInsertLength && result.location != NSNotFound) { + result.location += stagedInsertLength; + result.length = 0; + } if (result.location == NSNotFound) LOG(TextInput, " -> selectedRange returned (NSNotFound, %zu)", result.length); else diff --git a/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm b/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm index 73e0b41bb755..b649719cf889 100644 --- a/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm +++ b/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm @@ -294,6 +294,15 @@ static String commandNameForSelectorName(const String& selectorName) if (!editor->canEdit()) continue; + // Modeless input methods (Vietnamese Simple Telex, Korean Hangul) call insertText: + // with a replacementRange to commit a previously-inserted character into a longer + // sequence (e.g. replace 'v' with 'vi'). Set the selection to the replacement range + // first so editor->insertText replaces it, mirroring insertTextAsync. + if (currentCommand.replacementRange.location != WTF::notFound) { + if (auto replacementSimpleRange = EditingRange::toRange(*frame, EditingRange { currentCommand.replacementRange })) + protect(frame->selection())->setSelection(VisibleSelection(*replacementSimpleRange)); + } + // An insertText: might be handled by other responders in the chain if we don't handle it. // One example is space bar that results in scrolling down the page. eventWasHandled |= editor->insertText(currentCommand.text, event); diff --git a/Tools/Scripts/webkitpy/api_tests/allowlist.txt b/Tools/Scripts/webkitpy/api_tests/allowlist.txt index e46236d3d34a..caa9fada2faa 100644 --- a/Tools/Scripts/webkitpy/api_tests/allowlist.txt +++ b/Tools/Scripts/webkitpy/api_tests/allowlist.txt @@ -3302,6 +3302,12 @@ TestWebKitAPI.WKWebViewMacEditingTests.FirstRectForCharacterRangeInTextArea TestWebKitAPI.WKWebViewMacEditingTests.FirstRectForCharacterRangeWithNewlinesAndWrapping TestWebKitAPI.WKWebViewMacEditingTests.FirstRectForCharacterRangeWithNewlinesAndWrappingLineBreakAfterWhiteSpace TestWebKitAPI.WKWebViewMacEditingTests.InlinePredictionsShouldSuppressAutocorrection +TestWebKitAPI.WKWebViewMacEditingTests.KeyDownFiresBeforeCompositionEvent +TestWebKitAPI.WKWebViewMacEditingTests.KeyDownForInputMethodCommitUsesCompositionKeyCode +TestWebKitAPI.WKWebViewMacEditingTests.KeyDownForModelessInputMethodInsertUsesRealKeyCode +TestWebKitAPI.WKWebViewMacEditingTests.KeyDownInsertAccentedCharacterOnce +TestWebKitAPI.WKWebViewMacEditingTests.ModelessInputMethodEventOrderingMatchesNormalTyping +TestWebKitAPI.WKWebViewMacEditingTests.ModelessInputMethodInsertTextWithReplacementRangeInsertsCorrectly TestWebKitAPI.WKWebViewMacEditingTests.ProcessSwapAfterSettingMarkedText TestWebKitAPI.WKWebViewMacEditingTests.SetMarkedTextWithNoAttributedString TestWebKitAPI.WKWebViewUnderPageBackgroundColor.KVO diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm index 4b97922ea04e..9bb09a6814a9 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm @@ -371,6 +371,109 @@ - (NSRange)range EXPECT_STREQ("229,229", [webView stringByEvaluatingJavaScript:@"window.keyCodes.join(',')"].UTF8String); } +TEST(WKWebViewMacEditingTests, KeyDownForModelessInputMethodInsertUsesRealKeyCode) +{ + RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); + for (_WKFeature *feature in WKPreferences._features) { + NSString *key = feature.key; + if ([key isEqualToString:@"InputMethodUsesCorrectKeyEventOrder"]) + [[configuration preferences] _setEnabled:YES forFeature:feature]; + } + + RetainPtr webView = adoptNS([[TestWebViewWithMockTextInputContext alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]); + // Modeless IME: calls insertText: directly (no setMarkedText:) with no prior composition. + // This is Vietnamese Simple Telex / Korean Hangul typing a character that commits inline. + [webView _web_superInputContext].actions = [@[ + [[[MockTextInputContextAction alloc] initWithInsertText:@"v" replacementRange:NSMakeRange(NSNotFound, 0)] autorelease], + ].mutableCopy autorelease]; + [webView synchronouslyLoadHTMLString:@""]; + // The keydown should report the real keyCode (not 229), a keypress should fire, and the + // text insertion should happen during the Char phase with underlyingEvent=keypress. Without + // the modeless routing, the keydown would be reported as handled-by-input-method, keyCode + // would be 229, no keypress would fire, and the Char-phase TextInput event would trip the + // underlyingEvent=keydown assertion in EventHandler::handleTextInputEvent. + [webView stringByEvaluatingJavaScript:@"window.events = [];" + "const q = document.getElementById('q');" + "q.addEventListener('keydown', (event) => window.events.push('keydown:' + event.keyCode), true);" + "q.addEventListener('keypress', (event) => window.events.push('keypress:' + event.keyCode), true);" + "q.focus();"]; + [webView waitForNextPresentationUpdate]; + [webView removeFromSuperview]; + [webView typeCharacter:'v']; + Util::runFor(1_s); + + EXPECT_STREQ("v", [webView stringByEvaluatingJavaScript:@"document.getElementById('q').value"].UTF8String); + // keydown reports 'V' (86); keypress reports 'v' (118). + EXPECT_STREQ("keydown:86,keypress:118", [webView stringByEvaluatingJavaScript:@"window.events.join(',')"].UTF8String); +} + +TEST(WKWebViewMacEditingTests, ModelessInputMethodInsertTextWithReplacementRangeInsertsCorrectly) +{ + RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); + for (_WKFeature *feature in WKPreferences._features) { + NSString *key = feature.key; + if ([key isEqualToString:@"InputMethodUsesCorrectKeyEventOrder"]) + [[configuration preferences] _setEnabled:YES forFeature:feature]; + } + + RetainPtr webView = adoptNS([[TestWebViewWithMockTextInputContext alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]); + // Modeless IME continuation: first keystroke inserts 'v', second keystroke extends the + // syllable by inserting "vi" with replacementRange=(0,1) to replace the prior 'v'. The + // replacement-range path must not trip the queue's assertion and must end up with "vi". + [webView _web_superInputContext].actions = [@[ + [[[MockTextInputContextAction alloc] initWithInsertText:@"v" replacementRange:NSMakeRange(NSNotFound, 0)] autorelease], + [[[MockTextInputContextAction alloc] initWithInsertText:@"vi" replacementRange:NSMakeRange(0, 1)] autorelease], + ].mutableCopy autorelease]; + [webView synchronouslyLoadHTMLString:@""]; + [webView stringByEvaluatingJavaScript:@"document.getElementById('q').focus();"]; + [webView waitForNextPresentationUpdate]; + [webView removeFromSuperview]; + [webView typeCharacter:'v']; + Util::runFor(1_s); + [webView typeCharacter:'i']; + Util::runFor(1_s); + + EXPECT_STREQ("vi", [webView stringByEvaluatingJavaScript:@"document.getElementById('q').value"].UTF8String); +} + +TEST(WKWebViewMacEditingTests, ModelessInputMethodEventOrderingMatchesNormalTyping) +{ + RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); + for (_WKFeature *feature in WKPreferences._features) { + NSString *key = feature.key; + if ([key isEqualToString:@"InputMethodUsesCorrectKeyEventOrder"]) + [[configuration preferences] _setEnabled:YES forFeature:feature]; + } + + RetainPtr webView = adoptNS([[TestWebViewWithMockTextInputContext alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]); + // Two modeless keystrokes: 'v' as a plain insertText:, then 'i' as a replacement-range + // insertText: that extends the syllable to "vi" (mirroring Vietnamese Simple Telex). Both + // should produce the same JS event ordering as plain typing — keydown, keypress, then input + // from the queued execution during the keydown default handler in the web process. No + // composition events should fire (no setMarkedText: was called). 297270@main's keydown-first + // ordering must hold for both the plain and replacement-range insertText: paths. + [webView _web_superInputContext].actions = [@[ + [[[MockTextInputContextAction alloc] initWithInsertText:@"v" replacementRange:NSMakeRange(NSNotFound, 0)] autorelease], + [[[MockTextInputContextAction alloc] initWithInsertText:@"vi" replacementRange:NSMakeRange(0, 1)] autorelease], + ].mutableCopy autorelease]; + [webView synchronouslyLoadHTMLString:@""]; + [webView stringByEvaluatingJavaScript:@"window.events = [];" + "['keydown', 'keypress', 'keyup', 'input', 'compositionstart', 'compositionupdate', 'compositionend']" + ".forEach((type) => { document.body.addEventListener(type, (event) => window.events.push(type), true); });" + "document.body.focus();"]; + [webView waitForNextPresentationUpdate]; + [webView removeFromSuperview]; + [webView typeCharacter:'v']; + Util::runFor(1_s); + [webView typeCharacter:'i']; + Util::runFor(1_s); + + EXPECT_STREQ("vi", [webView stringByEvaluatingJavaScript:@"document.body.textContent"].UTF8String); + // Per keystroke: keydown -> keypress -> input -> keyup. No composition events for modeless. + EXPECT_STREQ("keydown,keypress,input,keyup,keydown,keypress,input,keyup", + [webView stringByEvaluatingJavaScript:@"window.events.join(',')"].UTF8String); +} + TEST(WKWebViewMacEditingTests, DoNotCrashWhenInterpretingKeyEventWhileDeallocatingView) { __block bool isDone = false; From c71210975aaf236bf29b61320c1a77a49882f2d2 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Thu, 14 May 2026 18:37:43 -0700 Subject: [PATCH 061/424] [JSC] Avoid creating both first resolving functions when only one is needed in `Promise.all` / `Promise.any` https://bugs.webkit.org/show_bug.cgi?id=314813 Reviewed by Yusuke Suzuki. Promise.all's slow path (custom thenables that go through `then`) lazily creates a shared onRejected via createFirstResolvingFunctions(), but discards the resolve function it returns. Likewise, Promise.any's slow path only needs the resolve function and discards the reject function. The two "first resolving" functions are independent: unlike createResolvingFunctions(), they don't cross-reference each other via JSFunctionWithFields::Field::ResolvingOther. The "already resolved" state is tracked by isFirstResolvingFunctionCalledFlag on the JSPromise itself. So creating just one of them is safe. Split createFirstResolvingFunctions() into createFirstResolveFunction() and createFirstRejectFunction(), and have Promise.all / Promise.any call only the one they need, saving one JSFunctionWithFields allocation per slow-path entry. Promise.race and newPromiseCapability still need both, so they continue to use the combined helper, which now delegates to the singular ones. * Source/JavaScriptCore/runtime/JSPromise.cpp: (JSC::JSPromise::createFirstResolveFunction): (JSC::JSPromise::createFirstRejectFunction): * Source/JavaScriptCore/runtime/JSPromise.h: * Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): Canonical link: https://commits.webkit.org/313287@main --- Source/JavaScriptCore/runtime/JSPromise.cpp | 16 ++++++++++++---- Source/JavaScriptCore/runtime/JSPromise.h | 2 ++ .../runtime/JSPromiseConstructor.cpp | 12 ++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index 55b3662861c6..c74d5bffed1c 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -687,15 +687,23 @@ std::tuple JSPromise::createResolvingFunctions(VM& vm, return std::tuple { resolve, reject }; } -std::tuple JSPromise::createFirstResolvingFunctions(VM& vm, JSGlobalObject* globalObject) +JSFunction* JSPromise::createFirstResolveFunction(VM& vm, JSGlobalObject* globalObject) { auto* resolve = JSFunctionWithFields::create(vm, globalObject, vm.promiseFirstResolvingFunctionResolveExecutable(), 1, nullString()); - auto* reject = JSFunctionWithFields::create(vm, globalObject, vm.promiseFirstResolvingFunctionRejectExecutable(), 1, nullString()); - resolve->setField(vm, JSFunctionWithFields::Field::FirstResolvingPromise, this); + return resolve; +} + +JSFunction* JSPromise::createFirstRejectFunction(VM& vm, JSGlobalObject* globalObject) +{ + auto* reject = JSFunctionWithFields::create(vm, globalObject, vm.promiseFirstResolvingFunctionRejectExecutable(), 1, nullString()); reject->setField(vm, JSFunctionWithFields::Field::FirstResolvingPromise, this); + return reject; +} - return std::tuple { resolve, reject }; +std::tuple JSPromise::createFirstResolvingFunctions(VM& vm, JSGlobalObject* globalObject) +{ + return std::tuple { createFirstResolveFunction(vm, globalObject), createFirstRejectFunction(vm, globalObject) }; } std::tuple JSPromise::createResolvingFunctionsWithInternalMicrotask(VM& vm, JSGlobalObject* globalObject, InternalMicrotask task, JSValue context) diff --git a/Source/JavaScriptCore/runtime/JSPromise.h b/Source/JavaScriptCore/runtime/JSPromise.h index 60dcdac7c099..2d17676d7d92 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.h +++ b/Source/JavaScriptCore/runtime/JSPromise.h @@ -161,6 +161,8 @@ class JSPromise : public JSNonFinalObject { std::tuple createResolvingFunctions(VM&, JSGlobalObject*); std::tuple createFirstResolvingFunctions(VM&, JSGlobalObject*); + JSFunction* createFirstResolveFunction(VM&, JSGlobalObject*); + JSFunction* createFirstRejectFunction(VM&, JSGlobalObject*); static std::tuple createResolvingFunctionsWithInternalMicrotask(VM&, JSGlobalObject*, InternalMicrotask, JSValue context); static std::tuple newPromiseCapability(JSGlobalObject*, JSValue constructor); static JSValue createPromiseCapability(VM&, JSGlobalObject*, JSObject* promise, JSObject* resolve, JSObject* reject); diff --git a/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp b/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp index 3f0414fae6e7..9b0575c1c31b 100644 --- a/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp +++ b/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp @@ -499,10 +499,8 @@ JSC_DEFINE_HOST_FUNCTION(promiseConstructorFuncAll, (JSGlobalObject* globalObjec } } - if (!onRejected) { - auto [resolve, reject] = promise->createFirstResolvingFunctions(vm, globalObject); - onRejected = reject; - } + if (!onRejected) + onRejected = promise->createFirstRejectFunction(vm, globalObject); JSValue then = nextPromise->get(globalObject, vm.propertyNames->then); RETURN_IF_EXCEPTION(scope, void()); CallData thenCallData = getCallDataInline(then); @@ -1299,10 +1297,8 @@ JSC_DEFINE_HOST_FUNCTION(promiseConstructorFuncAny, (JSGlobalObject* globalObjec } // For Promise.any, onFulfilled just resolves the main promise directly - if (!resolve) { - auto [onFulfilled, onRejected] = promise->createFirstResolvingFunctions(vm, globalObject); - resolve = onFulfilled; - } + if (!resolve) + resolve = promise->createFirstResolveFunction(vm, globalObject); auto* onRejected = JSFunctionWithFields::create(vm, globalObject, vm.promiseAnyRejectFunctionExecutable(), 1, emptyString()); onRejected->setField(vm, JSFunctionWithFields::Field::PromiseAnyContext, context); From d59fcec2dc3b52bc4ee17efb43945f604e9cbcd0 Mon Sep 17 00:00:00 2001 From: Abrar Rahman Protyasha Date: Thu, 14 May 2026 19:17:54 -0700 Subject: [PATCH 062/424] [AppKit Gestures] Triple click does not generate a line selection on a PDF file https://bugs.webkit.org/show_bug.cgi?id=314804 rdar://177052413 Reviewed by Aditya Keerthi and Richard Robinson. Right now, a triple click action leads to a .ParagraphGranularity selection request from WebCore, but we fail to respect that and fallback to a character granularity selection. Triple clicks, however, are used to produce line selections for PDF content elsewhere in the system, namely in Preview.app. Since PDFKit performs this same decomposition from NSTextSelectionGranularityParagraph to PDFSelectionGranularityLine for this purpose, we just follow suit in this patch. Test: AppKitGestureTests.tripleClickingInPDFSelectsLine UnifiedPDF.TripleClickSelectsLineInPDF * Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm: (WebKit::UnifiedPDFPlugin::documentEditingContext const): * Tools/TestWebKitAPI/Helpers/cocoa/WebPage+Extras.swift: (copySelection): * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UnifiedPDFTests.mm: (TestWebKitAPI::UNIFIED_PDF_TEST): * Tools/TestWebKitAPI/Tests/WebKit/WebPage/AppKitGesturesTests.swift: (AppKitGesturesTests.tripleClickingInPDFSelectsLine): Canonical link: https://commits.webkit.org/313288@main --- .../PDF/UnifiedPDF/UnifiedPDFPlugin.mm | 1 + .../Helpers/cocoa/WebPage+Extras.swift | 8 ++++ .../Tests/WebKit/WKWebView/UnifiedPDFTests.mm | 21 ++++++++++ .../WebKit/WebPage/AppKitGesturesTests.swift | 42 +++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm b/Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm index ed2201ff76a4..96b81cdc4960 100644 --- a/Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm +++ b/Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm @@ -4740,6 +4740,7 @@ static bool isEmpty(PDFSelection *selection) case TextGranularity::WordGranularity: return PDFSelectionGranularityWord; case TextGranularity::LineGranularity: + case TextGranularity::ParagraphGranularity: return PDFSelectionGranularityLine; default: ASSERT_NOT_REACHED(); diff --git a/Tools/TestWebKitAPI/Helpers/cocoa/WebPage+Extras.swift b/Tools/TestWebKitAPI/Helpers/cocoa/WebPage+Extras.swift index de11af55625e..6c77933633a4 100644 --- a/Tools/TestWebKitAPI/Helpers/cocoa/WebPage+Extras.swift +++ b/Tools/TestWebKitAPI/Helpers/cocoa/WebPage+Extras.swift @@ -164,6 +164,14 @@ extension WebPage { } } + /// Copies the current selection to the system pasteboard and returns its string representation. + public func copySelection() async -> String? { + NSPasteboard.general.clearContents() + NSApp.sendAction(#selector(NSText.copy(_:)), to: backingWebView, from: nil) + await waitForNextPresentationUpdate() + return NSPasteboard.general.string(forType: .string) + } + private func mouseEvent( _ type: NSEvent.EventType, at location: NSPoint, diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UnifiedPDFTests.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UnifiedPDFTests.mm index 7792387c69e5..0f90e1fea7e8 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UnifiedPDFTests.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UnifiedPDFTests.mm @@ -865,6 +865,27 @@ static void runSelectAllAndVerifySelectedTextMatches(NSString *expected, void (^ }); } +UNIFIED_PDF_TEST(TripleClickSelectsLineInPDF) +{ + RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 600, 600) configuration:configurationForWebViewTestingUnifiedPDF().get()]); + [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSBundle.test_resourcesBundle URLForResource:@"test" withExtension:@"pdf"]]]; + [webView waitForNextPresentationUpdate]; + + [[webView window] makeFirstResponder:webView.get()]; + [[webView window] makeKeyAndOrderFront:nil]; + [[webView window] orderFrontRegardless]; + + [[NSPasteboard generalPasteboard] clearContents]; + + [webView sendClicksAtPoint:NSMakePoint(100, 500) numberOfClicks:3]; + [webView waitForPendingMouseEvents]; + [webView waitForNextPresentationUpdate]; + [webView copy:nil]; + [webView waitForNextPresentationUpdate]; + + EXPECT_WK_STREQ("Test PDF Content", [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); +} + #endif // PLATFORM(MAC) #if PLATFORM(IOS_FAMILY) diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WebPage/AppKitGesturesTests.swift b/Tools/TestWebKitAPI/Tests/WebKit/WebPage/AppKitGesturesTests.swift index 16626fed9e7d..159f5b141aa3 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WebPage/AppKitGesturesTests.swift +++ b/Tools/TestWebKitAPI/Tests/WebKit/WebPage/AppKitGesturesTests.swift @@ -199,6 +199,48 @@ struct AppKitGesturesTests { #expect(newSelection == crazySelection) } + @Test( + .bug("https://webkit.org/b/314804", "Triple click does not generate a line selection on PDF") + ) + func tripleClickingInPDFSelectsLine() async throws { + let pdfURL = try #require(Bundle.testResources.url(forResource: "test", withExtension: "pdf")) + try await page.load(URLRequest(url: pdfURL)).wait() + await page.waitForNextPresentationUpdate() + + // Recap requires this test to be ran within an app host. + guard NSApp.isActive else { + return + } + + let pointInDOMCoords = try #require( + DOMRect(decodedRepresentation: [ + "x": 100.0, + "y": 100.0, + "width": 0.0, + "height": 0.0, + ]) + ) + let clickPoint = convertToCoreGraphicsScreenCoordinates( + rectInViewportCoordinates: pointInDOMCoords, + window: window + ) + .origin + + await recap.play { composer in + composer._wk_click(at: clickPoint, for: .seconds(0.1)) + composer.advanceTime(0.1) + composer._wk_click(at: clickPoint, for: .seconds(0.1)) + composer.advanceTime(0.1) + composer._wk_click(at: clickPoint, for: .seconds(0.1)) + } + + await page.waitForPendingMouseEvents() + await page.waitForNextPresentationUpdate() + + let selectedText = await page.copySelection() + #expect(selectedText == "Test PDF Content") + } + @Test(arguments: [0.1, 0.5, 0.9]) func clickingInWordChangesSelection(fractionOfWordToClick: Double) async throws { try await loadHTML(contentEditable: true) From 4d1d47e3352540f732bb2f163c7f9d9b28d23e36 Mon Sep 17 00:00:00 2001 From: Per Arne Vollan Date: Thu, 14 May 2026 19:24:30 -0700 Subject: [PATCH 063/424] Registering for notifications are triggering fault logs in the WebContent process https://bugs.webkit.org/show_bug.cgi?id=314849 rdar://177093518 Reviewed by Ben Nham. We need to add these notifications to the WebContent notification entitlement list to avoid these fault logs. * Source/WebKit/Resources/cocoa/NotificationAllowList/NonForwardedNotifications.def: Canonical link: https://commits.webkit.org/313289@main --- .../NotificationAllowList/NonForwardedNotifications.def | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/WebKit/Resources/cocoa/NotificationAllowList/NonForwardedNotifications.def b/Source/WebKit/Resources/cocoa/NotificationAllowList/NonForwardedNotifications.def index 035df2f6bb39..3cb1d1227871 100644 --- a/Source/WebKit/Resources/cocoa/NotificationAllowList/NonForwardedNotifications.def +++ b/Source/WebKit/Resources/cocoa/NotificationAllowList/NonForwardedNotifications.def @@ -1,6 +1,7 @@ WK_NOTIFICATION_COMMENT("WebContent registers for these notifications but they are only posted in-process.") WK_NOTIFICATION_COMMENT("FIXME: Remove AX cache notifications once wildcard syntax is supported everywhere. See rdar://130683194.") +WK_NOTIFICATION("com.apple.CFNetwork.CookiesChanged.*") WK_NOTIFICATION("com.apple.accessibility.cache.app.ax") WK_NOTIFICATION("com.apple.accessibility.cache.ast") WK_NOTIFICATION("com.apple.accessibility.cache.automation.localized.lookup") @@ -28,6 +29,8 @@ WK_NOTIFICATION("com.apple.accessibility.cache.switch.control") WK_NOTIFICATION("com.apple.accessibility.cache.vot") WK_NOTIFICATION("com.apple.accessibility.cache.zoom") WK_NOTIFICATION("com.apple.accessibility.cache.*") -WK_NOTIFICATION("com.apple.system.DirectoryService.InvalidateCache*") WK_NOTIFICATION("com.apple.coreservices.launchservices.session.*") +WK_NOTIFICATION("com.apple.devicemanagementclient.skipKeyChanged") +WK_NOTIFICATION("com.apple.neconfigurationchanged") +WK_NOTIFICATION("com.apple.system.DirectoryService.InvalidateCache*") WK_NOTIFICATION("user.uid.501.syslog.*") From edd7a9128891559641d803177f9cc9a2390b30af Mon Sep 17 00:00:00 2001 From: Sammy Gill Date: Thu, 14 May 2026 19:33:50 -0700 Subject: [PATCH 064/424] Convert flexbox-min-height-auto-{003,004}.html from reftests to testharness.js https://bugs.webkit.org/show_bug.cgi?id=314824 rdar://problem/177078782 Reviewed by Alan Baradlay. These tests are checking the behavior of min-height: auto on flex items in a column flex layout. When a flex item's computed overflow is a scrollable value then that item has no minimum height and min-height acts as having a value of 0. For the cases that exercise overflow-y:scroll/auto (directly in 003, indirectly via overflow-x in 004), this only matched when the implementation reserved scrollbar gutter the same way for both flex items and floats. That coupling made the tests sensitive to scrollbar handling that is not what the section is actually trying to verify, and failures could occur for reasons unrelated to min-height. Convert each test to a testharness.js test that asserts the relevant geometry directly: - offsetHeight on the flex item proves the spec assertion: the visible case keeps the 84px intrinsic border-box height; the non-visible cases shrink to the 30px container. - offsetWidth on the flex item proves the scroll container correctly grew its border-box to accommodate the vertical scrollbar gutter, which is exactly the behavior whose absence would have produced a spurious horizontal scrollbar in the original reftests. - scrollWidth === clientWidth on the scrolling cases confirms no horizontal overflow snuck in. One could argue that we do not need to have the second point for the purposes of this test, but I decided to keep them in to limit the number of changes in the test. Canonical link: https://commits.webkit.org/313290@main --- .../flexbox-min-height-auto-003-expected.html | 41 ------------- .../flexbox-min-height-auto-003-expected.txt | 6 ++ .../flexbox-min-height-auto-003-ref.html | 41 ------------- .../flexbox-min-height-auto-003.html | 57 ++++++++++++++++-- .../flexbox-min-height-auto-004-expected.html | 41 ------------- .../flexbox-min-height-auto-004-expected.txt | 6 ++ .../flexbox-min-height-auto-004-ref.html | 41 ------------- .../flexbox-min-height-auto-004.html | 58 +++++++++++++++++-- 8 files changed, 117 insertions(+), 174 deletions(-) delete mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.txt delete mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-ref.html delete mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-expected.txt delete mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-ref.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.html deleted file mode 100644 index ff1ced0c0351..000000000000 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - CSS Reftest Reference - - - - -
-
-
-
- - diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.txt new file mode 100644 index 000000000000..b20942093fac --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-expected.txt @@ -0,0 +1,6 @@ + +PASS overflow-y:visible: min-height:auto holds; width has no scrollbar gutter +PASS overflow-y:hidden: min-height:auto resolves to 0; no scrollbar gutter +PASS overflow-y:scroll: min-height:auto resolves to 0; width includes scrollbar gutter +FAIL overflow-y:auto: min-height:auto resolves to 0; width includes scrollbar gutter assert_equals: offsetWidth expected 59 but got 44 + diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-ref.html deleted file mode 100644 index ff1ced0c0351..000000000000 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003-ref.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - CSS Reftest Reference - - - - -
-
-
-
- - diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003.html index 15ef35d9cd7f..5177ed062b57 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003.html +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-003.html @@ -9,7 +9,6 @@ CSS Test: Testing min-height:auto & 'overflow' interaction - + + - - -
-
-
-
- - diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-expected.txt new file mode 100644 index 000000000000..c5d84d2f8d9e --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-expected.txt @@ -0,0 +1,6 @@ + +PASS overflow-x:visible: overflow-y stays visible; min-height:auto holds; no scrollbar gutter +FAIL overflow-x:hidden: forces overflow-y to auto; min-height:auto resolves to 0; width includes scrollbar gutter assert_equals: offsetWidth expected 59 but got 44 +FAIL overflow-x:scroll: forces overflow-y to auto; min-height:auto resolves to 0; width includes scrollbar gutter assert_equals: offsetWidth expected 59 but got 44 +FAIL overflow-x:auto: forces overflow-y to auto; min-height:auto resolves to 0; width includes scrollbar gutter assert_equals: offsetWidth expected 59 but got 44 + diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-ref.html deleted file mode 100644 index 768185d43194..000000000000 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004-ref.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - CSS Reftest Reference - - - - -
-
-
-
- - diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004.html index a04b9b573e2b..bcc6adcbac25 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004.html +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-flexbox/flexbox-min-height-auto-004.html @@ -9,7 +9,6 @@ CSS Test: Testing min-height:auto & 'overflow' interaction - + + + + + + +
Check out this link.
+ + diff --git a/Source/WebCore/page/text-extraction/TextExtraction.cpp b/Source/WebCore/page/text-extraction/TextExtraction.cpp index 90a2e8b11736..ac6811dacecb 100644 --- a/Source/WebCore/page/text-extraction/TextExtraction.cpp +++ b/Source/WebCore/page/text-extraction/TextExtraction.cpp @@ -532,6 +532,17 @@ static bool isInDisabledFormControl(Node& node) return control && control->isDisabledFormControl(); } +static String normalizedLabelText(const Element& element) +{ + for (auto attribute : { HTMLNames::aria_labelAttr.get(), HTMLNames::labelAttr.get() }) { + auto text = normalizeText(element.attributeWithoutSynchronization(attribute)); + if (!text.isEmpty()) + return text; + } + + return { }; +} + enum class SkipExtraction : bool { Self, SelfAndSubtree @@ -614,13 +625,21 @@ static inline Variant extractItemData(N return { SkipExtraction::Self }; } + bool focused = protect(element->document())->activeElement() == element; + if (!element->isInUserAgentShadowTree() && element->isRootEditableElement()) { - if (context.mergeParagraphs) - return { Editable { } }; + if (context.mergeParagraphs) { + return { Editable { + .label = normalizedLabelText(*element), + .placeholder = { }, + .isSecure = false, + .isFocused = focused, + } }; + } return { ContentEditableData { .isPlainTextOnly = !element->hasRichlyEditableStyle(), - .isFocused = protect(element->document())->activeElement() == element, + .isFocused = focused, } }; } @@ -2278,17 +2297,6 @@ void handleInteraction(Interaction&& interaction, LocalFrame& frame, CompletionH }); } -static String normalizedLabelText(const Element& element) -{ - for (auto attribute : { HTMLNames::aria_labelAttr.get(), HTMLNames::labelAttr.get() }) { - auto text = normalizeText(element.attributeWithoutSynchronization(attribute)); - if (!text.isEmpty()) - return text; - } - - return { }; -} - static String wrapWithDoubleQuotes(StringView text) { return makeString(u"“", text, u"”"); From ecec9b6009d9e06e0958c87e6304a6fc98f263e4 Mon Sep 17 00:00:00 2001 From: Philippe Normand Date: Fri, 15 May 2026 06:57:22 -0700 Subject: [PATCH 083/424] REGRESSION(311577@main): [GTK][GStreamer][Rice] WebKitNetworkProcess is using massive amounts of CPU and spanning several threads named webrtc-rice-XX https://bugs.webkit.org/show_bug.cgi?id=313710 Reviewed by Xabier Rodriguez-Calvar. Un-register TCP and UDP sockets from the librice I/O registry when destroying the backend. The recv GSource was also prone to infinite looping due to missing RICE_IO_RECV_CLOSED notifications from rice-io and infinite blocking on rice_sockets_recv() due to lack of configured timeout on the sockets. * Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.cpp: (WebKit::RiceBackend::~RiceBackend): (WebKit::RiceBackend::finalizeStream): (WebKit::RiceBackend::allocateSocket): (WebKit::RiceBackend::removeSocket): * Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.h: Canonical link: https://commits.webkit.org/313309@main --- .../webrtc/rice/RiceBackend.cpp | 126 ++++++++++++------ .../NetworkProcess/webrtc/rice/RiceBackend.h | 6 +- Tools/Scripts/webkitpy/port/glib.py | 4 +- 3 files changed, 92 insertions(+), 44 deletions(-) diff --git a/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.cpp b/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.cpp index e3279d4f0bcf..ce2b880f8301 100644 --- a/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.cpp +++ b/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.cpp @@ -148,9 +148,15 @@ RiceBackend::RiceBackend(NetworkConnectionToWebProcess& connection) RiceBackend::~RiceBackend() { - for (auto& socketData : m_sockets.values()) { + auto streamIds = copyToVector(m_udpAddresses.keys()); + for (auto streamId : streamIds) + finalizeStream(streamId); + + Locker locker { m_socketsLock }; + while (!m_sockets.isEmpty()) { + auto socketData = m_sockets.takeFirst(); GRefPtr source = socketData.source; - if (!source) [[unlikely]] + if (!source) continue; g_source_destroy(source.get()); @@ -177,11 +183,13 @@ std::optional RiceBackend::sharedPreferencesForW GRefPtr RiceBackend::getSocketsForStream(unsigned streamId) { + Locker locker { m_socketsLock }; return m_sockets.get(streamId).sockets; } GRefPtr RiceBackend::getRecvSourceForStream(unsigned streamId) { + Locker locker { m_socketsLock }; return m_sockets.get(streamId).source; } @@ -301,9 +309,27 @@ void RiceBackend::finalizeStream(unsigned streamId) return true; }); - auto data = m_sockets.take(streamId); - if (data.source) - g_source_destroy(data.source.get()); + + m_tcpAddresses.removeIf([&](auto& keyValue) -> bool { + auto& [key, addresses] = keyValue; + if (key != streamId) + return false; + + auto riceSockets = getSocketsForStream(streamId); + for (auto& [localAddressString, remoteAddressString] : addresses) { + GUniquePtr localAddress(riceAddressFromString(localAddressString)); + GUniquePtr remoteAddress(riceAddressFromString(remoteAddressString)); + rice_sockets_remove_tcp(riceSockets.get(), localAddress.get(), remoteAddress.get()); + } + return true; + }); + + { + Locker locker { m_socketsLock }; + auto data = m_sockets.take(streamId); + if (data.source) + g_source_destroy(data.source.get()); + } } void RiceBackend::gatherSocketAddresses(ScriptExecutionContextIdentifier identifier, unsigned streamId, unsigned minRtpPort, unsigned maxRtpPort, GatherSocketAddressesCallback&& completionHandler) @@ -363,7 +389,7 @@ void RiceBackend::gatherSocketAddresses(ScriptExecutionContextIdentifier identif return; auto sockets = backend->getSocketsForStream(recvData->streamId); rice_sockets_add_tcp(sockets.get(), socket); - backend->configureSocketBufferSizes(); + backend->configureSockets(); }, recvData, reinterpret_cast(destroyRecvSourceData))); GUniquePtr localAddress(rice_tcp_listener_local_addr(tcpListener.get())); @@ -381,7 +407,7 @@ void RiceBackend::gatherSocketAddresses(ScriptExecutionContextIdentifier identif return; auto sockets = backend->getSocketsForStream(recvData->streamId); rice_sockets_add_tcp(sockets.get(), socket); - backend->configureSocketBufferSizes(); + backend->configureSockets(); }, recvData, reinterpret_cast(destroyRecvSourceData))); GUniquePtr tcpListenerLocalAddress(rice_tcp_listener_local_addr(tcpListener.get())); @@ -397,9 +423,6 @@ void RiceBackend::gatherSocketAddresses(ScriptExecutionContextIdentifier identif recvData->streamId = streamId; g_source_set_callback(source.get(), static_cast([](auto userData) -> gboolean { - RiceIoRecv recv; - std::array data; - auto sourceData = reinterpret_cast(userData); RefPtr backend = sourceData->backend.get(); if (!backend) @@ -407,36 +430,38 @@ void RiceBackend::gatherSocketAddresses(ScriptExecutionContextIdentifier identif auto sockets = backend->getSocketsForStream(sourceData->streamId); if (!sockets) - return G_SOURCE_CONTINUE; - - while (true) { - rice_sockets_recv(sockets.get(), data.data(), data.size(), &recv); - switch (recv.tag) { - case RICE_IO_RECV_WOULD_BLOCK: - rice_recv_clear(&recv); - return G_SOURCE_CONTINUE; - case RICE_IO_RECV_DATA: { - auto from = riceAddressToString(recv.data.from); - auto to = riceAddressToString(recv.data.to); - auto protocol = toRTCIceProtocol(recv.data.transport); - auto handle = SharedMemoryHandle::createCopy(std::span { data }.first(recv.data.len), SharedMemoryProtection::ReadOnly); - if (!handle) [[unlikely]] - break; - backend->notifyIncomingData(sourceData->streamId, protocol, WTF::move(from), WTF::move(to), WTF::move(*handle)); - break; - } - case RICE_IO_RECV_CLOSED: - // This enum value is currently unused in rice-io. + return G_SOURCE_REMOVE; + + bool isClosed = false; + RiceIoRecv recv; + std::array data; + rice_sockets_recv(sockets.get(), data.data(), data.size(), &recv); + switch (recv.tag) { + case RICE_IO_RECV_WOULD_BLOCK: + break; + case RICE_IO_RECV_DATA: { + auto from = riceAddressToString(recv.data.from); + auto to = riceAddressToString(recv.data.to); + auto protocol = toRTCIceProtocol(recv.data.transport); + auto handle = SharedMemoryHandle::createCopy(std::span { data }.first(recv.data.len), SharedMemoryProtection::ReadOnly); + if (!handle) [[unlikely]] break; - } - rice_recv_clear(&recv); + backend->notifyIncomingData(sourceData->streamId, protocol, WTF::move(from), WTF::move(to), WTF::move(*handle)); + break; } - - return G_SOURCE_CONTINUE; + case RICE_IO_RECV_CLOSED: + isClosed = true; + break; + } + rice_recv_clear(&recv); + return isClosed ? G_SOURCE_REMOVE : G_SOURCE_CONTINUE; }), recvData, reinterpret_cast(destroyRecvSourceData)); g_source_attach(source.get(), m_runLoop->mainContext()); - m_sockets.add(streamId, SocketData { WTF::move(sockets), WTF::move(source) }); + { + Locker locker { m_socketsLock }; + m_sockets.add(streamId, SocketData { WTF::move(sockets), WTF::move(source) }); + } m_udpAddresses.add(streamId, WTF::move(udpAddresses)); completionHandler({ WTF::move(gatheredAddresses), WTF::move(turnAddresses) }); @@ -461,9 +486,10 @@ void RiceBackend::setSocketTypeOfService(unsigned streamId, unsigned value) void RiceBackend::allocateSocket(unsigned streamId, unsigned componentId, WebCore::RTCIceProtocol protocol, const String& from, const String& to) { - // FIXME: TCP sockets support requires further debugging. For now keep it disabled to un-tangle post-commit bots. - // https://bugs.webkit.org/show_bug.cgi?id=313710 + // Old rice-io versions lack socket close notifications support and socket timeout configuration support. +#if !RICE_CHECK_VERSION(0, 4, 3) return; +#endif if (protocol == WebCore::RTCIceProtocol::Udp) return; @@ -494,8 +520,13 @@ void RiceBackend::allocateSocket(unsigned streamId, unsigned componentId, WebCor GUniquePtr localAddress(rice_tcp_socket_local_addr(socket)); auto localAddressString = riceAddressToString(localAddress.get()); + auto tcpAddresses = backend->m_tcpAddresses.ensure(data->streamId, [] { + return Vector> { }; + }); + tcpAddresses.iterator->value.append({ localAddressString, data->to }); + rice_sockets_add_tcp(sockets.get(), socket); - backend->configureSocketBufferSizes(); + backend->configureSockets(); callOnMainRunLoopAndWait([&, address = WTF::move(localAddressString)] mutable { if (RefPtr connection = backend->messageSenderConnection()) @@ -513,15 +544,30 @@ void RiceBackend::removeSocket(unsigned streamId, unsigned componentId, WebCore: GUniquePtr remoteAddress(riceAddressFromString(to)); auto sockets = getSocketsForStream(streamId); rice_sockets_remove_tcp(sockets.get(), localAddress.get(), remoteAddress.get()); + m_tcpAddresses.removeIf([&](auto& item) -> bool { + auto& [key, addresses] = item; + if (key != streamId) + return false; + + addresses.removeFirstMatching([&](const auto& pair) -> bool { + return pair.first == from && pair.second == to; + }); + return addresses.isEmpty(); + }); } -void RiceBackend::configureSocketBufferSizes() +void RiceBackend::configureSockets() { // Setting same librice socket size options as LibWebRTC. 1MB for incoming streams and 256Kb for outgoing streams. static const uint32_t receiveBufferSize = 1048576; static const uint32_t sendBufferSize = 262144; - for (auto& data : m_sockets.values()) + Locker locker { m_socketsLock }; + for (auto& data : m_sockets.values()) { rice_sockets_set_buffer_sizes(data.sockets.get(), sendBufferSize, receiveBufferSize); +#if RICE_CHECK_VERSION(0, 4, 3) + rice_sockets_set_timeouts(data.sockets.get(), 1000000, 1000000); +#endif + } } } // namespace WebKit diff --git a/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.h b/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.h index f0fa10e0ec09..01683d54ef95 100644 --- a/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.h +++ b/Source/WebKit/NetworkProcess/webrtc/rice/RiceBackend.h @@ -108,15 +108,17 @@ class RiceBackend : public ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtr m_runLoop; - void configureSocketBufferSizes(); + void configureSockets(); struct SocketData { GRefPtr sockets; GRefPtr source; }; - HashMap, WTF::UnsignedWithZeroKeyHashTraits> m_sockets; + Lock m_socketsLock; + HashMap, WTF::UnsignedWithZeroKeyHashTraits> m_sockets WTF_GUARDED_BY_LOCK(m_socketsLock); HashMap>, WTF::IntHash, WTF::UnsignedWithZeroKeyHashTraits> m_udpAddresses; + HashMap>, WTF::IntHash, WTF::UnsignedWithZeroKeyHashTraits> m_tcpAddresses; Vector> m_tcpListeners; HashMap> m_addressCache; diff --git a/Tools/Scripts/webkitpy/port/glib.py b/Tools/Scripts/webkitpy/port/glib.py index b76969ccc19d..4e62e3f5cc05 100644 --- a/Tools/Scripts/webkitpy/port/glib.py +++ b/Tools/Scripts/webkitpy/port/glib.py @@ -154,8 +154,8 @@ def setup_environ_for_server(self, server_name=None): # actual sound card. environment['WEBKIT_GST_MAX_NUMBER_OF_AUDIO_OUTPUT_CHANNELS'] = '2' - # Workaround for bots not using latest SDK version. - environment['RICE_LOG'] = 'none' + # LibRice logging, example: RICE_LOG=trace. + self._copy_values_from_environ_with_prefix(environment, 'RICE_') if self.get_option("leaks"): # Turn off GLib memory optimisations https://wiki.gnome.org/Valgrind. From 79003b865f9f903646dca04845d072f0a91766fa Mon Sep 17 00:00:00 2001 From: Eric Carlson Date: Fri, 15 May 2026 07:23:38 -0700 Subject: [PATCH 084/424] [Cocoa] Use AVCapturePhotoOutputReadinessCoordinator when taking photo https://bugs.webkit.org/show_bug.cgi?id=313854 rdar://176054992 Reviewed by Youenn Fablet. When the AVCapturePhotoOutput pipeline is not yet ready, store the pending photo settings and wait for a readiness change callback before dispatching the capture, instead of dispatching immediately and risking failure. Use a 60-second watchdog timer to reject the pending request if readiness is never signaled. Tested manually. * Source/WTF/wtf/PlatformHave.h: * Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.h: * Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.mm: * Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.h: * Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.mm: (WebCore::AVVideoCaptureSource::clearSession): (WebCore::AVVideoCaptureSource::photoOutput): (WebCore::AVVideoCaptureSource::takePhotoInternal): (WebCore::AVVideoCaptureSource::dispatchCaptureOnPhotoQueue): (WebCore::AVVideoCaptureSource::captureReadinessDidChange): (-[WebCoreAVVideoCaptureSourceObserver addNotificationObservers]): (-[WebCoreAVVideoCaptureSourceObserver captureOutput:didOutputSampleBuffer:fromConnection:]): (-[WebCoreAVVideoCaptureSourceObserver captureOutput:didFinishProcessingPhoto:error:]): (-[WebCoreAVVideoCaptureSourceObserver readinessCoordinator:captureReadinessDidChange:]): (-[WebCoreAVVideoCaptureSourceObserver deviceConnectedDidChange:]): (-[WebCoreAVVideoCaptureSourceObserver sessionRuntimeError:]): (-[WebCoreAVVideoCaptureSourceObserver beginSessionInterrupted:]): (-[WebCoreAVVideoCaptureSourceObserver endSessionInterrupted:]): Canonical link: https://commits.webkit.org/313310@main --- Source/WTF/wtf/PlatformHave.h | 5 + .../PAL/pal/cocoa/AVFoundationSoftLink.h | 4 + .../PAL/pal/cocoa/AVFoundationSoftLink.mm | 4 + .../mediastream/cocoa/AVVideoCaptureSource.h | 11 ++ .../mediastream/cocoa/AVVideoCaptureSource.mm | 145 ++++++++++++++---- 5 files changed, 135 insertions(+), 34 deletions(-) diff --git a/Source/WTF/wtf/PlatformHave.h b/Source/WTF/wtf/PlatformHave.h index 90c3ad31c3ff..377802f7aa50 100644 --- a/Source/WTF/wtf/PlatformHave.h +++ b/Source/WTF/wtf/PlatformHave.h @@ -762,6 +762,11 @@ #define HAVE_AVCAPTUREDEVICE 1 #endif +#if (PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 140000) \ + || ((PLATFORM(IOS) || PLATFORM(MACCATALYST) || PLATFORM(APPLETV)) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 170000) +#define HAVE_AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR 1 +#endif + #if PLATFORM(MAC) && defined __has_include && __has_include() #define HAVE_SANDBOX_MESSAGE_FILTERING 1 #endif diff --git a/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.h b/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.h index 9647657325b8..d2152ae83ac8 100644 --- a/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.h +++ b/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.h @@ -113,6 +113,10 @@ SOFT_LINK_CLASS_FOR_HEADER(PAL, AVCapturePhotoOutput) #if HAVE(AVCAPTUREDEVICEROTATIONCOORDINATOR) SOFT_LINK_CLASS_FOR_HEADER(PAL, AVCaptureDeviceRotationCoordinator) #endif + +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) +SOFT_LINK_CLASS_FOR_HEADER(PAL, AVCapturePhotoOutputReadinessCoordinator) +#endif #endif // HAVE(AVCAPTUREDEVICE) SOFT_LINK_CONSTANT_FOR_HEADER(PAL, AVFoundation, AVAudioTimePitchAlgorithmSpectral, NSString *) diff --git a/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.mm b/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.mm index 02c5cd6a2dd6..3f7054a8f956 100644 --- a/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.mm +++ b/Source/WebCore/PAL/pal/cocoa/AVFoundationSoftLink.mm @@ -135,6 +135,10 @@ static BOOL justReturnsNO() #if HAVE(AVCAPTUREDEVICEROTATIONCOORDINATOR) SOFT_LINK_CLASS_FOR_SOURCE_WITH_EXPORT(PAL, AVFoundation, AVCaptureDeviceRotationCoordinator, PAL_EXPORT) #endif + +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) +SOFT_LINK_CLASS_FOR_SOURCE_WITH_EXPORT(PAL, AVFoundation, AVCapturePhotoOutputReadinessCoordinator, PAL_EXPORT) +#endif #endif // HAVE(AVCAPTUREDEVICE) SOFT_LINK_CONSTANT_FOR_SOURCE_WITH_EXPORT(PAL, AVFoundation, AVAssetExportPresetHighestQuality, NSString *, PAL_EXPORT) diff --git a/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.h b/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.h index 7ba9a13939ac..44f4b85265aa 100644 --- a/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.h +++ b/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.h @@ -42,6 +42,7 @@ OBJC_CLASS AVCaptureDeviceFormat; OBJC_CLASS AVCaptureDeviceRotationCoordinator; OBJC_CLASS AVCapturePhoto; OBJC_CLASS AVCapturePhotoOutput; +OBJC_CLASS AVCapturePhotoOutputReadinessCoordinator; OBJC_CLASS AVCapturePhotoSettings; OBJC_CLASS AVCaptureOutput; OBJC_CLASS AVCaptureResolvedPhotoSettings; @@ -78,6 +79,9 @@ class AVVideoCaptureSource : public RealtimeVideoCaptureSource { void captureOutputDidOutputSampleBufferFromConnection(AVCaptureOutput*, CMSampleBufferRef, AVCaptureConnection*); void captureDeviceSuspendedDidChange(); void captureOutputDidFinishProcessingPhoto(RetainPtr, RetainPtr, RetainPtr); +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) + void captureReadinessDidChange(); +#endif void configurationChanged() final; @@ -160,6 +164,7 @@ class AVVideoCaptureSource : public RealtimeVideoCaptureSource { RetainPtr photoConfiguration(const PhotoSettings&, AVCapturePhotoOutput*); IntSize maxPhotoSizeForCurrentPreset(IntSize requestedSize) const; AVCapturePhotoOutput* photoOutput(); + void dispatchCaptureOnPhotoQueue(RetainPtr, RetainPtr&&); RefPtr m_buffer; RetainPtr m_videoOutput; @@ -177,6 +182,12 @@ class AVVideoCaptureSource : public RealtimeVideoCaptureSource { RetainPtr m_photoOutput WTF_GUARDED_BY_CAPABILITY(RunLoop::mainSingleton()); std::unique_ptr m_photoProducer WTF_GUARDED_BY_LOCK(m_photoLock); +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) + RetainPtr m_readinessCoordinator WTF_GUARDED_BY_CAPABILITY(RunLoop::mainSingleton()); + RetainPtr m_pendingPhotoSettings WTF_GUARDED_BY_CAPABILITY(RunLoop::mainSingleton()); + std::unique_ptr m_pendingCaptureWatchdog WTF_GUARDED_BY_CAPABILITY(RunLoop::mainSingleton()); + static constexpr Seconds photoCapturePipelineReadinessTimeout = 60_s; +#endif Lock m_photoLock; std::optional m_currentPreset WTF_GUARDED_BY_CAPABILITY(RunLoop::mainSingleton()); diff --git a/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.mm b/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.mm index 43f27189c3fd..f49dbfe7d4b1 100644 --- a/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.mm +++ b/Source/WebCore/platform/mediastream/cocoa/AVVideoCaptureSource.mm @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2024 Apple Inc. All rights reserved. + * Copyright (C) 2013-2026 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -82,7 +82,11 @@ @interface AVCaptureSession(Staging_113653478) - (instancetype)initWithMediaEnvironment:(NSString *)mediaEnvironment; @end -@interface WebCoreAVVideoCaptureSourceObserver : NSObject { +@interface WebCoreAVVideoCaptureSourceObserver : NSObject { ThreadSafeWeakPtr m_captureSource; } @@ -92,13 +96,19 @@ -(void)addNotificationObservers; -(void)removeNotificationObservers; -(void)captureOutput:(AVCaptureOutput*)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection*)connection; -(void)observeValueForKeyPath:keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context; +-(void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error; + #if PLATFORM(IOS_FAMILY) -(void)sessionRuntimeError:(NSNotification*)notification; -(void)beginSessionInterrupted:(NSNotification*)notification; -(void)endSessionInterrupted:(NSNotification*)notification; -(void)deviceConnectedDidChange:(NSNotification*)notification; #endif -- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error; + +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) +-(void)readinessCoordinator:(AVCapturePhotoOutputReadinessCoordinator *)coordinator captureReadinessDidChange:(AVCapturePhotoOutputCaptureReadiness)captureReadiness; +#endif + @end namespace WebCore { @@ -356,6 +366,14 @@ static double cameraZoomScaleFactor(AVCaptureDeviceType deviceType) [m_session removeObserver:m_objcObserver.get() forKeyPath:@"running"]; m_session = nullptr; m_photoOutput = nullptr; +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) + m_readinessCoordinator = nullptr; + if (m_pendingPhotoSettings) { + m_pendingPhotoSettings = nullptr; + m_pendingCaptureWatchdog = nullptr; + rejectPendingPhotoRequest("Session cleared"_s); + } +#endif } void AVVideoCaptureSource::startProducingData() @@ -661,6 +679,11 @@ static bool isZoomSupported(const Vector& presets) } [session() addOutput:m_photoOutput.get()]; +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) + m_readinessCoordinator = adoptNS([PAL::allocAVCapturePhotoOutputReadinessCoordinatorInstance() initWithPhotoOutput:m_photoOutput.get()]); + [m_readinessCoordinator setDelegate:m_objcObserver.get()]; +#endif + return m_photoOutput.get(); } @@ -748,7 +771,6 @@ static bool isZoomSupported(const Vector& presets) { assertIsCurrent(RunLoop::mainSingleton()); - auto identifier = LOGIDENTIFIER; RetainPtr photoOutput = this->photoOutput(); if (!photoOutput) return TakePhotoNativePromise::createAndReject("Internal error"_s); @@ -757,7 +779,7 @@ static bool isZoomSupported(const Vector& presets) { Locker lock { m_photoLock }; if (m_photoProducer) { - ERROR_LOG_IF_POSSIBLE(identifier, "m_photoProducer should be NULL!"); + ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "m_photoProducer should be NULL!"); return TakePhotoNativePromise::createAndReject("Internal error"_s); } @@ -767,43 +789,87 @@ static bool isZoomSupported(const Vector& presets) RetainPtr avPhotoSettings = photoConfiguration(photoSettings, photoOutput.get()); if (!avPhotoSettings) { - ERROR_LOG_IF_POSSIBLE(identifier, "photoConfiguration() failed"); + ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "photoConfiguration() failed"); return TakePhotoNativePromise::createAndReject("Internal error"_s); } - photoQueueSingleton().dispatch([protectedThis = Ref { *this }, this, photoOutput = WTF::move(photoOutput), avPhotoSettings = WTF::move(avPhotoSettings), identifier = WTF::move(identifier)] mutable { - assertIsCurrent(photoQueueSingleton()); - - @try { - if ([avPhotoSettings respondsToSelector:@selector(setMaxPhotoDimensions:)]) { - auto requestedPhotoDimensions = toIntSize([avPhotoSettings maxPhotoDimensions]); - if (!requestedPhotoDimensions.isEmpty()) { - auto maxOutputDimensions = toIntSize([photoOutput maxPhotoDimensions]); - if (requestedPhotoDimensions.width() > maxOutputDimensions.width() || requestedPhotoDimensions.height() > maxOutputDimensions.height()) - [photoOutput setMaxPhotoDimensions:toCMVideoDimensions(requestedPhotoDimensions)]; + if ([avPhotoSettings respondsToSelector:@selector(setMaxPhotoDimensions:)]) { + auto requestedDims = toIntSize([avPhotoSettings maxPhotoDimensions]); + if (!requestedDims.isEmpty()) { + auto currentDims = toIntSize([photoOutput maxPhotoDimensions]); + if (requestedDims.width() > currentDims.width() || requestedDims.height() > currentDims.height()) { + @try { + [photoOutput setMaxPhotoDimensions:toCMVideoDimensions(requestedDims)]; + } @catch (NSException *exception) { + ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "error calling setMaxPhotoDimensions: ", [[exception name] UTF8String], ", reason: ", [exception reason]); + rejectPendingPhotoRequest("setMaxPhotoDimensions failed"_s); + return promise.releaseNonNull(); } } - } @catch(NSException *exception) { - RunLoop::mainSingleton().dispatch([protectedThis = WTF::move(protectedThis), identifier = WTF::move(identifier), exception = RetainPtr { exception }] mutable { - ERROR_LOG_WITH_THIS_IF_POSSIBLE(protectedThis, identifier, "error configuring photo output ", [[exception name] UTF8String], ", reason : ", [exception reason]); - protectedThis->rejectPendingPhotoRequest("setMaxPhotoDimensions failed"_s); - }); - return; } + } + +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) + if (m_readinessCoordinator && [m_readinessCoordinator captureReadiness] != AVCapturePhotoOutputCaptureReadinessReady) { + m_pendingPhotoSettings = WTF::move(avPhotoSettings); + m_pendingCaptureWatchdog = WTF::makeUnique([this, protectedThis = Ref { *this }, identifier = LOGIDENTIFIER] { + assertIsCurrent(RunLoop::mainSingleton()); + if (!m_pendingPhotoSettings) + return; + + ERROR_LOG_IF_POSSIBLE(identifier, "photo pipeline readiness timeout"); + m_pendingPhotoSettings = nullptr; + rejectPendingPhotoRequest("Photo capture timed out waiting for pipeline readiness"_s); + }); + m_pendingCaptureWatchdog->startOneShot(photoCapturePipelineReadinessTimeout); + return promise.releaseNonNull(); + } +#endif + + dispatchCaptureOnPhotoQueue(WTF::move(photoOutput), WTF::move(avPhotoSettings)); + return promise.releaseNonNull(); +} +void AVVideoCaptureSource::dispatchCaptureOnPhotoQueue(RetainPtr photoOutput, RetainPtr&& avPhotoSettings) +{ + assertIsCurrent(RunLoop::mainSingleton()); + auto identifier = LOGIDENTIFIER; + photoQueueSingleton().dispatch([protectedThis = Ref { *this }, this, + photoOutput = WTF::move(photoOutput), avPhotoSettings = WTF::move(avPhotoSettings), identifier] mutable { + assertIsCurrent(photoQueueSingleton()); @try { [photoOutput capturePhotoWithSettings:avPhotoSettings.get() delegate:m_objcObserver.get()]; - } @catch(NSException *exception) { - RunLoop::mainSingleton().dispatch([protectedThis = WTF::move(protectedThis), identifier = WTF::move(identifier), exception = RetainPtr { exception }] mutable { - ERROR_LOG_WITH_THIS_IF_POSSIBLE(protectedThis, identifier, "error taking photo ", [[exception name] UTF8String], ", reason : ", [exception reason]); + } @catch (NSException *exception) { + RunLoop::mainSingleton().dispatch([protectedThis = WTF::move(protectedThis), identifier, exception = RetainPtr { exception }] mutable { + ERROR_LOG_WITH_THIS_IF_POSSIBLE(protectedThis, identifier, "error taking photo ", [[exception name] UTF8String], ", reason: ", [exception reason]); protectedThis->rejectPendingPhotoRequest("capturePhotoWithSettings failed"_s); }); } - }); +} - return promise.releaseNonNull(); +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) +void AVVideoCaptureSource::captureReadinessDidChange() +{ + assertIsCurrent(RunLoop::mainSingleton()); + if (!m_readinessCoordinator || !m_pendingPhotoSettings) + return; + + auto readiness = [m_readinessCoordinator captureReadiness]; + if (readiness == AVCapturePhotoOutputCaptureReadinessSessionNotRunning) { + m_pendingPhotoSettings = nullptr; + m_pendingCaptureWatchdog = nullptr; + rejectPendingPhotoRequest("Session not running"_s); + return; + } + if (readiness != AVCapturePhotoOutputCaptureReadinessReady) + return; + + m_pendingCaptureWatchdog = nullptr; + auto pendingSettings = std::exchange(m_pendingPhotoSettings, { }); + dispatchCaptureOnPhotoQueue(m_photoOutput, WTF::move(pendingSettings)); } +#endif auto AVVideoCaptureSource::getPhotoCapabilities() -> Ref { @@ -1537,7 +1603,7 @@ - (void)disconnect - (void)addNotificationObservers { - auto source = m_captureSource.get(); + RefPtr source = m_captureSource.get(); ASSERT(source); NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; @@ -1559,16 +1625,27 @@ - (void)removeNotificationObservers - (void)captureOutput:(AVCaptureOutput*)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection*)connection { - if (auto source = m_captureSource.get()) + if (RefPtr source = m_captureSource.get()) source->captureOutputDidOutputSampleBufferFromConnection(captureOutput, sampleBuffer, connection); } - (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error { - if (auto source = m_captureSource.get()) + if (RefPtr source = m_captureSource.get()) source->captureOutputDidFinishProcessingPhoto(captureOutput, photo, error); } +#if HAVE(AVCAPTUREPHOTOOUTPUT_READINESS_COORDINATOR) +- (void)readinessCoordinator:(AVCapturePhotoOutputReadinessCoordinator *)coordinator captureReadinessDidChange:(AVCapturePhotoOutputCaptureReadiness)captureReadiness +{ + UNUSED_PARAM(coordinator); + UNUSED_PARAM(captureReadiness); + + if (RefPtr source = m_captureSource.get()) + source->captureReadinessDidChange(); +} +#endif + - (void)observeValueForKeyPath:keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { UNUSED_PARAM(object); @@ -1602,7 +1679,7 @@ - (void)observeValueForKeyPath:keyPath ofObject:(id)object change:(NSDictionary* - (void)deviceConnectedDidChange:(NSNotification*)notification { - if (auto source = m_captureSource.get()) + if (RefPtr source = m_captureSource.get()) source->deviceDisconnected(notification); } @@ -1610,19 +1687,19 @@ - (void)deviceConnectedDidChange:(NSNotification*)notification - (void)sessionRuntimeError:(NSNotification*)notification { NSError *error = notification.userInfo[AVCaptureSessionErrorKey]; - if (auto source = m_captureSource.get()) + if (RefPtr source = m_captureSource.get()) source->captureSessionRuntimeError(error); } - (void)beginSessionInterrupted:(NSNotification*)notification { - if (auto source = m_captureSource.get()) + if (RefPtr source = m_captureSource.get()) source->captureSessionBeginInterruption(notification); } - (void)endSessionInterrupted:(NSNotification*)notification { - if (auto source = m_captureSource.get()) + if (RefPtr source = m_captureSource.get()) source->captureSessionEndInterruption(notification); } #endif From f27d4cd2eaf50afdc963faa7d08e702d4459b465 Mon Sep 17 00:00:00 2001 From: Alan Baradlay Date: Fri, 15 May 2026 08:27:50 -0700 Subject: [PATCH 085/424] [cleanup] Replace chained assignments in intrinsic width computations https://bugs.webkit.org/show_bug.cgi?id=314496 Reviewed by Antti Koivisto. * Source/WebCore/rendering/RenderBlock.cpp: (WebCore::RenderBlock::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderBox.cpp: (WebCore::RenderBox::computeIntrinsicKeywordLogicalWidths const): * Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp: (WebCore::RenderDeprecatedFlexibleBox::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderFileUploadControl.cpp: (WebCore::RenderFileUploadControl::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderFragmentContainer.cpp: (WebCore::RenderFragmentContainer::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderListBox.cpp: (WebCore::RenderListBox::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderListMarker.cpp: (WebCore::RenderListMarker::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderMenuList.cpp: (WebCore::RenderMenuList::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderReplaced.cpp: (WebCore::RenderReplaced::computeIntrinsicLogicalWidths const): * Source/WebCore/rendering/RenderSlider.cpp: (WebCore::RenderSlider::computePreferredLogicalWidths): * Source/WebCore/rendering/RenderTextControl.cpp: (WebCore::RenderTextControl::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLFraction.cpp: (WebCore::RenderMathMLFraction::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLMenclose.cpp: (WebCore::RenderMathMLMenclose::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLOperator.cpp: (WebCore::RenderMathMLOperator::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLPadded.cpp: (WebCore::RenderMathMLPadded::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLRoot.cpp: (WebCore::RenderMathMLRoot::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLRow.cpp: (WebCore::RenderMathMLRow::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLSpace.cpp: (WebCore::RenderMathMLSpace::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLToken.cpp: (WebCore::RenderMathMLToken::computePreferredLogicalWidths): * Source/WebCore/rendering/mathml/RenderMathMLUnderOver.cpp: (WebCore::RenderMathMLUnderOver::computePreferredLogicalWidths): Canonical link: https://commits.webkit.org/313311@main --- Source/WebCore/rendering/RenderBlock.cpp | 3 +- Source/WebCore/rendering/RenderBox.cpp | 40 ++++++++++--------- .../rendering/RenderDeprecatedFlexibleBox.cpp | 10 +++-- .../rendering/RenderFileUploadControl.cpp | 7 ++-- .../rendering/RenderFragmentContainer.cpp | 10 +++-- Source/WebCore/rendering/RenderListBox.cpp | 7 ++-- Source/WebCore/rendering/RenderListMarker.cpp | 3 +- Source/WebCore/rendering/RenderMenuList.cpp | 7 ++-- Source/WebCore/rendering/RenderReplaced.cpp | 3 +- Source/WebCore/rendering/RenderSlider.cpp | 7 ++-- .../WebCore/rendering/RenderTextControl.cpp | 7 ++-- .../rendering/mathml/RenderMathMLFraction.cpp | 3 +- .../rendering/mathml/RenderMathMLMenclose.cpp | 3 +- .../rendering/mathml/RenderMathMLOperator.cpp | 3 +- .../rendering/mathml/RenderMathMLPadded.cpp | 3 +- .../rendering/mathml/RenderMathMLRoot.cpp | 3 +- .../rendering/mathml/RenderMathMLRow.cpp | 3 +- .../rendering/mathml/RenderMathMLSpace.cpp | 3 +- .../rendering/mathml/RenderMathMLToken.cpp | 3 +- .../mathml/RenderMathMLUnderOver.cpp | 3 +- 20 files changed, 78 insertions(+), 53 deletions(-) diff --git a/Source/WebCore/rendering/RenderBlock.cpp b/Source/WebCore/rendering/RenderBlock.cpp index 4f72ae3b1a83..f91c28a34c01 100644 --- a/Source/WebCore/rendering/RenderBlock.cpp +++ b/Source/WebCore/rendering/RenderBlock.cpp @@ -2245,7 +2245,8 @@ void RenderBlock::computePreferredLogicalWidths() computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; } else if (shouldComputeLogicalWidthFromAspectRatio()) { - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = (computeLogicalWidthFromAspectRatio() - borderAndPaddingLogicalWidth()); + m_maxPreferredLogicalWidth = computeLogicalWidthFromAspectRatio() - borderAndPaddingLogicalWidth(); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; m_minPreferredLogicalWidth = std::max(0_lu, m_minPreferredLogicalWidth); m_maxPreferredLogicalWidth = std::max(0_lu, m_maxPreferredLogicalWidth); applyAutomaticContentBasedMinimumSize(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); diff --git a/Source/WebCore/rendering/RenderBox.cpp b/Source/WebCore/rendering/RenderBox.cpp index 2c2f7897a37c..947dcdb44b22 100644 --- a/Source/WebCore/rendering/RenderBox.cpp +++ b/Source/WebCore/rendering/RenderBox.cpp @@ -2928,25 +2928,29 @@ LayoutUnit RenderBox::fillAvailableMeasure(LayoutUnit availableLogicalWidth, Lay template void RenderBox::computeIntrinsicKeywordLogicalWidths(Keyword, LayoutUnit borderAndPadding, LayoutUnit& minLogicalWidth, LayoutUnit& maxLogicalWidth) const { if constexpr (std::same_as) - computeIntrinsicKeywordLogicalWidths(minLogicalWidth, maxLogicalWidth); - else { - if (shouldComputeLogicalWidthFromAspectRatio()) { - minLogicalWidth = maxLogicalWidth = computeLogicalWidthFromAspectRatio() - borderAndPadding; - applyAutomaticContentBasedMinimumSize(minLogicalWidth, maxLogicalWidth); - } else if (CheckedPtr renderReplaced = dynamicDowncast(*this)) { - // For replaced elements with an intrinsic aspect ratio (e.g. ) and a - // specified block size, compute the transferred min/max-content inline size - // through the intrinsic ratio rather than using the raw natural width. - auto preferredRatio = renderReplaced->preferredAspectRatioAsSize().aspectRatioDouble(); - if (preferredRatio && style().logicalHeight().isSpecified()) { - auto computedValues = computeLogicalHeight(logicalHeight(), logicalTop()); - auto contentBlockSize = std::max(0_lu, computedValues.extent - borderAndPaddingLogicalHeight()); - minLogicalWidth = maxLogicalWidth = LayoutUnit(contentBlockSize * preferredRatio); - } else - computeIntrinsicKeywordLogicalWidths(minLogicalWidth, maxLogicalWidth); - } else - computeIntrinsicKeywordLogicalWidths(minLogicalWidth, maxLogicalWidth); + return computeIntrinsicKeywordLogicalWidths(minLogicalWidth, maxLogicalWidth); + + if (shouldComputeLogicalWidthFromAspectRatio()) { + maxLogicalWidth = computeLogicalWidthFromAspectRatio() - borderAndPadding; + minLogicalWidth = maxLogicalWidth; + applyAutomaticContentBasedMinimumSize(minLogicalWidth, maxLogicalWidth); + return; + } + + if (CheckedPtr renderReplaced = dynamicDowncast(*this)) { + // For replaced elements with an intrinsic aspect ratio (e.g. ) and a + // specified block size, compute the transferred min/max-content inline size + // through the intrinsic ratio rather than using the raw natural width. + auto preferredRatio = renderReplaced->preferredAspectRatioAsSize().aspectRatioDouble(); + if (preferredRatio && style().logicalHeight().isSpecified()) { + auto computedValues = computeLogicalHeight(logicalHeight(), logicalTop()); + auto contentBlockSize = std::max(0_lu, computedValues.extent - borderAndPaddingLogicalHeight()); + maxLogicalWidth = LayoutUnit { contentBlockSize * preferredRatio }; + minLogicalWidth = maxLogicalWidth; + return; + } } + computeIntrinsicKeywordLogicalWidths(minLogicalWidth, maxLogicalWidth); } static inline bool NODELETE isOrthogonal(const RenderBox& renderer, const RenderElement& ancestor) diff --git a/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp b/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp index d1d3e5e4020a..e3140eb6d0d5 100644 --- a/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp +++ b/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp @@ -270,10 +270,12 @@ void RenderDeprecatedFlexibleBox::computePreferredLogicalWidths() { ASSERT(needsPreferredLogicalWidthsUpdate()); - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = 0; - if (auto fixedWidth = style().width().tryFixed(); fixedWidth && fixedWidth->isPositive()) - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedWidth); - else + m_minPreferredLogicalWidth = 0; + m_maxPreferredLogicalWidth = 0; + if (auto fixedWidth = style().width().tryFixed(); fixedWidth && fixedWidth->isPositive()) { + m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedWidth); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; + } else computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); constrainPreferredLogicalWidthsByMinMax(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); diff --git a/Source/WebCore/rendering/RenderFileUploadControl.cpp b/Source/WebCore/rendering/RenderFileUploadControl.cpp index b9e0ec9905b6..3a630ca2431d 100644 --- a/Source/WebCore/rendering/RenderFileUploadControl.cpp +++ b/Source/WebCore/rendering/RenderFileUploadControl.cpp @@ -309,9 +309,10 @@ void RenderFileUploadControl::computePreferredLogicalWidths() m_minPreferredLogicalWidth = 0; m_maxPreferredLogicalWidth = 0; - if (auto fixedLogicalWidth = style().logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); - else + if (auto fixedLogicalWidth = style().logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) { + m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; + } else computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); constrainPreferredLogicalWidthsByMinMax(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); diff --git a/Source/WebCore/rendering/RenderFragmentContainer.cpp b/Source/WebCore/rendering/RenderFragmentContainer.cpp index 321c314573db..78f8935524f6 100644 --- a/Source/WebCore/rendering/RenderFragmentContainer.cpp +++ b/Source/WebCore/rendering/RenderFragmentContainer.cpp @@ -397,12 +397,14 @@ void RenderFragmentContainer::computePreferredLogicalWidths() // FIXME: Currently, the code handles only the case for min-width/max-width. // It should also support other values, like percentage, calc or viewport relative. - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = 0; + m_minPreferredLogicalWidth = 0; + m_maxPreferredLogicalWidth = 0; CheckedRef styleToUse = style(); - if (auto fixedLogicalWidth = styleToUse->logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); - else + if (auto fixedLogicalWidth = styleToUse->logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) { + m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; + } else computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); constrainPreferredLogicalWidthsByMinMax(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); diff --git a/Source/WebCore/rendering/RenderListBox.cpp b/Source/WebCore/rendering/RenderListBox.cpp index 8ec1b51ee3f7..e45a54613e9e 100644 --- a/Source/WebCore/rendering/RenderListBox.cpp +++ b/Source/WebCore/rendering/RenderListBox.cpp @@ -250,9 +250,10 @@ void RenderListBox::computePreferredLogicalWidths() m_minPreferredLogicalWidth = 0; m_maxPreferredLogicalWidth = 0; - if (auto fixedLogicalWidth = style().logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); - else + if (auto fixedLogicalWidth = style().logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) { + m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; + } else computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); constrainPreferredLogicalWidthsByMinMax(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); diff --git a/Source/WebCore/rendering/RenderListMarker.cpp b/Source/WebCore/rendering/RenderListMarker.cpp index 6f407cf9f31b..4d04bcb64b31 100644 --- a/Source/WebCore/rendering/RenderListMarker.cpp +++ b/Source/WebCore/rendering/RenderListMarker.cpp @@ -443,7 +443,8 @@ void RenderListMarker::computePreferredLogicalWidths() if (isImage()) { LayoutSize imageSize = LayoutSize(m_image->imageSize(this, style().usedZoom())); - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = writingMode().isHorizontal() ? imageSize.width() : imageSize.height(); + m_maxPreferredLogicalWidth = writingMode().isHorizontal() ? imageSize.width() : imageSize.height(); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; clearNeedsPreferredWidthsUpdate(); updateInlineMargins(); return; diff --git a/Source/WebCore/rendering/RenderMenuList.cpp b/Source/WebCore/rendering/RenderMenuList.cpp index 24fd7b6fe5b5..fedd9574ba5b 100644 --- a/Source/WebCore/rendering/RenderMenuList.cpp +++ b/Source/WebCore/rendering/RenderMenuList.cpp @@ -178,9 +178,10 @@ void RenderMenuList::computePreferredLogicalWidths() m_minPreferredLogicalWidth = 0; m_maxPreferredLogicalWidth = 0; - if (auto fixedLogicalWidth = style().logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); - else + if (auto fixedLogicalWidth = style().logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositive()) { + m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; + } else computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); constrainPreferredLogicalWidthsByMinMax(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); diff --git a/Source/WebCore/rendering/RenderReplaced.cpp b/Source/WebCore/rendering/RenderReplaced.cpp index b39545afc688..457da965f171 100644 --- a/Source/WebCore/rendering/RenderReplaced.cpp +++ b/Source/WebCore/rendering/RenderReplaced.cpp @@ -816,7 +816,8 @@ LayoutUnit RenderReplaced::computeReplacedLogicalHeight(std::optionalisPositiveOrZero()) - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); - else + if (auto fixedLogicalWidth = style().logicalWidth().tryFixed(); fixedLogicalWidth && fixedLogicalWidth->isPositiveOrZero()) { + m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(*fixedLogicalWidth); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; + } else computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); constrainPreferredLogicalWidthsByMinMax(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth); diff --git a/Source/WebCore/rendering/mathml/RenderMathMLFraction.cpp b/Source/WebCore/rendering/mathml/RenderMathMLFraction.cpp index 8157c5600f07..4063def72e03 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLFraction.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLFraction.cpp @@ -197,7 +197,8 @@ void RenderMathMLFraction::computePreferredLogicalWidths() LayoutUnit numeratorWidth = numerator().maxPreferredLogicalWidth() + marginIntrinsicLogicalWidthForChild(numerator()); LayoutUnit denominatorWidth = denominator().maxPreferredLogicalWidth() + marginIntrinsicLogicalWidthForChild(denominator()); - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = std::max(numeratorWidth, denominatorWidth); + m_maxPreferredLogicalWidth = std::max(numeratorWidth, denominatorWidth); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; auto sizes = sizeAppliedToMathContent(LayoutPhase::CalculatePreferredLogicalWidth); applySizeToMathContent(LayoutPhase::CalculatePreferredLogicalWidth, sizes); diff --git a/Source/WebCore/rendering/mathml/RenderMathMLMenclose.cpp b/Source/WebCore/rendering/mathml/RenderMathMLMenclose.cpp index 6806c5b021de..6b8556858c7f 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLMenclose.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLMenclose.cpp @@ -166,7 +166,8 @@ void RenderMathMLMenclose::computePreferredLogicalWidths() LayoutUnit preferredWidth = preferredLogicalWidthOfRowItems(); SpaceAroundContent space = spaceAroundContent(preferredWidth, 0); preferredWidth += space.left + space.right; - m_maxPreferredLogicalWidth = m_minPreferredLogicalWidth = preferredWidth; + m_minPreferredLogicalWidth = preferredWidth; + m_maxPreferredLogicalWidth = preferredWidth; auto sizes = sizeAppliedToMathContent(LayoutPhase::CalculatePreferredLogicalWidth); applySizeToMathContent(LayoutPhase::CalculatePreferredLogicalWidth, sizes); diff --git a/Source/WebCore/rendering/mathml/RenderMathMLOperator.cpp b/Source/WebCore/rendering/mathml/RenderMathMLOperator.cpp index 66c120b36789..ca5a7d26633e 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLOperator.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLOperator.cpp @@ -220,7 +220,8 @@ void RenderMathMLOperator::computePreferredLogicalWidths() // FIXME: The spacing should only be added inside (perhaps inferred) mrow (http://www.w3.org/TR/MathML/chapter3.html#presm.opspacing). preferredWidth = leadingSpace() + preferredWidth + trailingSpace(); - m_maxPreferredLogicalWidth = m_minPreferredLogicalWidth = preferredWidth; + m_minPreferredLogicalWidth = preferredWidth; + m_maxPreferredLogicalWidth = preferredWidth; clearNeedsPreferredWidthsUpdate(); } diff --git a/Source/WebCore/rendering/mathml/RenderMathMLPadded.cpp b/Source/WebCore/rendering/mathml/RenderMathMLPadded.cpp index 75b486e56065..9f565862179e 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLPadded.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLPadded.cpp @@ -96,7 +96,8 @@ void RenderMathMLPadded::computePreferredLogicalWidths() // We parse it using the preferred width of the content as its default value. LayoutUnit preferredWidth = preferredLogicalWidthOfRowItems(); preferredWidth = mpaddedWidth(preferredWidth); - m_maxPreferredLogicalWidth = m_minPreferredLogicalWidth = preferredWidth; + m_minPreferredLogicalWidth = preferredWidth; + m_maxPreferredLogicalWidth = preferredWidth; auto sizes = sizeAppliedToMathContent(LayoutPhase::CalculatePreferredLogicalWidth); applySizeToMathContent(LayoutPhase::CalculatePreferredLogicalWidth, sizes); diff --git a/Source/WebCore/rendering/mathml/RenderMathMLRoot.cpp b/Source/WebCore/rendering/mathml/RenderMathMLRoot.cpp index 34a0581b6ad4..49edd413437f 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLRoot.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLRoot.cpp @@ -182,7 +182,8 @@ void RenderMathMLRoot::computePreferredLogicalWidths() preferredWidth += m_radicalOperator.maxPreferredWidth(); preferredWidth += getBase().maxPreferredLogicalWidth() + marginIntrinsicLogicalWidthForChild(getBase()); } - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = preferredWidth; + m_maxPreferredLogicalWidth = preferredWidth; + m_minPreferredLogicalWidth = preferredWidth; auto sizes = sizeAppliedToMathContent(LayoutPhase::CalculatePreferredLogicalWidth); applySizeToMathContent(LayoutPhase::CalculatePreferredLogicalWidth, sizes); diff --git a/Source/WebCore/rendering/mathml/RenderMathMLRow.cpp b/Source/WebCore/rendering/mathml/RenderMathMLRow.cpp index 9e7332703ca3..e283d9cc0351 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLRow.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLRow.cpp @@ -134,7 +134,8 @@ void RenderMathMLRow::computePreferredLogicalWidths() { ASSERT(needsPreferredLogicalWidthsUpdate()); - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = preferredLogicalWidthOfRowItems(); + m_maxPreferredLogicalWidth = preferredLogicalWidthOfRowItems(); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; auto sizes = sizeAppliedToMathContent(LayoutPhase::CalculatePreferredLogicalWidth); applySizeToMathContent(LayoutPhase::CalculatePreferredLogicalWidth, sizes); diff --git a/Source/WebCore/rendering/mathml/RenderMathMLSpace.cpp b/Source/WebCore/rendering/mathml/RenderMathMLSpace.cpp index 6205db6f6b3b..d083324dcbf3 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLSpace.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLSpace.cpp @@ -55,7 +55,8 @@ void RenderMathMLSpace::computePreferredLogicalWidths() { ASSERT(needsPreferredLogicalWidthsUpdate()); - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = spaceWidth(); + m_maxPreferredLogicalWidth = spaceWidth(); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; auto sizes = sizeAppliedToMathContent(LayoutPhase::CalculatePreferredLogicalWidth); applySizeToMathContent(LayoutPhase::CalculatePreferredLogicalWidth, sizes); diff --git a/Source/WebCore/rendering/mathml/RenderMathMLToken.cpp b/Source/WebCore/rendering/mathml/RenderMathMLToken.cpp index 209185bc9360..ca1967f1cd58 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLToken.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLToken.cpp @@ -89,7 +89,8 @@ void RenderMathMLToken::computePreferredLogicalWidths() if (m_mathVariantCodePoint) { auto mathVariantGlyph = style().fontCascade().glyphDataForCharacter(m_mathVariantCodePoint.value(), m_mathVariantIsMirrored); if (mathVariantGlyph.font) { - m_maxPreferredLogicalWidth = m_minPreferredLogicalWidth = mathVariantGlyph.font->widthForGlyph(mathVariantGlyph.glyph); + m_maxPreferredLogicalWidth = mathVariantGlyph.font->widthForGlyph(mathVariantGlyph.glyph); + m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth; adjustPreferredLogicalWidthsForBorderAndPadding(); clearNeedsPreferredWidthsUpdate(); return; diff --git a/Source/WebCore/rendering/mathml/RenderMathMLUnderOver.cpp b/Source/WebCore/rendering/mathml/RenderMathMLUnderOver.cpp index 922020b94149..534122e6eea3 100644 --- a/Source/WebCore/rendering/mathml/RenderMathMLUnderOver.cpp +++ b/Source/WebCore/rendering/mathml/RenderMathMLUnderOver.cpp @@ -207,7 +207,8 @@ void RenderMathMLUnderOver::computePreferredLogicalWidths() if (scriptType() == MathMLScriptsElement::ScriptType::Over || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) preferredWidth = std::max(preferredWidth, over().maxPreferredLogicalWidth() + marginIntrinsicLogicalWidthForChild(over())); - m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = preferredWidth; + m_maxPreferredLogicalWidth = preferredWidth; + m_minPreferredLogicalWidth = preferredWidth; auto sizes = sizeAppliedToMathContent(LayoutPhase::CalculatePreferredLogicalWidth); applySizeToMathContent(LayoutPhase::CalculatePreferredLogicalWidth, sizes); From 83e85f875a707c58a94138d4e6cca9aae8b41e97 Mon Sep 17 00:00:00 2001 From: Yusuke Suzuki Date: Fri, 15 May 2026 09:59:24 -0700 Subject: [PATCH 086/424] [JSC] Simplify bytecode writer https://bugs.webkit.org/show_bug.cgi?id=314878 rdar://177142767 Reviewed by Keith Miller. Currently bytecode writer generated by a script is emitting things like, ``` gen->write(Fits::opcodeIDSize>::convert(opcodeID)); gen->write(Fits::convert(dst)); gen->write(Fits::convert(scope)); gen->write(Fits::convert(var)); gen->write(Fits::convert(getPutInfo)); gen->write(Fits::convert(localScopeDepth)); gen->write(Fits::convert(offset)); gen->write(Fits::convert(valueProfile)); gen->write(Fits::convert(__metadataID)); ``` and each write function is checking capacity and writing byte. And we are checking for each byte. So this is slow. Instead, we generate things like, ``` gen->template writeOpcode<__size>(opcodeID, message, errorType); ``` and this function will use variadic-templatized `write` function which accumulate necessary size, reserve region and emit them via WTF::unalignedStore. ``` m_writer.write( Fits::convert(opcodeID), Fits::convert(ops)...); ``` This is significantly faster for JS code which generates huge amount of bytecode. * Source/JavaScriptCore/bytecode/InstructionStream.h: (JSC::InstructionStreamWriter::write): (JSC::InstructionStreamWriter::reserve): * Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBase.h: (JSC::BytecodeGeneratorBase::write): * Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBaseInlines.h: (JSC::BytecodeGeneratorBase::writeOpcode): (JSC::BytecodeGeneratorBase::write): Deleted. * Source/JavaScriptCore/generator/Argument.rb: * Source/JavaScriptCore/generator/Fits.rb: * Source/JavaScriptCore/generator/Opcode.rb: Canonical link: https://commits.webkit.org/313312@main --- .../bytecode/InstructionStream.h | 46 +++++---------- .../bytecompiler/BytecodeGeneratorBase.h | 15 +++-- .../BytecodeGeneratorBaseInlines.h | 56 ++++++++----------- Source/JavaScriptCore/generator/Argument.rb | 4 -- Source/JavaScriptCore/generator/Fits.rb | 4 -- Source/JavaScriptCore/generator/Opcode.rb | 14 +++-- 6 files changed, 53 insertions(+), 86 deletions(-) diff --git a/Source/JavaScriptCore/bytecode/InstructionStream.h b/Source/JavaScriptCore/bytecode/InstructionStream.h index 271fd7c493de..46c9a26bdabe 100644 --- a/Source/JavaScriptCore/bytecode/InstructionStream.h +++ b/Source/JavaScriptCore/bytecode/InstructionStream.h @@ -28,6 +28,7 @@ #include #include +#include #include WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN @@ -241,43 +242,24 @@ class InstructionStreamWriter : public InstructionStream { return m_position; } - void write(uint8_t byte) + template + requires (sizeof...(Args) > 0 && (... && std::integral)) + void write(Args... args) { - ASSERT(!m_finalized); - if (m_position < m_instructions.size()) - m_instructions[m_position++] = byte; - else { - m_instructions.append(byte); - m_position++; - } - } - - void write(uint16_t h) - { - ASSERT(!m_finalized); - uint8_t bytes[2]; - std::memcpy(bytes, &h, sizeof(h)); - - // Though not always obvious, we don't have to invert the order of the - // bytes written here for CPU(BIG_ENDIAN). This is because the incoming - // i value is already ordered in big endian on CPU(BIG_EDNDIAN) platforms. - write(bytes[0]); - write(bytes[1]); + constexpr size_t totalSize = (sizeof(Args) + ...); + auto* p = static_cast(reserve()); + ((WTF::unalignedStore(p, args), p += sizeof(args)), ...); } - void write(uint32_t i) + template + uint8_t* reserve() { ASSERT(!m_finalized); - uint8_t bytes[4]; - std::memcpy(bytes, &i, sizeof(i)); - - // Though not always obvious, we don't have to invert the order of the - // bytes written here for CPU(BIG_ENDIAN). This is because the incoming - // i value is already ordered in big endian on CPU(BIG_EDNDIAN) platforms. - write(bytes[0]); - write(bytes[1]); - write(bytes[2]); - write(bytes[3]); + if ((m_position + size) > m_instructions.size()) + m_instructions.grow(m_position + size); + auto* result = m_instructions.mutableSpan().data() + m_position; + m_position += size; + return result; } void rewind(MutableRef& ref) diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBase.h b/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBase.h index ecbc74ff379a..9cf2d00817f6 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBase.h +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBase.h @@ -72,12 +72,15 @@ class BytecodeGeneratorBase { void alignWideOpcode16(); void alignWideOpcode32(); - void write(uint8_t); - void write(uint16_t); - void write(uint32_t); - void write(int8_t); - void write(int16_t); - void write(int32_t); + template + requires (sizeof...(Args) > 0 && (... && std::integral)) + void write(Args... args) + { + m_writer.write(args...); + } + + template + void writeOpcode(typename Traits::OpcodeTraits::OpcodeID, Ops...); protected: void reclaimFreeRegisters(); diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBaseInlines.h b/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBaseInlines.h index c8a6f45c44b4..d136092623c2 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBaseInlines.h +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGeneratorBaseInlines.h @@ -27,6 +27,7 @@ #include "BytecodeGeneratorBase.h" +#include "Fits.h" #include "RegisterID.h" #include "StackAlignment.h" @@ -123,40 +124,27 @@ void BytecodeGeneratorBase::alignWideOpcode32() } template -void BytecodeGeneratorBase::write(uint8_t b) -{ - m_writer.write(b); -} - - -template -void BytecodeGeneratorBase::write(uint16_t h) -{ - m_writer.write(h); -} - -template -void BytecodeGeneratorBase::write(uint32_t i) -{ - m_writer.write(i); -} - -template -void BytecodeGeneratorBase::write(int8_t b) -{ - m_writer.write(static_cast(b)); -} - -template -void BytecodeGeneratorBase::write(int16_t h) -{ - m_writer.write(static_cast(h)); -} - -template -void BytecodeGeneratorBase::write(int32_t i) -{ - m_writer.write(static_cast(i)); +template +void BytecodeGeneratorBase::writeOpcode(typename Traits::OpcodeTraits::OpcodeID opcodeID, Ops... ops) +{ + using OpcodeTraits = typename Traits::OpcodeTraits; + using OpcodeIDType = typename OpcodeTraits::OpcodeID; + constexpr auto opcodeIDSize = OpcodeIDWidthBySize::opcodeIDSize; + if constexpr (size == OpcodeSize::Wide16) { + m_writer.write( + Fits::convert(OpcodeTraits::wide16), + Fits::convert(opcodeID), + Fits::convert(ops)...); + } else if constexpr (size == OpcodeSize::Wide32) { + m_writer.write( + Fits::convert(OpcodeTraits::wide32), + Fits::convert(opcodeID), + Fits::convert(ops)...); + } else { + m_writer.write( + Fits::convert(opcodeID), + Fits::convert(ops)...); + } } template diff --git a/Source/JavaScriptCore/generator/Argument.rb b/Source/JavaScriptCore/generator/Argument.rb index fdf4e97519b6..a375eb7441e5 100644 --- a/Source/JavaScriptCore/generator/Argument.rb +++ b/Source/JavaScriptCore/generator/Argument.rb @@ -54,10 +54,6 @@ def fits_check(size) Fits::check size, @name, @type end - def fits_write(size) - Fits::write size, @name, @type - end - def assert_fits(size) "ASSERT((#{fits_check size}));" end diff --git a/Source/JavaScriptCore/generator/Fits.rb b/Source/JavaScriptCore/generator/Fits.rb index d09b0985caab..8e75e0a1e208 100644 --- a/Source/JavaScriptCore/generator/Fits.rb +++ b/Source/JavaScriptCore/generator/Fits.rb @@ -29,8 +29,4 @@ def self.convert(size, name, type) def self.check(size, name, type) "Fits<#{type.to_s}, #{size}>::check(#{name})" end - - def self.write(size, name, type) - "gen->write(#{convert(size, name, type)});" - end end diff --git a/Source/JavaScriptCore/generator/Opcode.rb b/Source/JavaScriptCore/generator/Opcode.rb index ce42dc2b81a8..f3e22b818e3c 100644 --- a/Source/JavaScriptCore/generator/Opcode.rb +++ b/Source/JavaScriptCore/generator/Opcode.rb @@ -182,6 +182,13 @@ def temps ["enum Tmps : uint8_t {"].concat(@tmps.map {|(tmp, type)| " #{tmp},"}).push(" };").join("\n") end + def emitImpl + operand_args = (@args || []).map(&:name) + operand_args << @metadata.emitter_local.name unless @metadata.empty? + all_args = ["opcodeID"] + operand_args + " gen->template writeOpcode<__size>(#{all_args.join(", ")});" + end + def emitter op_wide16 = Argument.new(wide16, opcodeIDType, 0) op_wide32 = Argument.new(wide32, opcodeIDType, 0) @@ -253,12 +260,7 @@ def emitter if (checkImpl<__size>(gen#{untyped_args}#{metadata_arg})) { if (recordOpcode) gen->recordOpcode(opcodeID); - if (__size == OpcodeSize::Wide16) - #{op_wide16.fits_write Size::Narrow} - else if (__size == OpcodeSize::Wide32) - #{op_wide32.fits_write Size::Narrow} - #{Argument.new("opcodeID", opcodeIDType, 0).fits_write "OpcodeIDWidthBySize<#{type_prefix}OpcodeTraits, __size>::opcodeIDSize"} -#{map_operands_with_size(" ", "__size", &:fits_write).join "\n"} +#{emitImpl} return true; } return false; From 3bb4fc93fb37c04e859fb52436cdf511729b0989 Mon Sep 17 00:00:00 2001 From: Keith Miller Date: Fri, 15 May 2026 10:23:36 -0700 Subject: [PATCH 087/424] Remove JSHeapInt32/Double https://bugs.webkit.org/show_bug.cgi?id=314894 rdar://177169375 Reviewed by Justin Michaud. These headers were for a feature that's unlikely to happen at this point. Let's remove the headers for now and we can always restore them in the future if needed. * Source/JavaScriptCore/CMakeLists.txt: * Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj: * Source/JavaScriptCore/Sources.txt: * Source/JavaScriptCore/jit/JITOpcodes.cpp: (JSC::JIT::compileOpStrictEqJump): * Source/JavaScriptCore/runtime/JSCJSValue.h: * Source/JavaScriptCore/runtime/JSHeapDouble.cpp: Removed. * Source/JavaScriptCore/runtime/JSHeapDouble.h: Removed. * Source/JavaScriptCore/runtime/JSHeapInt32.cpp: Removed. * Source/JavaScriptCore/runtime/JSHeapInt32.h: Removed. * Source/JavaScriptCore/runtime/JSType.cpp: (WTF::printInternal): * Source/JavaScriptCore/runtime/JSType.h: Canonical link: https://commits.webkit.org/313313@main --- Source/JavaScriptCore/CMakeLists.txt | 2 - .../JavaScriptCore.xcodeproj/project.pbxproj | 12 ------ Source/JavaScriptCore/Sources.txt | 2 - Source/JavaScriptCore/jit/JITOpcodes.cpp | 4 +- Source/JavaScriptCore/runtime/JSCJSValue.h | 2 - .../JavaScriptCore/runtime/JSHeapDouble.cpp | 37 ------------------- Source/JavaScriptCore/runtime/JSHeapDouble.h | 34 ----------------- Source/JavaScriptCore/runtime/JSHeapInt32.cpp | 35 ------------------ Source/JavaScriptCore/runtime/JSHeapInt32.h | 35 ------------------ Source/JavaScriptCore/runtime/JSType.cpp | 2 - Source/JavaScriptCore/runtime/JSType.h | 4 +- 11 files changed, 3 insertions(+), 166 deletions(-) delete mode 100644 Source/JavaScriptCore/runtime/JSHeapDouble.cpp delete mode 100644 Source/JavaScriptCore/runtime/JSHeapDouble.h delete mode 100644 Source/JavaScriptCore/runtime/JSHeapInt32.cpp delete mode 100644 Source/JavaScriptCore/runtime/JSHeapInt32.h diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt index 744dca0eb4cf..9debdf58febc 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt @@ -1208,8 +1208,6 @@ set(JavaScriptCore_PRIVATE_FRAMEWORK_HEADERS runtime/JSDestructibleObject.h runtime/JSDestructibleObjectHeapCellType.h runtime/JSEmbedderArrayLike.h - runtime/JSHeapDouble.h - runtime/JSHeapInt32.h runtime/JSExportMacros.h runtime/JSFloat16Array.h runtime/JSFloat32Array.h diff --git a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj index 01ac908849ed..7d34156d8384 100644 --- a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj +++ b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj @@ -989,8 +989,6 @@ 451539B912DC994500EF7AC4 /* Yarr.h in Headers */ = {isa = PBXBuildFile; fileRef = 451539B812DC994500EF7AC4 /* Yarr.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4615E46B2B5849F4001D4D53 /* WasmBBQJIT64.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4615E4682B5833FB001D4D53 /* WasmBBQJIT64.cpp */; }; 473DA4A4764C45FE871B0485 /* DefinePropertyAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 169948EDE68D4054B01EF797 /* DefinePropertyAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 4B1BFA042E9D769100C98300 /* JSHeapDouble.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B1BF9FE2E9D769100C98300 /* JSHeapDouble.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 4B1BFA052E9D769100C98300 /* JSHeapInt32.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B1BFA002E9D769100C98300 /* JSHeapInt32.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4B1F22F62900BFC700CB5E66 /* Width.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BBA4CD428FF5FE5003EBFC4 /* Width.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4B46940328984FA800512FDF /* MacroAssemblerARM64.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FEB137561BB11EEE00CD5100 /* MacroAssemblerARM64.cpp */; }; 4B46940428984FEE00512FDF /* MacroAssemblerX86_64.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A7A4AE0717973B26005612B1 /* MacroAssemblerX86_64.cpp */; }; @@ -4320,10 +4318,6 @@ 45E12D8806A49B0F00E9DF84 /* jsc.cpp */ = {isa = PBXFileReference; fileEncoding = 30; indentWidth = 4; lastKnownFileType = sourcecode.cpp.cpp; path = jsc.cpp; sourceTree = ""; tabWidth = 4; }; 4615E4662B5833FB001D4D53 /* WasmBBQJIT64.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WasmBBQJIT64.h; sourceTree = ""; }; 4615E4682B5833FB001D4D53 /* WasmBBQJIT64.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WasmBBQJIT64.cpp; sourceTree = ""; }; - 4B1BF9FE2E9D769100C98300 /* JSHeapDouble.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSHeapDouble.h; sourceTree = ""; }; - 4B1BF9FF2E9D769100C98300 /* JSHeapDouble.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = JSHeapDouble.cpp; sourceTree = ""; }; - 4B1BFA002E9D769100C98300 /* JSHeapInt32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSHeapInt32.h; sourceTree = ""; }; - 4B1BFA012E9D769100C98300 /* JSHeapInt32.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = JSHeapInt32.cpp; sourceTree = ""; }; 4B78E098294427D2003C6682 /* B3SIMDValue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = B3SIMDValue.cpp; path = b3/B3SIMDValue.cpp; sourceTree = ""; }; 4B78E099294427D2003C6682 /* B3Const128Value.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = B3Const128Value.h; path = b3/B3Const128Value.h; sourceTree = ""; }; 4B78E09A294427D2003C6682 /* B3SIMDValue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = B3SIMDValue.h; path = b3/B3SIMDValue.h; sourceTree = ""; }; @@ -9073,10 +9067,6 @@ 862553CE16136AA5009F17D0 /* JSGlobalProxy.cpp */, 862553CF16136AA5009F17D0 /* JSGlobalProxy.h */, 276B390A2A71D2B400252F4E /* JSGlobalProxyInlines.h */, - 4B1BF9FF2E9D769100C98300 /* JSHeapDouble.cpp */, - 4B1BF9FE2E9D769100C98300 /* JSHeapDouble.h */, - 4B1BFA012E9D769100C98300 /* JSHeapInt32.cpp */, - 4B1BFA002E9D769100C98300 /* JSHeapInt32.h */, 0F2B66CA17B6B5AB00A7AE3F /* JSInt16Array.h */, 0F2B66CB17B6B5AB00A7AE3F /* JSInt32Array.h */, 0F2B66C917B6B5AB00A7AE3F /* JSInt8Array.h */, @@ -12154,9 +12144,7 @@ A50E4B6418809DD50068A46D /* JSGlobalObjectRuntimeAgent.h in Headers */, 862553D216136E1A009F17D0 /* JSGlobalProxy.h in Headers */, 27C241492A71F29000FCDA68 /* JSGlobalProxyInlines.h in Headers */, - 4B1BFA042E9D769100C98300 /* JSHeapDouble.h in Headers */, 0F0CAEFC1EC4DA6B00970D12 /* JSHeapFinalizerPrivate.h in Headers */, - 4B1BFA052E9D769100C98300 /* JSHeapInt32.h in Headers */, A513E5C0185BFACC007E95AD /* JSInjectedScriptHost.h in Headers */, A513E5C2185BFACC007E95AD /* JSInjectedScriptHostPrototype.h in Headers */, 0F2B66F817B6B5AB00A7AE3F /* JSInt16Array.h in Headers */, diff --git a/Source/JavaScriptCore/Sources.txt b/Source/JavaScriptCore/Sources.txt index f0e0629155ad..923585b9d495 100644 --- a/Source/JavaScriptCore/Sources.txt +++ b/Source/JavaScriptCore/Sources.txt @@ -913,7 +913,6 @@ runtime/JSBigInt.cpp runtime/JSBoundFunction.cpp runtime/JSCBytecodeCacheVersion.cpp runtime/JSCConfig.cpp -runtime/JSHeapDouble.cpp runtime/JSCJSValue.cpp runtime/JSCPtrTag.cpp runtime/JSCallee.cpp @@ -938,7 +937,6 @@ runtime/JSGlobalObjectDebuggable.cpp runtime/JSGlobalObjectFunctions.cpp runtime/JSGlobalProxy.cpp runtime/JSCellButterfly.cpp -runtime/JSHeapInt32.cpp runtime/JSIterator.cpp runtime/JSIteratorConstructor.cpp runtime/JSIteratorHelper.cpp diff --git a/Source/JavaScriptCore/jit/JITOpcodes.cpp b/Source/JavaScriptCore/jit/JITOpcodes.cpp index 89df8c642816..ba79188fed57 100644 --- a/Source/JavaScriptCore/jit/JITOpcodes.cpp +++ b/Source/JavaScriptCore/jit/JITOpcodes.cpp @@ -1051,8 +1051,8 @@ void JIT::compileOpStrictEqJump(const JSInstruction* currentInstruction) else notTaken.append(branch64(Equal, regT1, regT0)); - // Pointers differ. Cell comparison is complicated only when they are Strings / HeapBigInts / - // HeapDoubles / HeapInt32s. If either cell is something else, the pointer compare answers correctly. + // Pointers differ. Cell comparison is complicated only when they are Strings / HeapBigInts. + // If either cell is something else, the pointer compare answers correctly. if constexpr (std::same_as) { notTaken.append(branch8(Above, Address(regT0, JSCell::typeInfoTypeOffset()), TrustedImm32(LastValueCompareCellType))); notTaken.append(branch8(Above, Address(regT1, JSCell::typeInfoTypeOffset()), TrustedImm32(LastValueCompareCellType))); diff --git a/Source/JavaScriptCore/runtime/JSCJSValue.h b/Source/JavaScriptCore/runtime/JSCJSValue.h index df5bf85dee70..468d6c34ae27 100644 --- a/Source/JavaScriptCore/runtime/JSCJSValue.h +++ b/Source/JavaScriptCore/runtime/JSCJSValue.h @@ -39,8 +39,6 @@ namespace JSC { class AssemblyHelpers; class DeletePropertySlot; class JSBigInt; -class JSHeapDouble; -class JSHeapInt32; class CallFrame; class JSCell; class JSValueSource; diff --git a/Source/JavaScriptCore/runtime/JSHeapDouble.cpp b/Source/JavaScriptCore/runtime/JSHeapDouble.cpp deleted file mode 100644 index 0dd1865d5d59..000000000000 --- a/Source/JavaScriptCore/runtime/JSHeapDouble.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2025 Igalia S.L. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#include "config.h" -#include "JSHeapDouble.h" - -#include "JSCJSValueInlines.h" -#include "JSObjectInlines.h" - -#include - -namespace JSC { - -} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/JSHeapDouble.h b/Source/JavaScriptCore/runtime/JSHeapDouble.h deleted file mode 100644 index cb5194c177b7..000000000000 --- a/Source/JavaScriptCore/runtime/JSHeapDouble.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2025 Igalia S.L. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include -#include -#include - -namespace JSC { - -} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/JSHeapInt32.cpp b/Source/JavaScriptCore/runtime/JSHeapInt32.cpp deleted file mode 100644 index f92f48ae7751..000000000000 --- a/Source/JavaScriptCore/runtime/JSHeapInt32.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2025 Igalia S.L. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#include "config.h" -#include "JSHeapInt32.h" - -#include "JSCJSValueInlines.h" -#include "JSObjectInlines.h" - -namespace JSC { - -} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/JSHeapInt32.h b/Source/JavaScriptCore/runtime/JSHeapInt32.h deleted file mode 100644 index a563405ac9ec..000000000000 --- a/Source/JavaScriptCore/runtime/JSHeapInt32.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2025 Igalia S.L. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include -#include -#include -#include - -namespace JSC { - -} // namespace JSC diff --git a/Source/JavaScriptCore/runtime/JSType.cpp b/Source/JavaScriptCore/runtime/JSType.cpp index 485bad3ff419..cf0d9079cf16 100644 --- a/Source/JavaScriptCore/runtime/JSType.cpp +++ b/Source/JavaScriptCore/runtime/JSType.cpp @@ -45,8 +45,6 @@ void printInternal(PrintStream& out, JSC::JSType type) CASE(StringType) CASE(SymbolType) CASE(HeapBigIntType) - CASE(HeapDoubleType) - CASE(HeapInt32Type) CASE(CustomGetterSetterType) CASE(APIValueWrapperType) CASE(NativeExecutableType) diff --git a/Source/JavaScriptCore/runtime/JSType.h b/Source/JavaScriptCore/runtime/JSType.h index adfdec1ef40c..60a517f45cf7 100644 --- a/Source/JavaScriptCore/runtime/JSType.h +++ b/Source/JavaScriptCore/runtime/JSType.h @@ -36,8 +36,6 @@ namespace JSC { /* (e.g. String value comparison). Keep in sync with LastValueCompareCellType. */ \ macro(StringType, SpecString) \ macro(HeapBigIntType, SpecHeapBigInt) \ - macro(HeapDoubleType, SpecCellOther) \ - macro(HeapInt32Type, SpecCellOther) \ \ macro(SymbolType, SpecSymbol) \ \ @@ -172,7 +170,7 @@ enum JSType : uint8_t { static constexpr uint8_t EmbedderArrayLikeType = 0b11101101; -static constexpr uint32_t LastValueCompareCellType = HeapInt32Type; +static constexpr uint32_t LastValueCompareCellType = HeapBigIntType; static constexpr uint32_t FirstTypedArrayType = Int8ArrayType; static constexpr uint32_t LastTypedArrayType = DataViewType; From 56a1500e421596fe82ca6a345aa5a9fd03ff8c1a Mon Sep 17 00:00:00 2001 From: Per Arne Vollan Date: Fri, 15 May 2026 10:44:27 -0700 Subject: [PATCH 088/424] Only enable sandbox state flags on internal builds https://bugs.webkit.org/show_bug.cgi?id=313845 rdar://176048135 Reviewed by Brent Fulgham and Sihui Liu. Sandbox state flags require a private entitlement, so it only makes sense to enable the feature on internal builds. * Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml: * Source/WTF/wtf/PlatformHave.h: Canonical link: https://commits.webkit.org/313314@main --- Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml | 2 +- Source/WTF/wtf/PlatformHave.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml index fa2b99713b58..0e74729d45df 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml @@ -901,7 +901,7 @@ BlockIOKitInWebContentSandbox: humanReadableName: "IOKit blocking in the WebContent sandbox" humanReadableDescription: "Block IOKit access in the WebContent sandbox" webcoreBinding: none - condition: HAVE(SANDBOX_STATE_FLAGS) + condition: PLATFORM(COCOA) exposed: [ WebKit ] defaultValue: WebKit: diff --git a/Source/WTF/wtf/PlatformHave.h b/Source/WTF/wtf/PlatformHave.h index 377802f7aa50..7cb116369418 100644 --- a/Source/WTF/wtf/PlatformHave.h +++ b/Source/WTF/wtf/PlatformHave.h @@ -1003,8 +1003,7 @@ #define HAVE_WINDOW_CAPTURE 1 #endif -#if PLATFORM(MAC) || PLATFORM(IOS) || PLATFORM(MACCATALYST) || PLATFORM(VISION) \ - || PLATFORM(WATCHOS) || PLATFORM(APPLETV) +#if PLATFORM(COCOA) && __has_include() && !PLATFORM(IOS_FAMILY_SIMULATOR) #define HAVE_SANDBOX_STATE_FLAGS 1 #endif From 1bde266ec80f039c96c565e8399ced7dabc67a47 Mon Sep 17 00:00:00 2001 From: Per Arne Vollan Date: Fri, 15 May 2026 11:04:38 -0700 Subject: [PATCH 089/424] The WebProcess message SwitchFromStaticFontRegistryToUserFontRegistry is not required on recent macOS versions https://bugs.webkit.org/show_bug.cgi?id=314827 rdar://177083656 Reviewed by Sihui Liu. After removing XPC and Mach sandbox extensions for the WebContent process, we can disable this message. * Source/WebKit/Shared/WebPageCreationParameters.h: * Source/WebKit/Shared/WebPageCreationParameters.serialization.in: * Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm: (-[WKWebView _switchFromStaticFontRegistryToUserFontRegistry]): * Source/WebKit/UIProcess/WebPageProxy.h: * Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm: (WebKit::WebPageProxy::switchFromStaticFontRegistryToUserFontRegistry): * Source/WebKit/UIProcess/Cocoa/WebProcessProxyCocoa.mm: * Source/WebKit/UIProcess/WebProcessProxy.h: * Source/WebKit/WebProcess/WebPage/WebPage.cpp: (WebKit::m_allowsImmersiveEnvironments): * Source/WebKit/WebProcess/WebProcess.h: * Source/WebKit/WebProcess/WebProcess.messages.in: * Source/WebKit/WebProcess/cocoa/WebProcessCocoa.mm: (WebKit::WebProcess::accessibilityFocusedUIElement): * Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm: (-[WKWebView _switchFromStaticFontRegistryToUserFontRegistry]): * Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm: (WebKit::WebPageProxy::switchFromStaticFontRegistryToUserFontRegistry): * Source/WebKit/UIProcess/WebPageProxy.h: Canonical link: https://commits.webkit.org/313315@main --- Source/WebKit/Shared/WebPageCreationParameters.h | 2 +- .../WebKit/Shared/WebPageCreationParameters.serialization.in | 2 +- Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm | 2 ++ Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm | 2 ++ Source/WebKit/UIProcess/Cocoa/WebProcessProxyCocoa.mm | 2 ++ Source/WebKit/UIProcess/WebPageProxy.h | 3 ++- Source/WebKit/UIProcess/WebProcessProxy.h | 2 ++ Source/WebKit/WebProcess/WebPage/WebPage.cpp | 2 +- Source/WebKit/WebProcess/WebProcess.h | 3 ++- Source/WebKit/WebProcess/WebProcess.messages.in | 3 ++- Source/WebKit/WebProcess/cocoa/WebProcessCocoa.mm | 2 ++ 11 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Source/WebKit/Shared/WebPageCreationParameters.h b/Source/WebKit/Shared/WebPageCreationParameters.h index 3a96e992cfef..77cc0f3c3dc7 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.h +++ b/Source/WebKit/Shared/WebPageCreationParameters.h @@ -235,7 +235,7 @@ struct WebPageCreationParameters { #if ENABLE(TILED_CA_DRAWING_AREA) SandboxExtension::Handle renderServerMachExtensionHandle { }; #endif -#if HAVE(STATIC_FONT_REGISTRY) +#if HAVE(STATIC_FONT_REGISTRY) && !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) Vector fontMachExtensionHandles { }; #endif #if HAVE(APP_ACCENT_COLORS) diff --git a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in index 707ff0023e2b..638aec18be33 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in +++ b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in @@ -150,7 +150,7 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; #if ENABLE(TILED_CA_DRAWING_AREA) WebKit::SandboxExtensionHandle renderServerMachExtensionHandle; #endif -#if HAVE(STATIC_FONT_REGISTRY) +#if HAVE(STATIC_FONT_REGISTRY) && !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) Vector fontMachExtensionHandles; #endif #if HAVE(APP_ACCENT_COLORS) diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm index 05d1da7a75b3..5d5d62cc7585 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm +++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm @@ -5066,8 +5066,10 @@ - (void)_disableURLSchemeCheckInDataDetectors - (void)_switchFromStaticFontRegistryToUserFontRegistry { THROW_IF_SUSPENDED; +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) if (_page) _page->switchFromStaticFontRegistryToUserFontRegistry(); +#endif } - (void)_didLoadAppInitiatedRequest:(void (^)(BOOL result))completionHandler diff --git a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm index 3d871bd3fbb3..8a15e065a974 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm @@ -1065,11 +1065,13 @@ static bool NODELETE exceedsRenderTreeSizeSizeThreshold(uint64_t thresholdSize, protect(legacyMainFrameProcess())->send(Messages::WebProcess::DisableURLSchemeCheckInDataDetectors(), 0); } +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) void WebPageProxy::switchFromStaticFontRegistryToUserFontRegistry() { if (auto handles = protect(legacyMainFrameProcess())->fontdMachExtensionHandles()) protect(legacyMainFrameProcess())->send(Messages::WebProcess::SwitchFromStaticFontRegistryToUserFontRegistry(WTF::move(*handles)), 0); } +#endif // !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) NSDictionary *WebPageProxy::contentsOfUserInterfaceItem(NSString *userInterfaceItem) { diff --git a/Source/WebKit/UIProcess/Cocoa/WebProcessProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebProcessProxyCocoa.mm index a64b8fb48c1d..c443d14db556 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebProcessProxyCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebProcessProxyCocoa.mm @@ -275,12 +275,14 @@ return protect(connection())->getAuditToken(); } +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) std::optional> WebProcessProxy::fontdMachExtensionHandles() { if (std::exchange(m_sentFontdMachExtensionHandles, true)) return std::nullopt; return SandboxExtension::createHandlesForMachLookup({ "com.apple.fonts"_s }, auditToken(), SandboxExtension::MachBootstrapOptions::EnableMachBootstrap); } +#endif // !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) #if USE(APPLE_INTERNAL_SDK) && __has_include() #import diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h index c16f1cc7f6cb..763719f52b9f 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h @@ -2418,8 +2418,9 @@ class WebPageProxy final : public API::ObjectImpl, publ void grantAccessToAssetServices(); void revokeAccessToAssetServices(); +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) void switchFromStaticFontRegistryToUserFontRegistry(); - +#endif void disableURLSchemeCheckInDataDetectors() const; void setIsTakingSnapshotsForApplicationSuspension(bool); diff --git a/Source/WebKit/UIProcess/WebProcessProxy.h b/Source/WebKit/UIProcess/WebProcessProxy.h index a5925b3553e3..6e0411f7cc90 100644 --- a/Source/WebKit/UIProcess/WebProcessProxy.h +++ b/Source/WebKit/UIProcess/WebProcessProxy.h @@ -543,8 +543,10 @@ class WebProcessProxy final : public AuxiliaryProcessProxy { #if PLATFORM(COCOA) std::optional auditToken() const; +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) std::optional> fontdMachExtensionHandles(); #endif +#endif // PLATFORM(COCOA) bool isConnectedToHardwareConsole() const { return m_isConnectedToHardwareConsole; } diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp index 7c29ce6b9bf7..41b7742f7220 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp @@ -895,7 +895,7 @@ WebPage::WebPage(PageIdentifier pageID, WebPageCreationParameters&& parameters) } #endif -#if HAVE(STATIC_FONT_REGISTRY) +#if HAVE(STATIC_FONT_REGISTRY) && !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) if (parameters.fontMachExtensionHandles.size()) WebProcess::singleton().switchFromStaticFontRegistryToUserFontRegistry(WTF::move(parameters.fontMachExtensionHandles)); #endif diff --git a/Source/WebKit/WebProcess/WebProcess.h b/Source/WebKit/WebProcess/WebProcess.h index 8bc3b12ca0b4..29f56b181485 100644 --- a/Source/WebKit/WebProcess/WebProcess.h +++ b/Source/WebKit/WebProcess/WebProcess.h @@ -460,8 +460,9 @@ class WebProcess final : public AuxiliaryProcess { void grantAccessToAssetServices(Vector&& assetServicesHandles); void revokeAccessToAssetServices(); +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) void switchFromStaticFontRegistryToUserFontRegistry(Vector&& fontMachExtensionHandles); - +#endif void disableURLSchemeCheckInDataDetectors() const; #if PLATFORM(MAC) diff --git a/Source/WebKit/WebProcess/WebProcess.messages.in b/Source/WebKit/WebProcess/WebProcess.messages.in index a682232868fe..deb12199ceaa 100644 --- a/Source/WebKit/WebProcess/WebProcess.messages.in +++ b/Source/WebKit/WebProcess/WebProcess.messages.in @@ -191,8 +191,9 @@ messages -> WebProcess : AuxiliaryProcess WantsAsyncDispatchMessage { GrantAccessToAssetServices(Vector assetServicesHandles) RevokeAccessToAssetServices() +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) SwitchFromStaticFontRegistryToUserFontRegistry(Vector fontMachExtensionHandles) - +#endif #if PLATFORM(COCOA) DisableURLSchemeCheckInDataDetectors() UnblockServicesRequiredByAccessibility(Vector handleArray) diff --git a/Source/WebKit/WebProcess/cocoa/WebProcessCocoa.mm b/Source/WebKit/WebProcess/cocoa/WebProcessCocoa.mm index 1a824066d4fd..85331a8b73fd 100644 --- a/Source/WebKit/WebProcess/cocoa/WebProcessCocoa.mm +++ b/Source/WebKit/WebProcess/cocoa/WebProcessCocoa.mm @@ -1505,6 +1505,7 @@ static float currentBacklightLevel() #endif } +#if !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) void WebProcess::switchFromStaticFontRegistryToUserFontRegistry(Vector&& fontMachExtensionHandles) { SandboxExtension::consumePermanently(fontMachExtensionHandles); @@ -1512,6 +1513,7 @@ static float currentBacklightLevel() CTFontManagerEnableAllUserFonts(true); #endif } +#endif // !ENABLE(REMOVE_XPC_AND_MACH_SANDBOX_EXTENSIONS_IN_WEBCONTENT) void WebProcess::setScreenProperties(const WebCore::ScreenProperties& properties) { From 42cae1dc020ad35cbed9c56a8fd7a0c06f4904b7 Mon Sep 17 00:00:00 2001 From: Simon Fraser Date: Fri, 15 May 2026 11:11:15 -0700 Subject: [PATCH 090/424] REGRESSION (296844@main): drop-shadow and translation transforms render incorrectly clipped https://bugs.webkit.org/show_bug.cgi?id=313665 rdar://175905543 Reviewed by Brent Fulgham. In setupFilters, calculateLayerBounds was called with { PreserveAncestorFlags }. This flag causes descendant layers to inherit the parent's empty flags, which crucially omits IncludeSelfTransform. Without that flag, CSS transforms on descendant layers (like the inner circle's transform) are ignored when computing the filter source image bounds. This makes the filter source image buffer positioned incorrectly, clipping parts of the painted content. Fix by passing empty flags. Tests: imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001.html imported/w3c/web-platform-tests/css/filter-effects/reference/filter-overflow-transformed-child-001-ref.html * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001-expected.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/reference/filter-overflow-transformed-child-001-ref.html: Added. * Source/WebCore/rendering/RenderLayer.cpp: (WebCore::RenderLayer::setupFilters): Canonical link: https://commits.webkit.org/313316@main --- ...ter-overflow-transformed-child-001-expected.html | 9 +++++++++ .../filter-overflow-transformed-child-001.html | 13 +++++++++++++ .../filter-overflow-transformed-child-001-ref.html | 9 +++++++++ Source/WebCore/rendering/RenderLayer.cpp | 2 +- 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/reference/filter-overflow-transformed-child-001-ref.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001-expected.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001-expected.html new file mode 100644 index 000000000000..f3db078c4fd8 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001-expected.html @@ -0,0 +1,9 @@ + +CSS Filters: filter should not clip transformed overflow children (reference) + +
+
+
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001.html new file mode 100644 index 000000000000..fbe9aeb79fa5 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/filter-overflow-transformed-child-001.html @@ -0,0 +1,13 @@ + +CSS Filters: filter should not clip transformed overflow children + + + + +
+
+
+
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/reference/filter-overflow-transformed-child-001-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/reference/filter-overflow-transformed-child-001-ref.html new file mode 100644 index 000000000000..f3db078c4fd8 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/filter-effects/reference/filter-overflow-transformed-child-001-ref.html @@ -0,0 +1,9 @@ + +CSS Filters: filter should not clip transformed overflow children (reference) + +
+
+
+
diff --git a/Source/WebCore/rendering/RenderLayer.cpp b/Source/WebCore/rendering/RenderLayer.cpp index c4453b5759ba..ca9673572e29 100644 --- a/Source/WebCore/rendering/RenderLayer.cpp +++ b/Source/WebCore/rendering/RenderLayer.cpp @@ -3627,7 +3627,7 @@ GraphicsContext* RenderLayer::setupFilters(GraphicsContext& destinationContext, LayoutRect filterRepaintRect = paintingFilters->dirtySourceRect(); filterRepaintRect.move(offsetFromRoot); - auto rootRelativeBounds = calculateLayerBounds(paintingInfo.rootLayer, offsetFromRoot, { RenderLayer::PreserveAncestorFlags }); + auto rootRelativeBounds = calculateLayerBounds(paintingInfo.rootLayer, offsetFromRoot, { }); // When the filter is applied via a transparency layer directly on the destination context (e.g. CG drop-shadow), // the switcher doesn't consult applyFilters's clipToRect path, so the ancestor border-radius clip would be lost. From 537da1b74f0acf8b208e5f8939aa1ef6b9673a16 Mon Sep 17 00:00:00 2001 From: Cole Carley Date: Fri, 15 May 2026 11:15:51 -0700 Subject: [PATCH 091/424] REGRESSION(309054@main): Select menu list text align padding is wrong https://bugs.webkit.org/show_bug.cgi?id=314764 rdar://176357057 Reviewed by Alan Baradlay. 309054@main computed padding for select menulist buttons was based on text-alignment, placing it on the start side when text pointed toward the chevron (e.g. text-align:right in LTR). This resulted in the text overlapping with the chevron. The chevron is always drawn on the logical end regardless of text-align, so the internal padding should always be placed on the logical end as well. Test: fast/forms/select-menulist-text-align-chevron-padding.html * LayoutTests/fast/forms/select-menulist-text-align-chevron-padding-expected.txt: Added. * LayoutTests/fast/forms/select-menulist-text-align-chevron-padding.html: Added. * LayoutTests/platform/ios/fast/forms/listbox-bidi-align-expected.txt: * Source/WebCore/rendering/ios/RenderThemeIOS.mm: (WebCore::RenderThemeIOS::platformPopupInternalPaddingBox const): Canonical link: https://commits.webkit.org/313317@main --- ...st-text-align-chevron-padding-expected.txt | 13 ++++++++ ...t-menulist-text-align-chevron-padding.html | 29 ++++++++++++++++++ ...st-text-align-chevron-padding-expected.png | Bin 0 -> 44278 bytes .../international/bidi-menulist-expected.png | Bin 0 -> 109978 bytes .../forms/listbox-bidi-align-expected.txt | 24 +++++++-------- .../international/bidi-menulist-expected.txt | 6 ++-- .../WebCore/rendering/ios/RenderThemeIOS.mm | 16 +--------- 7 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding-expected.txt create mode 100644 LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding.html create mode 100644 LayoutTests/platform/ios-simulator/fast/forms/ios/select-menulist-text-align-chevron-padding-expected.png create mode 100644 LayoutTests/platform/ios-simulator/fast/text/international/bidi-menulist-expected.png diff --git a/LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding-expected.txt b/LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding-expected.txt new file mode 100644 index 000000000000..afbb53ac0fbc --- /dev/null +++ b/LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding-expected.txt @@ -0,0 +1,13 @@ +A select with text-align pointing toward the chevron should still have internal padding on the logical end, preventing text from overlapping with the chevron. + +On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE". + + +PASS ltrStyle.paddingRight is not "0px" +PASS ltrStyle.paddingLeft is "0px" +PASS rtlStyle.paddingLeft is not "0px" +PASS rtlStyle.paddingRight is "0px" +PASS successfullyParsed is true + +TEST COMPLETE + diff --git a/LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding.html b/LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding.html new file mode 100644 index 000000000000..38a3898bbc2a --- /dev/null +++ b/LayoutTests/fast/forms/ios/select-menulist-text-align-chevron-padding.html @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/LayoutTests/platform/ios-simulator/fast/forms/ios/select-menulist-text-align-chevron-padding-expected.png b/LayoutTests/platform/ios-simulator/fast/forms/ios/select-menulist-text-align-chevron-padding-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a9867fc6de4bd36cecf36d65a18501b86724a904 GIT binary patch literal 44278 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

_d(it_S8rvgs!LOoBq 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::trunc(padding + Style::evaluate(style.usedBorderTopWidth(), Style::ZoomNeeded { }))) / style.usedZoom() }; // Return in horizontal-tb LTR; popupInternalPaddingBox() handles conversion. - bool padStart = [&] { - switch (style.textAlign()) { - case Style::TextAlign::Start: - return false; - case Style::TextAlign::End: - return true; - case Style::TextAlign::Right: - return style.writingMode().isBidiLTR(); - default: - return style.writingMode().isBidiRTL(); - } - }(); - - if (padStart) - return { 0_css_px, 0_css_px, 0_css_px, value }; + // The chevron is always on the logical end, so pad the end to prevent text from overlapping with it. return { 0_css_px, value, 0_css_px, 0_css_px }; } return { 0_css_px }; From 04f8f8e8059d44ce8129b702dd578c7743e772bc Mon Sep 17 00:00:00 2001 From: BJ Burg Date: Fri, 15 May 2026 11:21:45 -0700 Subject: [PATCH 092/424] REGRESSION(313207@main): Web Inspector: ProxyingNetworkAgent leaks IPC::MessageReceiver after cross-origin iframe process swap https://bugs.webkit.org/show_bug.cgi?id=314808 rdar://177056433 Reviewed by Abrar Rahman Protyasha. Two distinct teardown bugs surface from the same cross-origin iframe process swap path under Site Isolation. Both manifest as flaky crashes attributed to whatever non-SI test runs after a Web Inspector layout test in WKTR's run order, which is why http/tests/ssl/applepay/ApplePayButton.html was the visible victim. The first issue is that WebPageInspectorController::didCommitProvisionalFrame re-instruments the new WebContent process for network events but never removes the old process's instrumentation. The (oldProcessID, oldPageID) entry stays in ProxyingNetworkAgent::m_instrumentedProcessPageCounts and its IPC::MessageReceiver registration on the old WebProcessProxy outlives the agent. ~ProxyingNetworkAgent then asserts in ~IPC::MessageReceiver during the next WKWebView teardown. Thread the iframe's old pageID from WebFrameProxy::commitProvisionalFrame through to didCommitProvisionalFrame, and disable instrumentation for the old process before re-enabling on the new one. The second issue is that FrameInspectorTarget::connect() chooses provisionalFrame() ?: coreLocalFrame() and registers the frontend channel with that frame's inspector controller. disconnect() makes the same choice independently. Under Site Isolation those two choices can resolve to different LocalFrames -- a provisional load can start after connect, or the previously-provisional frame commits and replaces coreLocalFrame(). Disconnecting from a controller that never registered the channel asserts in FrontendRouter::disconnectFrontend. Pin a WeakPtr at connect time and disconnect from that same frame's controller. Test changes: - evaluate-in-cross-origin-iframe.html (was [ Failure Crash ]) now passes. - execution-context-from-frame-target.html (was [ Failure ]) now passes. * Source/WebKit/UIProcess/WebFrameProxy.cpp: (WebKit::WebFrameProxy::commitProvisionalFrame): * Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h: * Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp: (WebKit::WebPageInspectorController::didCommitProvisionalFrame): * Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.h: * Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.cpp: (WebKit::FrameInspectorTarget::connect): (WebKit::FrameInspectorTarget::disconnect): * LayoutTests/platform/mac-wk2/TestExpectations: Canonical link: https://commits.webkit.org/313318@main --- LayoutTests/platform/mac-wk2/TestExpectations | 2 -- .../Inspector/WebPageInspectorController.cpp | 14 +++++++------- .../Inspector/WebPageInspectorController.h | 2 +- Source/WebKit/UIProcess/WebFrameProxy.cpp | 3 ++- .../WebProcess/Inspector/FrameInspectorTarget.cpp | 11 ++++++----- .../WebProcess/Inspector/FrameInspectorTarget.h | 7 +++++++ 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/LayoutTests/platform/mac-wk2/TestExpectations b/LayoutTests/platform/mac-wk2/TestExpectations index 7e93c000dd02..ff2e5f07f769 100644 --- a/LayoutTests/platform/mac-wk2/TestExpectations +++ b/LayoutTests/platform/mac-wk2/TestExpectations @@ -2242,8 +2242,6 @@ webkit.org/b/310302 imported/w3c/web-platform-tests/uievents/mouse/mouse_boundar http/tests/site-isolation/inspector/debugger/scriptParsed-frame-target.html [ Skip ] http/tests/site-isolation/inspector/debugger/pause-in-cross-origin-iframe.html [ Skip ] http/tests/site-isolation/inspector/debugger/setBreakpoint-cross-origin-iframe.html [ Skip ] -http/tests/site-isolation/inspector/runtime/evaluate-in-cross-origin-iframe.html [ Failure Crash ] -http/tests/site-isolation/inspector/runtime/execution-context-from-frame-target.html [ Failure ] http/tests/site-isolation/inspector/runtime/executionContextCreated-frame-target.html [ Failure ] http/tests/site-isolation/inspector/page/override-setting-cross-origin-iframe.html [ Pass Failure ] webkit.org/b/311836 http/tests/site-isolation/inspector/page/set-emulated-media-cross-origin-iframe.html [ Pass Failure ] diff --git a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp index 545107153141..ed993a5f8b58 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp @@ -379,7 +379,7 @@ void WebPageInspectorController::willDestroyProvisionalFrame(const ProvisionalFr removeTarget(targetId); } -void WebPageInspectorController::didCommitProvisionalFrame(WebFrameProxy& frame, WebCore::ProcessIdentifier oldProcessID, WebCore::ProcessIdentifier newProcessID) +void WebPageInspectorController::didCommitProvisionalFrame(WebFrameProxy& frame, WebCore::ProcessIdentifier oldProcessID, std::optional oldPageID, WebCore::ProcessIdentifier newProcessID) { if (!shouldManageFrameTargets()) return; @@ -397,16 +397,16 @@ void WebPageInspectorController::didCommitProvisionalFrame(WebFrameProxy& frame, if (auto oldTarget = m_targets.take(oldTargetID)) targetAgent->targetDestroyed(protect(*oldTarget)); - // Instrument the new process for network events now that the - // frame has committed in its final process. - // FIXME: We should also disable instrumentation for the old process that was - // hosting this frame before the process swap. Currently the old (processID, pageID) - // pair retains a refcount in m_instrumentedProcessPageCounts and the IPC message - // receiver stays registered until the old process is torn down. + // Instrument the new process for network events now that the frame has + // committed in its final process. Also disable instrumentation for the + // old process; the frame no longer lives there. + RefPtr oldProcess = WebProcessProxy::processForIdentifier(oldProcessID); Ref process = frame.process(); RefPtr networkAgent = m_networkAgent; if (networkAgent && networkAgent->isEnabled()) { + if (oldProcess && oldPageID) + networkAgent->disableInstrumentationForProcess(*oldProcess, *oldPageID); if (auto pageID = frame.webPageIDInCurrentProcess()) networkAgent->enableInstrumentationForProcess(process, *pageID); } diff --git a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h index 0c5d84e91c80..ab7b7282880a 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h @@ -88,7 +88,7 @@ class WebPageInspectorController { void willDestroyFrame(const WebFrameProxy&); void didCreateProvisionalFrame(ProvisionalFrameProxy&); void willDestroyProvisionalFrame(const ProvisionalFrameProxy&); - void didCommitProvisionalFrame(WebFrameProxy&, WebCore::ProcessIdentifier oldProcessID, WebCore::ProcessIdentifier newProcessID); + void didCommitProvisionalFrame(WebFrameProxy&, WebCore::ProcessIdentifier oldProcessID, std::optional oldPageID, WebCore::ProcessIdentifier newProcessID); InspectorBrowserAgent* NODELETE enabledBrowserAgent() const; void setEnabledBrowserAgent(InspectorBrowserAgent*); diff --git a/Source/WebKit/UIProcess/WebFrameProxy.cpp b/Source/WebKit/UIProcess/WebFrameProxy.cpp index 50dfcf057ee8..c29789d4cb1d 100644 --- a/Source/WebKit/UIProcess/WebFrameProxy.cpp +++ b/Source/WebKit/UIProcess/WebFrameProxy.cpp @@ -612,13 +612,14 @@ void WebFrameProxy::commitProvisionalFrame(IPC::Connection& connection, FrameIde protect(process())->send(Messages::WebPage::LoadDidCommitInAnotherProcess(frameID, m_layerHostingContextIdentifier, nullptr), *webPageIDInCurrentProcess()); WebCore::ProcessIdentifier oldProcessID = process().coreProcessIdentifier(); + std::optional oldPageID = webPageIDInCurrentProcess(); WebCore::ProcessIdentifier newProcessID = m_provisionalFrame->process().coreProcessIdentifier(); if (RefPtr process = std::exchange(m_provisionalFrame, nullptr)->takeFrameProcess()) setProcess(process.releaseNonNull()); if (RefPtr page = m_page.get()) - page->inspectorController().didCommitProvisionalFrame(*this, oldProcessID, newProcessID); + page->inspectorController().didCommitProvisionalFrame(*this, oldProcessID, oldPageID, newProcessID); } protect(page())->didCommitLoadForFrame(connection, frameID, WTF::move(frameInfo), WTF::move(request), navigationID, WTF::move(mimeType), frameHasCustomContentProvider, frameLoadType, certificateInfo, usedLegacyTLS, privateRelayed, WTF::move(proxyName), source, containsPluginDocument, hasInsecureContent, mouseEventPolicy, WTF::move(documentSecurityPolicy), userData); diff --git a/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.cpp b/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.cpp index 0cfb7a3fcb88..0266ccb753dd 100644 --- a/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.cpp +++ b/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.cpp @@ -64,8 +64,10 @@ void FrameInspectorTarget::connect(Inspector::FrontendChannel::ConnectionType co m_channel = makeUnique(*page, identifier(), connectionType); RefPtr coreFrame = frame->provisionalFrame() ?: frame->coreLocalFrame(); - if (coreFrame) + if (coreFrame) { + m_inspectedFrame = *coreFrame; protect(coreFrame->inspectorController())->connectFrontend(*m_channel); + } } void FrameInspectorTarget::disconnect() @@ -73,11 +75,10 @@ void FrameInspectorTarget::disconnect() if (!m_channel) return; - Ref frame = m_frame.get(); - RefPtr coreFrame = frame->provisionalFrame() ?: frame->coreLocalFrame(); - if (coreFrame) - protect(coreFrame->inspectorController())->disconnectFrontend(*m_channel); + if (RefPtr inspectedFrame = m_inspectedFrame.get()) + protect(inspectedFrame->inspectorController())->disconnectFrontend(*m_channel); + m_inspectedFrame = nullptr; m_channel.reset(); } diff --git a/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.h b/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.h index 2b2820a4ded0..cc107b48bb95 100644 --- a/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.h +++ b/Source/WebKit/WebProcess/Inspector/FrameInspectorTarget.h @@ -31,8 +31,13 @@ #include #include #include +#include #include +namespace WebCore { +class LocalFrame; +} + namespace WebKit { class UIProcessForwardingFrontendChannel; @@ -59,6 +64,8 @@ class FrameInspectorTarget final : public Inspector::InspectorTarget { private: WeakRef m_frame; std::unique_ptr m_channel; + // The WebCore::LocalFrame we registered m_channel with at connect() time. + WeakPtr m_inspectedFrame; }; } // namespace WebKit From 32cef0ff9d2769529279f325e77a33cfa97675b0 Mon Sep 17 00:00:00 2001 From: Ruthvik Konda Date: Fri, 15 May 2026 11:35:07 -0700 Subject: [PATCH 093/424] [model] Adopt public RealityCoreTextureProcessing API https://bugs.webkit.org/show_bug.cgi?id=314857 rdar://176179428 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Mike Wyrzykowski. RealityCoreTextureProcessing replaced _Proto_LowLevelTextureProcessingContext_v1 with two public classes: SkyboxGenerator (equirectangular→cube conversion) and ImageBasedLightTextureGenerator (cube→IBL diffuse/specular). Adopt the new API. The old single-context type split into two purpose-specific generators. Each method is a direct 1:1 replacement. Bump the canImport guard from RealityCoreTextureProcessing version 19 to 24 across all model files, since the new types ship in RealityCoreTexture-24. No new tests (no behavior change; existing model rendering verified manually). * Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift: * Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift: * Source/WebKit/GPUProcess/graphics/Model/USDModel.swift: Canonical link: https://commits.webkit.org/313319@main --- .../graphics/Model/ModelBridge.swift | 6 ++--- .../graphics/Model/ModelIBLTextures.swift | 2 +- .../graphics/Model/ModelParameters.swift | 2 +- .../graphics/Model/ModelRenderer.swift | 2 +- .../graphics/Model/ModelUtils.swift | 2 +- .../graphics/Model/USDModel+Deformation.swift | 2 +- .../GPUProcess/graphics/Model/USDModel.swift | 22 ++++++++++--------- 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift index 31a6c337e8f1..9a538baa849d 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift @@ -24,7 +24,7 @@ import Metal import WebKit -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit @_spi(RealityCoreRendererAPI) import RealityKit import USDKit @@ -300,7 +300,7 @@ extension WKBridgeUpdateMesh { } } -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) func decodeValues(from data: Data) -> [T] where T: BitwiseCopyable { let stride = MemoryLayout.stride guard !data.isEmpty, data.count % stride == 0 else { return [] } @@ -618,7 +618,7 @@ extension WKBridgeMaterialGraph { } } -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) func toData(_ input: [T]) -> Data { // rdar://164559261 - this is needed because there is no way to represnt an NSArray of diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift index 19d8188f2dcd..d78293d06096 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import Metal import USDKit diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift index 984b3f1f4d85..190619bbd06e 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import USDKit @_spi(UsdLoaderAPI) import _USDKit_RealityKit diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift index d91c80b45a64..f7a2979be911 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import QuartzCore import USDKit diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift index 74304b79458b..268b31699dcd 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) import DirectResource import Metal diff --git a/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift b/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift index 17f02b2f4852..49e9fd016e51 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift @@ -26,7 +26,7 @@ import OSLog import WebKit import simd -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit @_spi(RealityCoreRendererAPI) import RealityKit @_spi(RealityCoreTextureProcessingAPI) import RealityCoreTextureProcessing diff --git a/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift b/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift index 69f84eed7ef4..87cf169ad1e8 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift @@ -26,10 +26,9 @@ import OSLog import WebKit import simd -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 19) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit @_spi(RealityCoreRendererAPI) import RealityKit -@_spi(RealityCoreTextureProcessingAPI) import RealityCoreTextureProcessing import USDKit @_spi(SwiftAPI) import DirectResource import RealityKit @@ -413,7 +412,9 @@ extension WKBridgeReceiver { @nonobjc fileprivate let device: any MTLDevice @nonobjc - fileprivate let textureProcessingContext: _Proto_LowLevelTextureProcessingContext_v1 + fileprivate let skyboxGenerator: SkyboxGenerator + @nonobjc + fileprivate let imageBasedLightTextureGenerator: ImageBasedLightTextureGenerator @nonobjc fileprivate let commandQueue: any MTLCommandQueue @@ -507,7 +508,8 @@ extension WKBridgeReceiver { self.renderer = configuration.renderer self.appRenderer = configuration.appRenderer self.device = configuration.device - self.textureProcessingContext = _Proto_LowLevelTextureProcessingContext_v1(device: configuration.device) + self.skyboxGenerator = SkyboxGenerator(device: configuration.device) + self.imageBasedLightTextureGenerator = ImageBasedLightTextureGenerator(device: configuration.device) self.commandQueue = configuration.commandQueue self.deformationSystem = try _Proto_LowLevelDeformationSystem_v1.make(configuration.device, configuration.commandQueue).get() let meshInstances = try configuration.renderContext.makeMeshInstanceArray(renderTargets: [configuration.renderTarget], count: 16) @@ -917,7 +919,7 @@ extension WKBridgeReceiver { fatalError("Could not make metal texture from environment asset data") } - let cubeMTLTextureDescriptor = try self.textureProcessingContext.createCubeDescriptor( + let cubeMTLTextureDescriptor = try self.skyboxGenerator.makeDescriptor( fromEquirectangular: mtlTextureEquirectangular ) // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 @@ -925,13 +927,13 @@ extension WKBridgeReceiver { let cubeMTLTexture = self.device.makeTexture(descriptor: cubeMTLTextureDescriptor)! cubeMTLTexture.__setOwnerWithIdentity(self.memoryOwner) - let diffuseMTLTextureDescriptor = try self.textureProcessingContext.createImageBasedLightDiffuseDescriptor( + let diffuseMTLTextureDescriptor = try self.imageBasedLightTextureGenerator.makeDiffuseDescriptor( fromCube: cubeMTLTexture ) let diffuseTextureDescriptor = _Proto_LowLevelTextureResource_v1.Descriptor.from(diffuseMTLTextureDescriptor) let diffuseTexture = try self.renderContext.makeTextureResource(descriptor: diffuseTextureDescriptor) - let specularMTLTextureDescriptor = try self.textureProcessingContext.createImageBasedLightSpecularDescriptor( + let specularMTLTextureDescriptor = try self.imageBasedLightTextureGenerator.makeSpecularDescriptor( fromCube: cubeMTLTexture ) let specularTextureDescriptor = _Proto_LowLevelTextureResource_v1.Descriptor.from(specularMTLTextureDescriptor) @@ -941,7 +943,7 @@ extension WKBridgeReceiver { // swift-format-ignore: NeverForceUnwrap let commandBuffer = self.commandQueue.makeCommandBuffer()! - try self.textureProcessingContext.generateCube( + try self.skyboxGenerator.generateSkybox( using: commandBuffer, fromEquirectangular: mtlTextureEquirectangular, into: cubeMTLTexture @@ -950,12 +952,12 @@ extension WKBridgeReceiver { let diffuseMTLTexture = diffuseTexture.replace(using: commandBuffer) let specularMTLTexture = specularTexture.replace(using: commandBuffer) - try self.textureProcessingContext.generateImageBasedLightDiffuse( + try self.imageBasedLightTextureGenerator.generateDiffuse( using: commandBuffer, fromSkyboxCube: cubeMTLTexture, into: diffuseMTLTexture ) - try self.textureProcessingContext.generateImageBasedLightSpecular( + try self.imageBasedLightTextureGenerator.generateSpecular( using: commandBuffer, fromSkyboxCube: cubeMTLTexture, into: specularMTLTexture From d616b38375e0a34134261fd3250a0ed5283775ad Mon Sep 17 00:00:00 2001 From: Claudio Saavedra Date: Fri, 15 May 2026 11:51:18 -0700 Subject: [PATCH 094/424] [WPE] Shut compiler warnings when building with -DENABLE_WPE_LEGACY_API=ON -DENABLE_WPE_PLATFORM=OFF https://bugs.webkit.org/show_bug.cgi?id=314896 Reviewed by Adrian Perez de Castro. * Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.cpp: (WebKit::AcceleratedSurface::SwapChain::SwapChain): Canonical link: https://commits.webkit.org/313320@main --- .../WebPage/CoordinatedGraphics/AcceleratedSurface.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.cpp b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.cpp index 3c0576b5d21d..ed5fffec6993 100644 --- a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.cpp +++ b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.cpp @@ -626,6 +626,12 @@ AcceleratedSurface::SwapChain::SwapChain(AcceleratedSurface& surface) setupBufferFormat(); break; #endif +#else + case PlatformDisplay::Type::Surfaceless: +#if USE(GBM) + case PlatformDisplay::Type::GBM: +#endif + break; #endif // PLATFORM(GTK) || ENABLE(WPE_PLATFORM) #if USE(WPE_RENDERER) case PlatformDisplay::Type::WPE: From 4257db292740bc765680cc5cccae7d2e397ca213 Mon Sep 17 00:00:00 2001 From: Abrar Rahman Protyasha Date: Fri, 15 May 2026 11:53:00 -0700 Subject: [PATCH 095/424] Add isDHTMLDraggable and isColorInput flags to InteractionInformationAtPosition https://bugs.webkit.org/show_bug.cgi?id=314868 rdar://177130823 Reviewed by Richard Robinson. This patch teaches InteractionInformationAtPosition a better view of the signals that DragController consults when deciding what a draggable element is. This will be requisite in a forthcoming drag-and-drop patch. Namely, we add two flags: - isDHTMLDraggable: any node along the hit-test parent chain with style.userDrag() == UserDrag::Element. - isColorInput: a hit-tested HTMLInputElement that is an enabled color control. Drive-by: Also address some SaferCPP errors raised in an earlier EWS round of this patch. * Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.h: * Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.mm: (WebKit::InteractionInformationAtPosition::InteractionInformationAtPosition): * Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.serialization.in: * Source/WebKit/WebProcess/WebPage/Cocoa/PositionInformationForWebPage.mm: (WebKit::selectionPositionInformation): Canonical link: https://commits.webkit.org/313321@main --- .../Cocoa/InteractionInformationAtPosition.h | 4 ++++ .../Cocoa/InteractionInformationAtPosition.mm | 4 ++++ ...ctionInformationAtPosition.serialization.in | 2 ++ .../Cocoa/PositionInformationForWebPage.mm | 18 +++++++++++++----- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.h b/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.h index 11323f1c467e..7c8ddf8fd894 100644 --- a/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.h +++ b/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.h @@ -68,6 +68,8 @@ struct InteractionInformationAtPosition { Selectability&&, bool isSelected, bool prefersDraggingOverTextSelection, + bool isDHTMLDraggable, + bool isColorInput, bool isNearMarkedText, #if PLATFORM(IOS_FAMILY) bool touchCalloutEnabled, @@ -134,6 +136,8 @@ struct InteractionInformationAtPosition { bool isSelected { false }; bool prefersDraggingOverTextSelection { false }; + bool isDHTMLDraggable { false }; + bool isColorInput { false }; bool isNearMarkedText { false }; #if PLATFORM(IOS_FAMILY) bool touchCalloutEnabled { true }; diff --git a/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.mm b/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.mm index c39370d0fd62..364fccfb26d7 100644 --- a/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.mm +++ b/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.mm @@ -41,6 +41,8 @@ Selectability&& selectability, bool isSelected, bool prefersDraggingOverTextSelection, + bool isDHTMLDraggable, + bool isColorInput, bool isNearMarkedText, #if PLATFORM(IOS_FAMILY) bool touchCalloutEnabled, @@ -103,6 +105,8 @@ , selectability(selectability) , isSelected(isSelected) , prefersDraggingOverTextSelection(prefersDraggingOverTextSelection) + , isDHTMLDraggable(isDHTMLDraggable) + , isColorInput(isColorInput) , isNearMarkedText(isNearMarkedText) #if PLATFORM(IOS_FAMILY) , touchCalloutEnabled(touchCalloutEnabled) diff --git a/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.serialization.in b/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.serialization.in index abbcd5462e49..7f6285fea70e 100644 --- a/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.serialization.in +++ b/Source/WebKit/Shared/Cocoa/InteractionInformationAtPosition.serialization.in @@ -39,6 +39,8 @@ struct WebKit::InteractionInformationAtPosition { bool isSelected; bool prefersDraggingOverTextSelection; + bool isDHTMLDraggable; + bool isColorInput; bool isNearMarkedText; #if PLATFORM(IOS_FAMILY) bool touchCalloutEnabled; diff --git a/Source/WebKit/WebProcess/WebPage/Cocoa/PositionInformationForWebPage.mm b/Source/WebKit/WebProcess/WebPage/Cocoa/PositionInformationForWebPage.mm index ffacfb3e8192..20c5c042b39b 100644 --- a/Source/WebKit/WebProcess/WebPage/Cocoa/PositionInformationForWebPage.mm +++ b/Source/WebKit/WebProcess/WebPage/Cocoa/PositionInformationForWebPage.mm @@ -395,16 +395,24 @@ static void selectionPositionInformation(WebPage& page, const InteractionInforma info.url = URL::fileURLWithFileSystemPath(attachment->file()->path()); } - for (auto* currentNode = hitNode.get(); currentNode; currentNode = currentNode->parentOrShadowHostNode()) { - auto* renderer = currentNode->renderer(); + for (RefPtr currentNode = hitNode; currentNode; currentNode = currentNode->parentOrShadowHostNode()) { + CheckedPtr renderer = currentNode->renderer(); if (!renderer) continue; - auto& style = renderer->style(); - if (style.usedUserSelect() == WebCore::UserSelect::None && style.userDrag() == WebCore::UserDrag::Element) { + CheckedRef style = renderer->style(); + if (style->userDrag() == WebCore::UserDrag::Element) + info.isDHTMLDraggable = true; + if (style->usedUserSelect() == WebCore::UserSelect::None && style->userDrag() == WebCore::UserDrag::Element) info.prefersDraggingOverTextSelection = true; - break; + + if (!info.isColorInput) { + if (RefPtr input = dynamicDowncast(currentNode); input && input->isColorControl() && !input->isDisabledFormControl()) + info.isColorInput = true; } + + if (info.prefersDraggingOverTextSelection || info.isDHTMLDraggable || info.isColorInput) + break; } #if PLATFORM(MACCATALYST) bool isInsideFixedPosition; From d394350544f7a2ce2151e98966817567130be14d Mon Sep 17 00:00:00 2001 From: Abrar Rahman Protyasha Date: Fri, 15 May 2026 11:54:48 -0700 Subject: [PATCH 096/424] [AppKit Gestures] Gesture-driven text selection drag-and-drop is often ignored https://bugs.webkit.org/show_bug.cgi?id=314887 rdar://177152499 Reviewed by Wenson Hsieh. EventHandler::handleDrag enforces a 150ms delay between mouseDown and mouseDrag for text selection drags to help the engine figure out between user intent -- drag or selection extension. However, automation events do not have this ambiguity since this is solved by the AppKit gesture controller. Because of this unnecessary delay, we just drop the first N mosueDragged events and end up dropping the drag on the floor. In this patch, we fix this issue by skipping TextDragDelay when the input source is automation. * Source/WebCore/page/EventHandler.cpp: (WebCore::EventHandler::handleDrag): Canonical link: https://commits.webkit.org/313322@main --- Source/WebCore/page/EventHandler.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Source/WebCore/page/EventHandler.cpp b/Source/WebCore/page/EventHandler.cpp index 646b4e3bf11a..938ebfe91f42 100644 --- a/Source/WebCore/page/EventHandler.cpp +++ b/Source/WebCore/page/EventHandler.cpp @@ -4762,10 +4762,18 @@ bool EventHandler::handleDrag(const MouseEventWithHitTestResults& event, CheckDr else m_dragMayStartSelectionInstead = dragState().type.contains(DragSourceAction::Selection); } - - // For drags starting in the selection, the user must wait between the mousedown and mousedrag, - // or else we bail on the dragging stuff and allow selection to occur - if (m_mouseDownMayStartDrag && m_dragMayStartSelectionInstead && dragState().type.contains(DragSourceAction::Selection) && event.event().timestamp() - m_mouseDownTimestamp < TextDragDelay) { + + // Selection-drag candidates need a disambiguation window between mousedown and the first + // mousedrag so that the user can choose between extending a selection and dragging it. + // Automation-source events come from upstream components that have already disambiguated + // the interaction, so they bypass the window. + const bool isSelectionDragCandidate = m_mouseDownMayStartDrag + && m_dragMayStartSelectionInstead + && dragState().type.contains(DragSourceAction::Selection); + const bool inputRequiresDisambiguation = event.event().inputSource() != MouseEventInputSource::Automation; + const bool isWithinDisambiguationWindow = event.event().timestamp() - m_mouseDownTimestamp < TextDragDelay; + + if (isSelectionDragCandidate && inputRequiresDisambiguation && isWithinDisambiguationWindow) { ASSERT(event.event().type() == PlatformEvent::Type::MouseMoved); if (dragState().type.contains(DragSourceAction::Image)) { // ... unless the mouse is over an image, then we start dragging just the image From f9ade7a024fb0e64b2f8898e20d9c580ffd52020 Mon Sep 17 00:00:00 2001 From: Sammy Gill Date: Fri, 15 May 2026 12:05:30 -0700 Subject: [PATCH 097/424] SubtreeScrollbarChangesStateScope can outlive LayoutScope. https://bugs.webkit.org/show_bug.cgi?id=314822 rdar://problem/177077505 Reviewed by Alan Baradlay. If LayoutScope is destroyed before SubtreeScrollbarChangesStateScope we can end up in an odd state. SubtreeScrollbarChangesStateScope is supposed to be used to track any changes in descendants' scrollbars during the initial layout of the renderer so after this is done we should not need to keep it around any longer. Otherwise, when that renderer's LayoutScope destructor runs and sets up a RelayoutScopeForScrollbarChange, it will see the SubtreeScrollbarChangesStateScope still alive, add the renderer to the list of renderers to process later (which is already wrong because we only care about its descendants), but not doing anything with it. Restructure RenderBlock::layout() so the scrollbar-tracking guards are nested inside the LayoutScope branch rather than declared alongside it via std::optional. The three cases (track + handle / handle only / neither) are now spelled out explicitly, which removes the optional, the intermediate willHandleDescendantScrollbarChanges flag, and the reliance on reverse declaration order to get destruction sequencing right. In the tracking branch, destruction now flows handler -> scrollbar state scope -> LayoutScope. Also add an assert in ~RelayoutScopeForScrollbarChange that the block recording a scrollbar change is not the subtree root itself. While I have not been able to find any sort of functional issue with this on trunk, we will need to get this ordering correct to solve a bug in a follow up patch. Canonical link: https://commits.webkit.org/313323@main --- .../rendering/RelayoutScopeForScrollbarChange.cpp | 1 + Source/WebCore/rendering/RenderBlock.cpp | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/WebCore/rendering/RelayoutScopeForScrollbarChange.cpp b/Source/WebCore/rendering/RelayoutScopeForScrollbarChange.cpp index 7316726c7902..ef0f9409448b 100644 --- a/Source/WebCore/rendering/RelayoutScopeForScrollbarChange.cpp +++ b/Source/WebCore/rendering/RelayoutScopeForScrollbarChange.cpp @@ -56,6 +56,7 @@ RelayoutScopeForScrollbarChange::~RelayoutScopeForScrollbarChange() if (m_renderBlock->style().overflowX() == Overflow::Auto || m_renderBlock->style().overflowY() == Overflow::Auto) { if (m_inOverflowRelayout == InOverflowRelayout::No) { if (auto& subtreeScrollbarChangesState = m_renderBlock->layoutContext().subtreeScrollbarChangesState(); subtreeScrollbarChangesState && subtreeScrollbarChangesState->isEligibleForScrollbarHandlingByAncestor(m_renderBlock.get())) { + ASSERT(m_renderBlock.ptr() != subtreeScrollbarChangesState->subtreeRoot.ptr()); subtreeScrollbarChangesState->renderersWithScrollbarChange.add(m_renderBlock); return; } diff --git a/Source/WebCore/rendering/RenderBlock.cpp b/Source/WebCore/rendering/RenderBlock.cpp index f91c28a34c01..86c60de5e8d9 100644 --- a/Source/WebCore/rendering/RenderBlock.cpp +++ b/Source/WebCore/rendering/RenderBlock.cpp @@ -539,13 +539,12 @@ void RenderBlock::layout() // Table cells call layoutBlock directly, so don't add any logic here. Put code into layoutBlock(). { - std::optional subtreeScrollbarChangesStateScope; - if (needsToTrackDescendantScrollbarChanges(*this, layoutContext)) - subtreeScrollbarChangesStateScope.emplace(layoutContext, *this); - - bool willHandleDescendantScrollbarChanges = subtreeScrollbarChangesStateScope.has_value() || canContainDescendantScrollbarChanges(*this, layoutContext); auto scope = LayoutScope { *this }; - if (willHandleDescendantScrollbarChanges) { + if (needsToTrackDescendantScrollbarChanges(*this, layoutContext)) { + SubtreeScrollbarChangesStateScope subtreeScrollbarChangesStateScope(layoutContext, *this); + SubtreeScrollbarChangesHandler descendantScrollbarChangesHandler(*this); + layoutBlock(RelayoutChildren::No); + } else if (canContainDescendantScrollbarChanges(*this, layoutContext)) { SubtreeScrollbarChangesHandler descendantScrollbarChangesHandler(*this); layoutBlock(RelayoutChildren::No); } else From b437b3133f677ad0447c8489cdddaf21f6330318 Mon Sep 17 00:00:00 2001 From: Roberto Rodriguez Date: Fri, 15 May 2026 12:11:32 -0700 Subject: [PATCH 098/424] [ANGLE] Fix ANGLEEnd2End test build error from missed brace https://bugs.webkit.org/show_bug.cgi?id=314904 rdar://177181738 Unreviewed build fix. * Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp: Canonical link: https://commits.webkit.org/313324@main --- Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp b/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp index eb3e60423da8..1623c2e1471d 100644 --- a/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp +++ b/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp @@ -2528,6 +2528,7 @@ TEST_P(MipmapTestES3, MismatchingLevelFormats) glUniform1f(lodLoc, 2); drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); +} // Verifies texture uploads work correctly after size transitions that change mipmap dimensions. TEST_P(MipmapTest, UploadAfterSizeTransition) From 8f00ba97a1c6d23265daa9791e1fd999315bdbe4 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 15 May 2026 12:24:01 -0700 Subject: [PATCH 099/424] [Site Isolation] http/tests/css/filters-on-iframes.html is failing rdar://176928419 https://bugs.webkit.org/show_bug.cgi?id=314686 Reviewed by Simon Fraser. Two things in this patch: 1. RenderWidget::paintContents has a check to not paint anything if the widget is cross-origin and has a security-sensitive filter applied to it. The cross-origin check takes the security origin of the iframe from its Document object, which is not available in Site Isolation mode if the iframe is cross-site. Rewrite the check to use Frame::frameSecuritySecurityOrigin, which should be available when Site Isolation is enabled. This is not _really_ needed, as the above codepath is only used when the widget is not composited. When Site Isolation is enabled, cross-site (and therefore cross-origin) iframes are composited, so it'd never hit the check. But fix it anyway to make stuff SI-safe. 2. The two tests for this check are loaded from 127.0.0.1:8000, and loads the cross-origin iframe from localhost:8000. The sites are different, so the iframe is loaded in a different process and it'd not hit the check. This patch changes the cross-origin iframe to be loaded from 127.0.0.1:8080 instead. 127.0.0.1:8000 and 127.0.0.1:8080 are same-site, so Site Isolation will load them in the same process, but they're cross-origin as the ports are different. * LayoutTests/http/tests/css/filters-on-iframes-transform.html: * LayoutTests/http/tests/css/filters-on-iframes.html: * LayoutTests/http/tests/css/resources/references-external-green.html: * LayoutTests/http/tests/css/resources/references-external-red.html: * LayoutTests/platform/ios-site-isolation/TestExpectations: * LayoutTests/platform/mac-site-isolation/TestExpectations: * Source/WebCore/rendering/RenderWidget.cpp: (WebCore::RenderWidget::paintContents): Canonical link: https://commits.webkit.org/313325@main --- .../css/filters-on-iframes-transform.html | 4 ++-- .../http/tests/css/filters-on-iframes.html | 4 ++-- .../resources/references-external-green.html | 2 +- .../css/resources/references-external-red.html | 2 +- .../ios-site-isolation/TestExpectations | 2 -- .../mac-site-isolation/TestExpectations | 2 -- Source/WebCore/rendering/RenderWidget.cpp | 18 ++++++++++++++---- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/LayoutTests/http/tests/css/filters-on-iframes-transform.html b/LayoutTests/http/tests/css/filters-on-iframes-transform.html index bd0867be6af2..e20b1842133c 100644 --- a/LayoutTests/http/tests/css/filters-on-iframes-transform.html +++ b/LayoutTests/http/tests/css/filters-on-iframes-transform.html @@ -32,13 +32,13 @@

Cross origin with transform (must have http server running)

- +
- +
diff --git a/LayoutTests/http/tests/css/filters-on-iframes.html b/LayoutTests/http/tests/css/filters-on-iframes.html index 020f5acdb8ff..42b21e65a1c5 100644 --- a/LayoutTests/http/tests/css/filters-on-iframes.html +++ b/LayoutTests/http/tests/css/filters-on-iframes.html @@ -32,11 +32,11 @@

Cross origin (must have http server running)

- +
- +

diff --git a/LayoutTests/http/tests/css/resources/references-external-green.html b/LayoutTests/http/tests/css/resources/references-external-green.html index 926522d407af..6a0c4edb5831 100644 --- a/LayoutTests/http/tests/css/resources/references-external-green.html +++ b/LayoutTests/http/tests/css/resources/references-external-green.html @@ -8,4 +8,4 @@ border: none; } - + diff --git a/LayoutTests/http/tests/css/resources/references-external-red.html b/LayoutTests/http/tests/css/resources/references-external-red.html index dd6ec1219e6f..e1f6b5304c41 100644 --- a/LayoutTests/http/tests/css/resources/references-external-red.html +++ b/LayoutTests/http/tests/css/resources/references-external-red.html @@ -8,4 +8,4 @@ border: none; } - + diff --git a/LayoutTests/platform/ios-site-isolation/TestExpectations b/LayoutTests/platform/ios-site-isolation/TestExpectations index cf32f7ba9791..f8a4dbd50556 100644 --- a/LayoutTests/platform/ios-site-isolation/TestExpectations +++ b/LayoutTests/platform/ios-site-isolation/TestExpectations @@ -336,8 +336,6 @@ fast/history/visited-link-background-color.html [ ImageOnlyFailure ] fast/inline/blocks-in-inline-layout-7.html [ ImageOnlyFailure ] fast/inline/blocks-in-inline-margin-collapse-float.html [ ImageOnlyFailure ] fast/scrolling/anchor-overscroll-fixed.html [ ImageOnlyFailure ] -http/tests/css/filters-on-iframes-transform.html [ ImageOnlyFailure ] -http/tests/css/filters-on-iframes.html [ ImageOnlyFailure ] http/tests/loading/reusing-cached-stylesheet-from-different-domain.html [ ImageOnlyFailure ] http/tests/site-isolation/selection-focus.html [ ImageOnlyFailure ] imported/blink/fast/images/content-url-image-with-alt-text-dynamic.html [ ImageOnlyFailure ] diff --git a/LayoutTests/platform/mac-site-isolation/TestExpectations b/LayoutTests/platform/mac-site-isolation/TestExpectations index f8b5a35166d4..cd95a282237c 100644 --- a/LayoutTests/platform/mac-site-isolation/TestExpectations +++ b/LayoutTests/platform/mac-site-isolation/TestExpectations @@ -371,8 +371,6 @@ fast/scrolling/rtl-scrollbars.html [ ImageOnlyFailure ] fast/text/ideographic-content-with-pre-wrap-and-justify.html [ ImageOnlyFailure ] fast/text/simple-line-layout-innerText-with-newline.html [ ImageOnlyFailure ] fast/text/user-installed-canvas.html [ ImageOnlyFailure ] -http/tests/css/filters-on-iframes-transform.html [ ImageOnlyFailure ] -http/tests/css/filters-on-iframes.html [ ImageOnlyFailure ] http/tests/security/css-mask-image.html [ ImageOnlyFailure ] http/tests/webfont/font-loading-system-fallback-visibility.html [ ImageOnlyFailure ] imported/blink/fast/images/content-url-image-with-alt-text-dynamic.html [ ImageOnlyFailure ] diff --git a/Source/WebCore/rendering/RenderWidget.cpp b/Source/WebCore/rendering/RenderWidget.cpp index d9ff5a1d7c16..9f951e079ac2 100644 --- a/Source/WebCore/rendering/RenderWidget.cpp +++ b/Source/WebCore/rendering/RenderWidget.cpp @@ -257,10 +257,20 @@ void RenderWidget::paintContents(PaintInfo& paintInfo, const LayoutPoint& paintO ASSERT(!isSkippedContentRoot(*this)); if (paintInfo.requireSecurityOriginAccessForWidgets) { - if (RefPtr contentDocument = frameOwnerElement().contentDocument()) { - if (!protect(document().securityOrigin())->isSameOriginDomain(contentDocument->securityOrigin())) - return; - } + bool shouldAllow = [&] () { + RefPtr contentFrame = protect(frameOwnerElement())->contentFrame(); + if (!contentFrame) + return false; + + RefPtr frameSecurityOrigin = contentFrame->frameDocumentSecurityOrigin(); + if (!frameSecurityOrigin) + return false; + + return protect(document().securityOrigin())->isSameOriginDomain(*frameSecurityOrigin); + }(); + + if (!shouldAllow) + return; } auto contentPaintOffset = paintOffset + location() + contentBoxRect().location(); From 4221343a3e61e96082cef53d6bfd5b33003d3287 Mon Sep 17 00:00:00 2001 From: Zak Ridouh Date: Fri, 15 May 2026 12:48:31 -0700 Subject: [PATCH 100/424] [Site Isolation] Validation message bubble is positioned incorrectly in cross-origin iframes https://bugs.webkit.org/show_bug.cgi?id=312399 rdar://174855372 Reviewed by Aditya Keerthi. Add rootFrameID to the ShowValidationMessage IPC message and call convertRectToMainFrameCoordinates() on the UIProcess side before displaying the validation bubble. This converts the anchor rect from subframe coordinates to main-frame coordinates when the element is inside a cross-origin iframe. Updated the Mac, iOS, and GTK handlers to use the async coordinate conversion pattern already established by the color picker, datalist, and datetime picker fixes. Test: http/tests/site-isolation/validation-message-cross-origin-iframe.html * LayoutTests/http/tests/site-isolation/resources/validation-message-iframe.html: Added. * LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe-expected.txt: Added. * LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe.html: Added. * Source/WebCore/page/LocalFrameView.h: * Source/WebKit/UIProcess/WebPageProxy.cpp: (WebKit::WebPageProxy::showValidationMessage): * Source/WebKit/UIProcess/WebPageProxy.h: * Source/WebKit/UIProcess/WebPageProxy.messages.in: * Source/WebKit/UIProcess/gtk/WebPageProxyGtk.cpp: (WebKit::WebPageProxy::showValidationMessageWithMainFrameRect): (WebKit::WebPageProxy::showValidationMessage): * Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm: (WebKit::WebPageProxy::showValidationMessageWithMainFrameRect): (WebKit::WebPageProxy::showValidationMessage): * Source/WebKit/UIProcess/mac/WebPageProxyMac.mm: (WebKit::WebPageProxy::showValidationMessageWithMainFrameRect): (WebKit::WebPageProxy::showValidationMessage): * Source/WebKit/WebProcess/WebCoreSupport/WebValidationMessageClient.cpp: (WebKit::WebValidationMessageClient::showValidationMessage): Canonical link: https://commits.webkit.org/313326@main --- .../resources/validation-message-iframe.html | 38 ++++++++++ ...n-message-cross-origin-iframe-expected.txt | 14 ++++ ...alidation-message-cross-origin-iframe.html | 72 +++++++++++++++++++ Source/WebCore/page/LocalFrameView.h | 2 +- Source/WebKit/UIProcess/WebPageProxy.cpp | 18 +++++ Source/WebKit/UIProcess/WebPageProxy.h | 3 +- .../WebKit/UIProcess/WebPageProxy.messages.in | 2 +- .../WebKit/UIProcess/gtk/WebPageProxyGtk.cpp | 10 +-- .../WebKit/UIProcess/ios/WebPageProxyIOS.mm | 15 ++-- .../WebKit/UIProcess/mac/WebPageProxyMac.mm | 10 +-- .../WebValidationMessageClient.cpp | 9 ++- 11 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 LayoutTests/http/tests/site-isolation/resources/validation-message-iframe.html create mode 100644 LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe-expected.txt create mode 100644 LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe.html diff --git a/LayoutTests/http/tests/site-isolation/resources/validation-message-iframe.html b/LayoutTests/http/tests/site-isolation/resources/validation-message-iframe.html new file mode 100644 index 000000000000..92053ea84055 --- /dev/null +++ b/LayoutTests/http/tests/site-isolation/resources/validation-message-iframe.html @@ -0,0 +1,38 @@ + + + + + + +
+ + +
+ + + diff --git a/LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe-expected.txt b/LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe-expected.txt new file mode 100644 index 000000000000..c36f804da8f1 --- /dev/null +++ b/LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe-expected.txt @@ -0,0 +1,14 @@ +Test that validation bubble anchor rect is in main-frame coordinates for cross-origin iframes. + +On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE". + + +PASS bubbleMessage is "Fill out this field" +PASS anchorX is 150 +PASS anchorY is 130 +PASS anchorWidth is 200 +PASS anchorHeight is 30 +PASS successfullyParsed is true + +TEST COMPLETE + diff --git a/LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe.html b/LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe.html new file mode 100644 index 000000000000..4178ab0ad49d --- /dev/null +++ b/LayoutTests/http/tests/site-isolation/validation-message-cross-origin-iframe.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/Source/WebCore/page/LocalFrameView.h b/Source/WebCore/page/LocalFrameView.h index 0599ba16b361..35c62e2202ba 100644 --- a/Source/WebCore/page/LocalFrameView.h +++ b/Source/WebCore/page/LocalFrameView.h @@ -748,7 +748,7 @@ class LocalFrameView final : public FrameView { void scrollbarWidthChanged(ScrollbarWidth) override; - std::optional NODELETE rootFrameID() const final; + WEBCORE_EXPORT std::optional NODELETE rootFrameID() const final; IntSize totalScrollbarSpace() const final; int scrollbarGutterWidth(bool isHorizontalWritingMode = true) const; diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp index 75a263b0cf34..35436e6201fb 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp @@ -16114,6 +16114,24 @@ void WebPageProxy::hideValidationMessage() #endif } +#if PLATFORM(COCOA) || PLATFORM(GTK) +void WebPageProxy::showValidationMessage(const IntRect& anchorClientRect, String&& message, std::optional&& rootFrameID) +{ + RefPtr pageClient = this->pageClient(); + if (!pageClient) + return; + + m_validationBubble = pageClient->createValidationBubble(WTF::move(message), { protect(preferences())->minimumFontSize() }); + + convertRectToMainFrameCoordinates(anchorClientRect, rootFrameID, [weakThis = WeakPtr { *this }](std::optional convertedRect) { + RefPtr protectedThis = weakThis.get(); + if (!protectedThis || !convertedRect) + return; + protectedThis->showValidationMessageWithMainFrameRect(IntRect(*convertedRect)); + }); +} +#endif + // FIXME: Consolidate with dismissContentRelativeChildWindows void WebPageProxy::closeOverlayedViews() { diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h index 763719f52b9f..95cb377f56c1 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h @@ -2165,9 +2165,10 @@ class WebPageProxy final : public API::ObjectImpl, publ void logScrollingEvent(uint32_t eventType, MonotonicTime, uint64_t); // Form validation messages. - void showValidationMessage(const WebCore::IntRect& anchorClientRect, String&& message); + void showValidationMessage(const WebCore::IntRect& anchorClientRect, String&& message, std::optional&& rootFrameID); void hideValidationMessage(); #if PLATFORM(COCOA) || PLATFORM(GTK) + void showValidationMessageWithMainFrameRect(const WebCore::IntRect& mainFrameAnchorRect); WebCore::ValidationBubble* validationBubble() const { return m_validationBubble.get(); } // For testing. #endif diff --git a/Source/WebKit/UIProcess/WebPageProxy.messages.in b/Source/WebKit/UIProcess/WebPageProxy.messages.in index a467b69e02b5..9994904d0315 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.messages.in +++ b/Source/WebKit/UIProcess/WebPageProxy.messages.in @@ -65,7 +65,7 @@ messages -> WebPageProxy { SetAccessibilityMode(enum:uint8_t WebCore::AccessibilityMode mode) #if PLATFORM(COCOA) || PLATFORM(GTK) - ShowValidationMessage(WebCore::IntRect anchorRect, String message) + ShowValidationMessage(WebCore::IntRect anchorRect, String message, std::optional rootFrameID) HideValidationMessage() #endif diff --git a/Source/WebKit/UIProcess/gtk/WebPageProxyGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageProxyGtk.cpp index 8e993a8e70f3..fb62efbe2018 100644 --- a/Source/WebKit/UIProcess/gtk/WebPageProxyGtk.cpp +++ b/Source/WebKit/UIProcess/gtk/WebPageProxyGtk.cpp @@ -91,14 +91,10 @@ void WebPageProxy::showEmojiPicker(const WebCore::IntRect& caretRect, Completion webkitWebViewBaseShowEmojiChooser(WEBKIT_WEB_VIEW_BASE(viewWidget()), caretRect, WTF::move(completionHandler)); } -void WebPageProxy::showValidationMessage(const WebCore::IntRect& anchorClientRect, String&& message) +void WebPageProxy::showValidationMessageWithMainFrameRect(const WebCore::IntRect& mainFrameAnchorRect) { - RefPtr pageClient = this->pageClient(); - if (!pageClient) - return; - - m_validationBubble = pageClient->createValidationBubble(WTF::move(message), { m_preferences->minimumFontSize() }); - m_validationBubble->showRelativeTo(anchorClientRect); + if (RefPtr bubble = m_validationBubble) + bubble->showRelativeTo(mainFrameAnchorRect); } void WebPageProxy::sendMessageToWebViewWithReply(UserMessage&& message, CompletionHandler&& completionHandler) diff --git a/Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm b/Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm index fa75a4bc95d3..97cdefe1e9c9 100644 --- a/Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm +++ b/Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm @@ -1232,26 +1232,27 @@ static FloatSize fullscreenPreferencesScreenSize(CGFloat preferredWidth) m_waitingForPostLayoutEditorStateUpdateAfterFocusingElement = false; } -void WebPageProxy::showValidationMessage(const IntRect& anchorClientRect, String&& message) +void WebPageProxy::showValidationMessageWithMainFrameRect(const IntRect& mainFrameAnchorRect) { + RefPtr bubble = m_validationBubble; + if (!bubble) + return; + RefPtr pageClient = this->pageClient(); if (!pageClient) return; - m_validationBubble = pageClient->createValidationBubble(WTF::move(message), { protect(m_preferences)->minimumFontSize() }); - Ref validationBubble = *m_validationBubble; - - validationBubble->setShouldSuppressPresentation(pageClient->shouldSuppressFormValidationBubble()); + bubble->setShouldSuppressPresentation(pageClient->shouldSuppressFormValidationBubble()); // FIXME: When in element fullscreen, UIClient::presentingViewController() may not return the // WKFullScreenViewController even though that is the presenting view controller of the WKWebView. // We should call PageClientImpl::presentingViewController() instead. - validationBubble->setAnchorRect(anchorClientRect, protect(uiClient().presentingViewController())); + bubble->setAnchorRect(mainFrameAnchorRect, protect(uiClient().presentingViewController())); // If we are currently doing a scrolling / zoom animation, then we'll delay showing the validation // bubble until the animation is over. if (!m_isScrollingOrZooming) - validationBubble->show(); + bubble->show(); } void WebPageProxy::setIsScrollingOrZooming(bool isScrollingOrZooming) diff --git a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm index 43cce1523355..7412e6ac77d7 100644 --- a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm +++ b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm @@ -779,14 +779,10 @@ static inline bool expectsLegacyImplicitRubberBandControl() windowRect = pageClient ? pageClient->rootViewToWindow(viewRect) : WebCore::IntRect { }; } -void WebPageProxy::showValidationMessage(const IntRect& anchorClientRect, String&& message) +void WebPageProxy::showValidationMessageWithMainFrameRect(const IntRect& mainFrameAnchorRect) { - RefPtr pageClient = this->pageClient(); - if (!pageClient) - return; - - m_validationBubble = pageClient->createValidationBubble(WTF::move(message), { protect(preferences())->minimumFontSize() }); - protect(m_validationBubble)->showRelativeTo(anchorClientRect); + if (RefPtr bubble = m_validationBubble) + bubble->showRelativeTo(mainFrameAnchorRect); } RetainPtr WebPageProxy::inspectorAttachmentView() diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebValidationMessageClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebValidationMessageClient.cpp index 43e892fe9960..e2c1705c8024 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/WebValidationMessageClient.cpp +++ b/Source/WebKit/WebProcess/WebCoreSupport/WebValidationMessageClient.cpp @@ -29,8 +29,10 @@ #include "MessageSenderInlines.h" #include "WebPage.h" #include "WebPageProxyMessages.h" +#include #include #include +#include #include #include @@ -66,7 +68,12 @@ void WebValidationMessageClient::showValidationMessage(const Element& anchor, St m_currentAnchor = anchor; m_currentAnchorRect = anchor.boundingBoxInRootViewCoordinates(); - Ref { *m_page }->send(Messages::WebPageProxy::ShowValidationMessage(m_currentAnchorRect, WTF::move(message))); + + std::optional rootFrameID; + if (RefPtr view = anchor.document().view()) + rootFrameID = view->rootFrameID(); + + Ref { *m_page }->send(Messages::WebPageProxy::ShowValidationMessage(m_currentAnchorRect, WTF::move(message), rootFrameID)); } void WebValidationMessageClient::hideValidationMessage(const Element& anchor) From 4adff5893b791c7b0fc251f8a0761670a9f02cf6 Mon Sep 17 00:00:00 2001 From: Ruthvik Konda Date: Fri, 15 May 2026 13:15:32 -0700 Subject: [PATCH 101/424] [model] Adopt public RealityCoreRenderer and ShaderGraph APIs https://bugs.webkit.org/show_bug.cgi?id=314780 rdar://175205378 Reviewed by Mike Wyrzykowski. RealityCoreRenderer-21 promoted its SPI types from the _Proto_*_v1 naming convention to public API names. ShaderGraph-156 similarly promoted _Proto_ShaderNodeGraph and its nested types to a new public ShaderGraph class. Adopt these public names in the GPUProcess model code. Also adopts the RCR-22 LowLevelRenderContextShaderGraph protocol for makeShaderGraphFunctions. Bumps the #if canImport guards to require RealityCoreRenderer 22 and ShaderGraph 156 across all seven model files. Leaves _Proto_* references in place for RealityCoreDeformation, _USDKit_RealityKit, and RealityCoreTextureProcessing - those frameworks have not yet promoted their SPIs to public API. Also leaves serialize-side ShaderGraph helpers on _Proto_ShaderNodeGraph since USDKit's _Proto_MaterialDataUpdate_v1.shaderGraph still returns the old type. Follow-up work will migrate each family as those frameworks complete their API promotions. * Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift: * Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift: * Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift: * Source/WebKit/GPUProcess/graphics/Model/USDModel.swift: (textureCoordinate(from:)): (ShaderGraph.fromWKDescriptor(_:)): (fromWKBridgeDataType(_:)): (fromWKBridgeConstant(_:)): Canonical link: https://commits.webkit.org/313327@main --- .../graphics/Model/ModelBridge.swift | 8 +- .../graphics/Model/ModelIBLTextures.swift | 12 +- .../graphics/Model/ModelParameters.swift | 36 +-- .../graphics/Model/ModelRenderer.swift | 64 +++-- .../graphics/Model/ModelUtils.swift | 40 +-- .../graphics/Model/USDModel+Deformation.swift | 19 +- .../GPUProcess/graphics/Model/USDModel.swift | 249 +++++++++--------- 7 files changed, 227 insertions(+), 201 deletions(-) diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift index 9a538baa849d..97a1e800f814 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelBridge.swift @@ -24,9 +24,9 @@ import Metal import WebKit -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit -@_spi(RealityCoreRendererAPI) import RealityKit +import RealityKit import USDKit @_spi(SwiftAPI) import DirectResource import ShaderGraph @@ -300,7 +300,7 @@ extension WKBridgeUpdateMesh { } } -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) func decodeValues(from data: Data) -> [T] where T: BitwiseCopyable { let stride = MemoryLayout.stride guard !data.isEmpty, data.count % stride == 0 else { return [] } @@ -618,7 +618,7 @@ extension WKBridgeMaterialGraph { } } -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) func toData(_ input: [T]) -> Data { // rdar://164559261 - this is needed because there is no way to represnt an NSArray of diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift index d78293d06096..a20666643f97 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelIBLTextures.swift @@ -21,7 +21,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) import Metal import USDKit @@ -30,12 +30,12 @@ import USDKit class IBLTextures { static func loadIBLTextures( - renderContext: any _Proto_LowLevelRenderContext_v1, + renderContext: any LowLevelRenderContext, diffuseTextureOriginal: any MTLTexture, specularTextureOriginal: any MTLTexture ) throws -> ( - diffuse: _Proto_LowLevelTextureResource_v1, - specular: _Proto_LowLevelTextureResource_v1 + diffuse: LowLevelTextureResource, + specular: LowLevelTextureResource ) { guard let commandQueue = renderContext.device.makeCommandQueue() else { fatalError("Failed to create command queue") @@ -81,8 +81,8 @@ class IBLTextures { ) ) - let diffuseTexture = diffuseTextureResource.replace(using: commandBuffer) - let specularTexture = specularTextureResource.replace(using: commandBuffer) + let diffuseTexture = diffuseTextureResource.replace(commandBuffer: commandBuffer) + let specularTexture = specularTextureResource.replace(commandBuffer: commandBuffer) // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 // swift-format-ignore: NeverForceUnwrap diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift index 190619bbd06e..3eb2a00b1d32 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelParameters.swift @@ -21,18 +21,18 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) import USDKit @_spi(UsdLoaderAPI) import _USDKit_RealityKit -@_spi(RealityCoreRendererAPI) import RealityKit +import RealityKit func makeParameters( - for function: any _Proto_LowLevelMaterialResource_v1.Function, - renderContext: any _Proto_LowLevelRenderContext_v1, - buffers: [_Proto_LowLevelBufferSpan_v1] = [], - textures: [_Proto_LowLevelTextureResource_v1] = [] -) throws -> _Proto_LowLevelArgumentTable_v1? { + for function: any LowLevelMaterialResource.Function, + renderContext: any LowLevelRenderContext, + buffers: [LowLevelBufferSlice] = [], + textures: [LowLevelTextureResource] = [] +) throws -> LowLevelArgumentTable? { guard let argumentTableDescriptor = function.argumentTableDescriptor else { return nil } @@ -44,19 +44,19 @@ func makeParameters( } func makeParameters( - for material: _Proto_LowLevelMaterialResource_v1, - renderContext: any _Proto_LowLevelRenderContext_v1, - geometryBuffers: [_Proto_LowLevelBufferSpan_v1] = [], - geometryTextures: [_Proto_LowLevelTextureResource_v1] = [], - surfaceBuffers: [_Proto_LowLevelBufferSpan_v1] = [], - surfaceTextures: [_Proto_LowLevelTextureResource_v1] = [], - lightingBuffers: [_Proto_LowLevelBufferSpan_v1] = [], - lightingTextures: [_Proto_LowLevelTextureResource_v1] = [] + for material: LowLevelMaterialResource, + renderContext: any LowLevelRenderContext, + geometryBuffers: [LowLevelBufferSlice] = [], + geometryTextures: [LowLevelTextureResource] = [], + surfaceBuffers: [LowLevelBufferSlice] = [], + surfaceTextures: [LowLevelTextureResource] = [], + lightingBuffers: [LowLevelBufferSlice] = [], + lightingTextures: [LowLevelTextureResource] = [] ) throws -> ( - geometryArguments: _Proto_LowLevelArgumentTable_v1?, - surfaceArguments: _Proto_LowLevelArgumentTable_v1?, - lightingArguments: _Proto_LowLevelArgumentTable_v1? + geometryArguments: LowLevelArgumentTable?, + surfaceArguments: LowLevelArgumentTable?, + lightingArguments: LowLevelArgumentTable? ) { let geometryArguments = try makeParameters( diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift index f7a2979be911..49199dd145c7 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelRenderer.swift @@ -21,26 +21,27 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) import QuartzCore import USDKit @_spi(UsdLoaderAPI) import _USDKit_RealityKit -@_spi(RealityCoreRendererAPI) @_spi(Private) import RealityKit +@_spi(Private) import RealityKit import simd class Renderer { let device: any MTLDevice let commandQueue: any MTLCommandQueue - var renderContext: (any _Proto_LowLevelRenderContext_v1)? - var renderer: _Proto_LowLevelRenderer_v1? - var renderTargetDescriptor: _Proto_LowLevelRenderTarget_v1.Descriptor { + var renderContext: (any LowLevelRenderContext)? + var renderer: LowLevelRenderer? + var renderTargetDescriptor: LowLevelRenderTarget.Descriptor { // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 // swift-format-ignore: NeverForceUnwrap renderer!.renderTargetDescriptor } - var pose: _Proto_Pose_v1 + var cameraPosition: SIMD3 + var cameraRotation: simd_quatf static let cameraDistance: Float = 0.5 var effectiveCameraDistance: Float = Renderer.cameraDistance var fovY: Float = 60 * .pi / 180 @@ -55,29 +56,28 @@ class Renderer { self.device = device self.commandQueue = commandQueue - self.pose = .init( - translation: [0, 0, Renderer.cameraDistance], - rotation: .init(ix: 0, iy: 0, iz: 0, r: 1) - ) + self.cameraPosition = [0, 0, Renderer.cameraDistance] + self.cameraRotation = .init(ix: 0, iy: 0, iz: 0, r: 1) self.memoryOwner = memoryOwner } func createMaterialCompiler(colorPixelFormat: MTLPixelFormat, rasterSampleCount: Int, colorSpace: CGColorSpace? = nil) async throws { - var configuration = _Proto_LowLevelRenderContextStandaloneConfiguration_v1(device: device) + var configuration = LowLevelRenderContextStandalone.Configuration(device: device) configuration.memoryOwner = self.memoryOwner - configuration.residencySetBehavior = _Proto_LowLevelRenderContextStandaloneConfiguration_v1.ResidencySetBehavior.disable - let renderContext = try await _Proto_makeLowLevelRenderContextStandalone_v1(configuration: configuration) + configuration.residencySetBehavior = LowLevelRenderContextStandalone.Configuration.ResidencySetBehavior.disable + let renderContext = try await LowLevelRenderContextStandalone(configuration: configuration) - let renderer = try await _Proto_LowLevelRenderer_v1( + let renderer = try await LowLevelRenderer( configuration: .init( output: .init(colorPixelFormat: colorPixelFormat), rasterSampleCount: rasterSampleCount, enableTonemap: true, enableColorMatch: colorSpace != nil, - hasTransparentContent: false + alphaPremultiply: false ), renderContext: renderContext ) + renderer.meshInstancesArrayCount = 1 self.renderContext = renderContext self.renderer = renderer } @@ -91,7 +91,7 @@ class Renderer { } func render( - meshInstances: _Proto_LowLevelMeshInstanceArray_v1, + meshInstances: LowLevelMeshInstanceArray, texture: any MTLTexture, commandBuffer: any MTLCommandBuffer ) throws { @@ -104,7 +104,7 @@ class Renderer { let aspect = Float(texture.width) / Float(texture.height) let d = effectiveCameraDistance - let projection = _Proto_LowLevelRenderer_v1.Camera.Projection.perspective( + let projection = LowLevelRenderer.Camera.Projection.perspective( fovYRadians: fovY, aspectRatio: aspect, nearZ: d * 0.01, @@ -112,14 +112,31 @@ class Renderer { reverseZ: true ) - renderer.cameras[0].pose = pose + renderer.cameras[0].position = cameraPosition + renderer.cameras[0].rotation = cameraRotation renderer.cameras[0].projection = projection renderer.output.clearColor = clearColor renderer.output.color = .init(texture: texture) - renderer.meshInstances = meshInstances + try renderer.setMeshInstances(meshInstances, at: 0) commandBuffer.label = "Render Camera" - try renderer.render(for: commandBuffer) + var indices = Array(meshInstances.indices) + indices = LowLevelRenderer.cullMeshInstances( + meshInstances, + indices: indices.span, + configuration: .init(frustum: .init(from: renderer.cameras[0])) + ) + var span = indices.mutableSpan + LowLevelRenderer.sortMeshInstances( + meshInstances, + indices: &span, + configuration: .init(cameraPosition: renderer.cameras[0].position) + ) + renderer.render(using: commandBuffer) { state in + for index in indices { + state.render(meshInstancesArrayIndex: 0, meshInstanceIndex: index) + } + } commandBuffer.commit() } @@ -144,12 +161,13 @@ class Renderer { let col2 = simd_float3(cameraMatrix.columns.2.x, cameraMatrix.columns.2.y, cameraMatrix.columns.2.z) let scale = simd_float3(simd_length(col0), simd_length(col1), simd_length(col2)) let rotation = simd_quatf(simd_float3x3(col0 / scale.x, col1 / scale.y, col2 / scale.z)) - let translation = simd_float3(cameraMatrix.columns.3.x, cameraMatrix.columns.3.y, cameraMatrix.columns.3.z) - pose = .init(translation: translation, rotation: rotation) + let position = simd_float3(cameraMatrix.columns.3.x, cameraMatrix.columns.3.y, cameraMatrix.columns.3.z) + cameraPosition = position + cameraRotation = rotation // Derive the near/far distance from the camera's world-space position. // The model lives at its USD-space coordinates, so the camera can be far from // origin for large or offset models; simd_length gives the actual view distance. - effectiveCameraDistance = max(simd_length(translation), Self.cameraDistance) + effectiveCameraDistance = max(simd_length(position), Self.cameraDistance) } } diff --git a/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift b/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift index 268b31699dcd..89006185bf8c 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/ModelUtils.swift @@ -21,33 +21,33 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) import DirectResource import Metal import USDKit @_spi(UsdLoaderAPI) import _USDKit_RealityKit -@_spi(RealityCoreRendererAPI) import RealityKit +import RealityKit final class MeshInstancePool { - private(set) var meshInstances: _Proto_LowLevelMeshInstanceArray_v1 - private let renderContext: any _Proto_LowLevelRenderContext_v1 + private(set) var meshInstances: LowLevelMeshInstanceArray + private let renderContext: any LowLevelRenderContext init( - renderContext: any _Proto_LowLevelRenderContext_v1, - renderTargets: [_Proto_LowLevelRenderTarget_v1.Descriptor], + renderContext: any LowLevelRenderContext, + renderTargets: [LowLevelRenderTarget.Descriptor], initialCapacity: Int ) throws { self.renderContext = renderContext - self.meshInstances = try renderContext.makeMeshInstanceArray(renderTargets: renderTargets, count: initialCapacity) + self.meshInstances = try renderContext.makeMeshInstanceArray(renderTargets: .init(renderTargets), count: initialCapacity) } - init(renderContext: any _Proto_LowLevelRenderContext_v1, meshInstances: _Proto_LowLevelMeshInstanceArray_v1) { + init(renderContext: any LowLevelRenderContext, meshInstances: LowLevelMeshInstanceArray) { self.renderContext = renderContext self.meshInstances = meshInstances } - func add(_ instance: _Proto_LowLevelMeshInstance_v1) throws { + func add(_ instance: LowLevelMeshInstance) throws { if let emptyIndex = meshInstances.firstIndex(where: { $0 == nil }) { try meshInstances.setMeshInstance(instance, index: emptyIndex) } else { @@ -61,7 +61,7 @@ final class MeshInstancePool { } } - func remove(_ instance: _Proto_LowLevelMeshInstance_v1) throws { + func remove(_ instance: LowLevelMeshInstance) throws { guard let index = meshInstances.firstIndex(where: { $0 === instance }) else { fatalError("Mesh instance not found in MeshInstancePool") } @@ -85,7 +85,7 @@ extension simd_float4x4 { } } -extension _Proto_LowLevelMeshResource_v1.Descriptor { +extension LowLevelMeshResource.Descriptor { static func fromLlmDescriptor(_ llmDescriptor: LowLevelMesh.Descriptor) -> Self { var descriptor = Self.init() descriptor.vertexCapacity = llmDescriptor.vertexCapacity @@ -118,7 +118,7 @@ private func copyDataIntoBuffer(_ buffer: inout MutableRawSpan, from data: Data) unsafe buffer.withUnsafeMutableBytes { unsafe $0.copyBytes(from: data) } } -extension _Proto_LowLevelMeshResource_v1 { +extension LowLevelMeshResource { func replaceVertexData(_ vertexData: [Data]) { for (vertexBufferIndex, vertexData) in vertexData.enumerated() { self.replaceVertices(at: vertexBufferIndex) { copyDataIntoBuffer(&$0, from: vertexData) } @@ -140,9 +140,9 @@ extension _Proto_LowLevelMeshResource_v1 { } } -extension _Proto_LowLevelTextureResource_v1.Descriptor { - static func from(_ textureDescriptor: MTLTextureDescriptor) -> _Proto_LowLevelTextureResource_v1.Descriptor { - var descriptor = _Proto_LowLevelTextureResource_v1.Descriptor() +extension LowLevelTextureResource.Descriptor { + static func from(_ textureDescriptor: MTLTextureDescriptor) -> LowLevelTextureResource.Descriptor { + var descriptor = LowLevelTextureResource.Descriptor() descriptor.width = textureDescriptor.width descriptor.height = textureDescriptor.height descriptor.depth = textureDescriptor.depth @@ -156,8 +156,8 @@ extension _Proto_LowLevelTextureResource_v1.Descriptor { return descriptor } - static func from(_ texture: any MTLTexture, swizzle: MTLTextureSwizzleChannels) -> _Proto_LowLevelTextureResource_v1.Descriptor { - var descriptor = _Proto_LowLevelTextureResource_v1.Descriptor() + static func from(_ texture: any MTLTexture, swizzle: MTLTextureSwizzleChannels) -> LowLevelTextureResource.Descriptor { + var descriptor = LowLevelTextureResource.Descriptor() descriptor.width = texture.width descriptor.height = texture.height descriptor.depth = texture.depth @@ -186,7 +186,7 @@ extension _Proto_LowLevelTextureResource_v1.Descriptor { } } -private func mapSemantic(_ semantic: LowLevelMesh.VertexSemantic) -> _Proto_LowLevelMeshResource_v1.VertexSemantic { +private func mapSemantic(_ semantic: LowLevelMesh.VertexSemantic) -> LowLevelMeshResource.VertexSemantic { switch semantic { case .position: .position case .color: .color @@ -206,7 +206,7 @@ private func mapSemantic(_ semantic: LowLevelMesh.VertexSemantic) -> _Proto_LowL } } -private func mapSemantic(_ semantic: WKBridgeVertexSemantic) -> _Proto_LowLevelMeshResource_v1.VertexSemantic { +private func mapSemantic(_ semantic: WKBridgeVertexSemantic) -> LowLevelMeshResource.VertexSemantic { switch semantic { case .position: .position case .color: .color @@ -226,7 +226,7 @@ private func mapSemantic(_ semantic: WKBridgeVertexSemantic) -> _Proto_LowLevelM } } -extension _Proto_LowLevelMeshResource_v1.Descriptor { +extension LowLevelMeshResource.Descriptor { static func fromLlmDescriptor(_ llmDescriptor: WKBridgeMeshDescriptor) -> Self { var descriptor = Self.init() descriptor.vertexCapacity = Int(llmDescriptor.vertexCapacity) diff --git a/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift b/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift index 49e9fd016e51..8aa343a4408f 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/USDModel+Deformation.swift @@ -26,9 +26,8 @@ import OSLog import WebKit import simd -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit -@_spi(RealityCoreRendererAPI) import RealityKit @_spi(RealityCoreTextureProcessingAPI) import RealityCoreTextureProcessing import USDKit @_spi(SwiftAPI) import DirectResource @@ -358,7 +357,7 @@ func configureDeformation( deformationData: WKBridgeDeformationData, commandBuffer: any MTLCommandBuffer, device: any MTLDevice, - meshResource: _Proto_LowLevelMeshResource_v1, + meshResource: LowLevelMeshResource, meshResourceToDeformationContext: inout [WKBridgeTypedResourceId: DeformationContext], deformationSystem: _Proto_LowLevelDeformationSystem_v1, memoryOwner: task_id_token_t @@ -404,7 +403,7 @@ func updateDeformationContextInPlace(deformationData: WKBridgeDeformationData, c } func buildDeformationContext( - meshResource: _Proto_LowLevelMeshResource_v1, + meshResource: LowLevelMeshResource, deformationData: WKBridgeDeformationData, commandBuffer: any MTLCommandBuffer, device: any MTLDevice, @@ -496,7 +495,7 @@ struct DeformationMeshDescriptionData { } func makeMeshDescriptionForDeformation( - meshResource: _Proto_LowLevelMeshResource_v1, + meshResource: LowLevelMeshResource, deformationData: WKBridgeDeformationData, commandBuffer: any MTLCommandBuffer, isInput: Bool, @@ -513,7 +512,7 @@ func makeMeshDescriptionForDeformation( // Similar usage as the buffer index table. See comment above var meshResourceLayoutIndexToDeformationLayoutIndex: [Int: Int] = [:] - var inputAttributeSemantics: [_Proto_LowLevelMeshResource_v1.VertexSemantic] = [.position] + var inputAttributeSemantics: [LowLevelMeshResource.VertexSemantic] = [.position] if deformationData.renormalizationData != nil { inputAttributeSemantics.append(contentsOf: [.normal, .tangent, .bitangent]) } @@ -533,7 +532,7 @@ func makeMeshDescriptionForDeformation( } else { meshResourceBufferIndexToDeformationBufferIndex[bufferIndex] = deformationBufferIndex - let vertexBuffer = meshResource.readVertices_v2(at: bufferIndex, using: commandBuffer) + let vertexBuffer = meshResource.readVertices(at: bufferIndex, commandBuffer: commandBuffer) if isInput { // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 // swift-format-ignore: NeverForceUnwrap @@ -572,7 +571,7 @@ func makeMeshDescriptionForDeformation( } func makeInputMeshDescriptionForDeformation( - meshResource: _Proto_LowLevelMeshResource_v1, + meshResource: LowLevelMeshResource, deformationData: WKBridgeDeformationData, commandBuffer: any MTLCommandBuffer, device: any MTLDevice, @@ -624,7 +623,7 @@ func makeInputMeshDescriptionForDeformation( } func makeOutputMeshDescriptionForDeformation( - meshResource: _Proto_LowLevelMeshResource_v1, + meshResource: LowLevelMeshResource, deformationData: WKBridgeDeformationData, commandBuffer: any MTLCommandBuffer, device: any MTLDevice, @@ -655,7 +654,7 @@ func makeOutputMeshDescriptionForDeformation( } } -extension _Proto_LowLevelMeshResource_v1.VertexSemantic { +extension LowLevelMeshResource.VertexSemantic { func toDeformationVertexSemantic() -> _Proto_LowLevelDeformationDescription_v1.MeshSemantic? { switch self { case .position: diff --git a/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift b/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift index 87cf169ad1e8..3be677fee9b1 100644 --- a/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift +++ b/Source/WebKit/GPUProcess/graphics/Model/USDModel.swift @@ -26,7 +26,7 @@ import OSLog import WebKit import simd -#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && arch(arm64) +#if ENABLE_GPU_PROCESS_MODEL && canImport(RealityCoreTextureProcessing, _version: 24) && canImport(_USDKit_RealityKit, _version: 42) && canImport(RealityCoreRenderer, _version: 22) && canImport(ShaderGraph, _version: 156) && arch(arm64) @_spi(UsdLoaderAPI) import _USDKit_RealityKit @_spi(RealityCoreRendererAPI) import RealityKit import USDKit @@ -212,14 +212,14 @@ private func makeMTLTextureFromImageAsset( private func makeTextureFromImageAsset( _ imageAsset: WKBridgeImageAsset, device: any MTLDevice, - renderContext: any _Proto_LowLevelRenderContext_v1, + renderContext: any LowLevelRenderContext, commandQueue: any MTLCommandQueue, generateMips: Bool, memoryOwner: task_id_token_t, swizzle: MTLTextureSwizzleChannels, - existingTexture: _Proto_LowLevelTextureResource_v1?, + existingTexture: LowLevelTextureResource?, layout: [WKBridgeTextureLevelInfo] -) -> _Proto_LowLevelTextureResource_v1? { +) -> LowLevelTextureResource? { guard let (mtlTexture, mipLevelsInData) = makeMTLTextureFromImageAsset( imageAsset, @@ -233,7 +233,7 @@ private func makeTextureFromImageAsset( return nil } - let descriptor = _Proto_LowLevelTextureResource_v1.Descriptor.from(mtlTexture, swizzle: swizzle) + let descriptor = LowLevelTextureResource.Descriptor.from(mtlTexture, swizzle: swizzle) if let textureResource = existingTexture ?? (try? renderContext.makeTextureResource(descriptor: descriptor)) { guard let commandBuffer = commandQueue.makeCommandBuffer() else { fatalError("Could not create command buffer") @@ -245,7 +245,7 @@ private func makeTextureFromImageAsset( blitEncoder.generateMipmaps(for: mtlTexture) } - let outTexture = textureResource.replace(using: commandBuffer) + let outTexture = textureResource.replace(commandBuffer: commandBuffer) blitEncoder.copy(from: mtlTexture, to: outTexture) blitEncoder.endEncoding() @@ -260,12 +260,12 @@ private func makeTextureFromImageAsset( private func makeTextureFromImageAsset( _ imageAsset: WKBridgeImageAsset, device: any MTLDevice, - renderContext: any _Proto_LowLevelRenderContext_v1, + renderContext: any LowLevelRenderContext, commandQueue: any MTLCommandQueue, generateMips: Bool, memoryOwner: task_id_token_t, swizzle: MTLTextureSwizzleChannels = .init(red: .red, green: .green, blue: .blue, alpha: .alpha) -) -> _Proto_LowLevelTextureResource_v1? { +) -> LowLevelTextureResource? { makeTextureFromImageAsset( imageAsset, device: device, @@ -282,13 +282,13 @@ private func makeTextureFromImageAsset( private func makeTextureFromImageAsset( _ imageAsset: WKBridgeImageAsset, device: any MTLDevice, - renderContext: any _Proto_LowLevelRenderContext_v1, + renderContext: any LowLevelRenderContext, commandQueue: any MTLCommandQueue, generateMips: Bool, memoryOwner: task_id_token_t, - existingTexture: _Proto_LowLevelTextureResource_v1?, + existingTexture: LowLevelTextureResource?, layout: [WKBridgeTextureLevelInfo] -) -> _Proto_LowLevelTextureResource_v1? { +) -> LowLevelTextureResource? { makeTextureFromImageAsset( imageAsset, device: device, @@ -303,16 +303,16 @@ private func makeTextureFromImageAsset( } private func makeParameters( - for function: (any _Proto_LowLevelMaterialResource_v1.Function)?, - renderContext: any _Proto_LowLevelRenderContext_v1, - textureHashesAndResources: [WKBridgeTypedResourceId: (String, _Proto_LowLevelTextureResource_v1)], - fallbackTexture: _Proto_LowLevelTextureResource_v1 -) throws -> _Proto_LowLevelArgumentTable_v1? { + for function: (any LowLevelMaterialResource.Function)?, + renderContext: any LowLevelRenderContext, + textureHashesAndResources: [WKBridgeTypedResourceId: (String, LowLevelTextureResource)], + fallbackTexture: LowLevelTextureResource +) throws -> LowLevelArgumentTable? { guard let function else { return nil } guard let argumentTableDescriptor = function.argumentTableDescriptor else { return nil } let parameterMapping = function.parameterMapping - var optTextures: [_Proto_LowLevelTextureResource_v1?] = argumentTableDescriptor.textures.map({ _ in nil }) + var optTextures: [LowLevelTextureResource?] = argumentTableDescriptor.textures.map({ _ in nil }) for parameter in parameterMapping?.textures ?? [] { if let textureHashAndResource = textureHashesAndResources.values.first(where: { $0.0 == parameter.name }) { optTextures[parameter.textureIndex] = textureHashAndResource.1 @@ -324,7 +324,7 @@ private func makeParameters( } let textures = optTextures.map({ $0! }) - let buffers: [_Proto_LowLevelBufferSpan_v1] = try argumentTableDescriptor.buffers.map { bufferRequirements in + let buffers: [LowLevelBufferSlice] = try argumentTableDescriptor.buffers.map { bufferRequirements in let capacity = (bufferRequirements.size + 16 - 1) / 16 * 16 let buffer = try renderContext.makeBufferResource(descriptor: .init(capacity: capacity)) buffer.replace { span in @@ -332,7 +332,7 @@ private func makeParameters( span.storeBytes(of: 0, toByteOffset: byteOffset, as: UInt8.self) } } - return try _Proto_LowLevelBufferSpan_v1(buffer: buffer, offset: 0, size: bufferRequirements.size) + return try LowLevelBufferSlice(buffer: buffer, offset: 0, size: bufferRequirements.size) } return try renderContext.makeArgumentTable( @@ -366,7 +366,7 @@ extension WKBridgeUSDConfiguration { get { appRenderer.commandQueue } } @nonobjc - fileprivate final var renderer: _Proto_LowLevelRenderer_v1 { + fileprivate final var renderer: LowLevelRenderer { get { // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 // swift-format-ignore: NeverForceUnwrap @@ -374,7 +374,7 @@ extension WKBridgeUSDConfiguration { } } @nonobjc - fileprivate final var renderContext: any _Proto_LowLevelRenderContext_v1 { + fileprivate final var renderContext: any LowLevelRenderContext { get { // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 // swift-format-ignore: NeverForceUnwrap @@ -383,7 +383,7 @@ extension WKBridgeUSDConfiguration { } @nonobjc - fileprivate final var renderTarget: _Proto_LowLevelRenderTarget_v1.Descriptor { + fileprivate final var renderTarget: LowLevelRenderTarget.Descriptor { get { appRenderer.renderTargetDescriptor } } @@ -419,31 +419,31 @@ extension WKBridgeReceiver { fileprivate let commandQueue: any MTLCommandQueue @nonobjc - fileprivate let renderContext: any _Proto_LowLevelRenderContext_v1 + fileprivate let renderContext: any LowLevelRenderContext @nonobjc - fileprivate let renderer: _Proto_LowLevelRenderer_v1 + fileprivate let renderer: LowLevelRenderer @nonobjc fileprivate let appRenderer: Renderer @nonobjc - fileprivate let lightingFunction: _Proto_LowLevelMaterialResource_v1.LightingFunction + fileprivate let lightingFunction: LowLevelMaterialResource.LightingFunction @nonobjc - fileprivate let lightingArguments: _Proto_LowLevelArgumentTable_v1 + fileprivate let lightingArguments: LowLevelArgumentTable @nonobjc - fileprivate var lightingArgumentBuffer: _Proto_LowLevelArgumentTable_v1? + fileprivate var lightingArgumentBuffer: LowLevelArgumentTable? @nonobjc - fileprivate final var renderTarget: _Proto_LowLevelRenderTarget_v1.Descriptor { + fileprivate final var renderTarget: LowLevelRenderTarget.Descriptor { get { appRenderer.renderTargetDescriptor } } @nonobjc fileprivate var meshInstancePool: MeshInstancePool @nonobjc - fileprivate var meshResources: [WKBridgeTypedResourceId: _Proto_LowLevelMeshResource_v1] = [:] + fileprivate var meshResources: [WKBridgeTypedResourceId: LowLevelMeshResource] = [:] @nonobjc fileprivate var meshResourceToMaterials: [WKBridgeTypedResourceId: [WKBridgeTypedResourceId]] = [:] @nonobjc - fileprivate var meshToMeshInstances: [WKBridgeTypedResourceId: [_Proto_LowLevelMeshInstance_v1]] = [:] + fileprivate var meshToMeshInstances: [WKBridgeTypedResourceId: [LowLevelMeshInstance]] = [:] @nonobjc fileprivate var rotationAngle: Float = 0 @@ -454,16 +454,16 @@ extension WKBridgeReceiver { fileprivate var meshResourceToDeformationContext: [WKBridgeTypedResourceId: DeformationContext] = [:] struct Material { - let resource: _Proto_LowLevelMaterialResource_v1 - let geometryArguments: _Proto_LowLevelArgumentTable_v1? - let surfaceArguments: _Proto_LowLevelArgumentTable_v1? - let blending: _Proto_LowLevelMaterialResource_v1.ShaderGraphOutput.Blending + let resource: LowLevelMaterialResource + let geometryArguments: LowLevelArgumentTable? + let surfaceArguments: LowLevelArgumentTable? + let blending: LowLevelMaterialResource.ShaderGraphOutput.Blending } @nonobjc fileprivate var materialsAndParams: [WKBridgeTypedResourceId: Material] = [:] @nonobjc - fileprivate var textureHashesAndResources: [WKBridgeTypedResourceId: (String, _Proto_LowLevelTextureResource_v1)] = [:] + fileprivate var textureHashesAndResources: [WKBridgeTypedResourceId: (String, LowLevelTextureResource)] = [:] @nonobjc fileprivate var dontCaptureAgain: Bool = false @@ -474,7 +474,7 @@ extension WKBridgeReceiver { } @nonobjc - fileprivate let fallbackTexture: _Proto_LowLevelTextureResource_v1 + fileprivate let fallbackTexture: LowLevelTextureResource struct DeferredMeshUpdate { enum UpdateType { @@ -486,9 +486,9 @@ extension WKBridgeReceiver { let identifier: WKBridgeTypedResourceId let type: UpdateType - var updatedInstances: [_Proto_LowLevelMeshInstance_v1] + var updatedInstances: [LowLevelMeshInstance] - init(identifier: WKBridgeTypedResourceId, type: UpdateType, updatedInstances: [_Proto_LowLevelMeshInstance_v1]) { + init(identifier: WKBridgeTypedResourceId, type: UpdateType, updatedInstances: [LowLevelMeshInstance]) { self.identifier = identifier self.type = type self.updatedInstances = updatedInstances @@ -514,7 +514,7 @@ extension WKBridgeReceiver { self.deformationSystem = try _Proto_LowLevelDeformationSystem_v1.make(configuration.device, configuration.commandQueue).get() let meshInstances = try configuration.renderContext.makeMeshInstanceArray(renderTargets: [configuration.renderTarget], count: 16) self.meshInstancePool = MeshInstancePool(renderContext: configuration.renderContext, meshInstances: meshInstances) - let lightingFunction = configuration.renderContext.makePhysicallyBasedLightingFunction() + let lightingFunction = configuration.renderContext.lighting.makeImageBasedLightingFunction() guard let diffuseTexture = makeTextureFromImageAsset( diffuseAsset, @@ -622,7 +622,7 @@ extension WKBridgeReceiver { let existingTexture = textureHashesAndResources[textureData.identifier]?.1 let needsNewTexture: Bool if let existingTexture { - needsNewTexture = existingTexture.descriptor != _Proto_LowLevelTextureResource_v1.Descriptor(from: asset) + needsNewTexture = existingTexture.descriptor != LowLevelTextureResource.Descriptor(from: asset) } else { needsNewTexture = true } @@ -652,11 +652,14 @@ extension WKBridgeReceiver { let identifier = data.identifier logInfo("updateMaterial \(identifier)") - guard let shaderGraph = _Proto_ShaderNodeGraph.fromWKDescriptor(data.materialGraph) else { + guard let shaderGraph = ShaderGraph.fromWKDescriptor(data.materialGraph) else { fatalError("No materialGraph data provided for material \(identifier)") } - let shaderGraphOutput = try await renderContext.makeShaderGraphFunctions(shaderGraph: shaderGraph) + let shaderGraphOutput = try await renderContext.shaderGraph.makeShaderGraphFunctions( + shaderGraph: shaderGraph, + constantValues: .init() + ) let geometryArguments = try makeParameters( for: shaderGraphOutput.geometryModifier, @@ -736,12 +739,12 @@ extension WKBridgeReceiver { for meshData in updates { let identifier = meshData.identifier - let meshResource: _Proto_LowLevelMeshResource_v1 + let meshResource: LowLevelMeshResource if meshData.updateType == .initial || meshData.descriptor != nil { // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 // swift-format-ignore: NeverForceUnwrap let meshDescriptor = meshData.descriptor! - let descriptor = _Proto_LowLevelMeshResource_v1.Descriptor.fromLlmDescriptor(meshDescriptor) + let descriptor = LowLevelMeshResource.Descriptor.fromLlmDescriptor(meshDescriptor) if let cachedMeshResource = meshResources[identifier] { meshResource = cachedMeshResource } else { @@ -814,20 +817,21 @@ extension WKBridgeReceiver { indexCount: meshData.parts[partIndex].indexCount, primitive: meshData.parts[partIndex].topology, windingOrder: .counterClockwise, - boundsMin: meshData.parts[partIndex].boundsMin, - boundsMax: meshData.parts[partIndex].boundsMax + bounds: .init( + boxMin: meshData.parts[partIndex].boundsMin, + boxMax: meshData.parts[partIndex].boundsMax + ) ) for instanceTransform in meshData.instanceTransforms { - let position = instanceTransform.transformPosition(.zero) let meshInstance = try renderContext.makeMeshInstance( meshPart: meshPart, pipeline: pipeline, geometryArguments: material.geometryArguments, surfaceArguments: material.surfaceArguments, lightingArguments: lightingArguments, - transform: .single(instanceTransform), - sortCategory: material.blending == .transparent ? .transparent(sortPosition: position) : .opaque + transform: instanceTransform, + sortCategory: material.blending == .transparent ? .transparent : .opaque ) // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 @@ -843,7 +847,7 @@ extension WKBridgeReceiver { // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 // swift-format-ignore: NeverForceUnwrap var newTransforms: [simd_float4x4] = [] - var updatedInstances: [_Proto_LowLevelMeshInstance_v1] = [] + var updatedInstances: [LowLevelMeshInstance] = [] let partCount = meshToMeshInstances[identifier]!.count / meshData.instanceTransforms.count for (instanceIndex, instanceTransform) in meshData.instanceTransforms.enumerated() { @@ -879,7 +883,7 @@ extension WKBridgeReceiver { } case .transformUpdate(let newTransforms): for (instanceIndex, meshInstance) in deferredUpdate.updatedInstances.enumerated() { - meshInstance.setTransform(.single(newTransforms[instanceIndex])) + meshInstance.transform = newTransforms[instanceIndex] } } } @@ -930,13 +934,13 @@ extension WKBridgeReceiver { let diffuseMTLTextureDescriptor = try self.imageBasedLightTextureGenerator.makeDiffuseDescriptor( fromCube: cubeMTLTexture ) - let diffuseTextureDescriptor = _Proto_LowLevelTextureResource_v1.Descriptor.from(diffuseMTLTextureDescriptor) + let diffuseTextureDescriptor = LowLevelTextureResource.Descriptor.from(diffuseMTLTextureDescriptor) let diffuseTexture = try self.renderContext.makeTextureResource(descriptor: diffuseTextureDescriptor) let specularMTLTextureDescriptor = try self.imageBasedLightTextureGenerator.makeSpecularDescriptor( fromCube: cubeMTLTexture ) - let specularTextureDescriptor = _Proto_LowLevelTextureResource_v1.Descriptor.from(specularMTLTextureDescriptor) + let specularTextureDescriptor = LowLevelTextureResource.Descriptor.from(specularMTLTextureDescriptor) let specularTexture = try self.renderContext.makeTextureResource(descriptor: specularTextureDescriptor) // FIXME: https://bugs.webkit.org/show_bug.cgi?id=305857 @@ -949,8 +953,8 @@ extension WKBridgeReceiver { into: cubeMTLTexture ) - let diffuseMTLTexture = diffuseTexture.replace(using: commandBuffer) - let specularMTLTexture = specularTexture.replace(using: commandBuffer) + let diffuseMTLTexture = diffuseTexture.replace(commandBuffer: commandBuffer) + let specularMTLTexture = specularTexture.replace(commandBuffer: commandBuffer) try self.imageBasedLightTextureGenerator.generateDiffuse( using: commandBuffer, @@ -963,8 +967,8 @@ extension WKBridgeReceiver { into: specularMTLTexture ) - try self.lightingArguments.setTexture(at: 0, diffuseTexture) - try self.lightingArguments.setTexture(at: 1, specularTexture) + try self.lightingArguments.setTexture(diffuseTexture, at: 0) + try self.lightingArguments.setTexture(specularTexture, at: 1) commandBuffer.commit() } catch { @@ -1349,7 +1353,7 @@ private func texcoordName(for coord: _Proto_TextureCoordinate) -> String { } } -private func textureCoordinate(from name: String) -> _Proto_TextureCoordinate? { +private func textureCoordinate(from name: String) -> ShaderGraph.TextureCoordinate? { switch name { case "UV0": .uv0 case "UV1": .uv1 @@ -1435,23 +1439,23 @@ func webUpdateMaterialRequestFromUpdateMaterialRequest( ) } -extension _Proto_ShaderNodeGraph { - // Reconstructs the graph from the IPC bridge representation using the _Proto_ShaderNodeGraph API. - static func fromWKDescriptor(_ descriptor: WKBridgeMaterialGraph?) -> _Proto_ShaderNodeGraph? { +extension ShaderGraph { + // Reconstructs the graph from the IPC bridge representation using the ShaderGraph API. + static func fromWKDescriptor(_ descriptor: WKBridgeMaterialGraph?) -> ShaderGraph? { guard let descriptor else { return nil } do { - let graph = try _Proto_ShaderNodeGraph( + let graph = try ShaderGraph( named: descriptor.graphName.isEmpty ? "MaterialGraph" : descriptor.graphName, inputs: descriptor.inputs.map { - _Proto_ShaderGraphNodeDefinition.Input( + ShaderGraph.NodeDefinition.Input( name: $0.name, type: fromWKBridgeDataType($0.type), semanticType: $0.semanticTypeName.map { .init(name: $0) } ) }, outputs: descriptor.outputs.map { - _Proto_ShaderGraphNodeDefinition.Output( + ShaderGraph.NodeDefinition.Output( name: $0.name, type: fromWKBridgeDataType($0.type), semanticType: $0.semanticTypeName.map { .init(name: $0) } @@ -1459,19 +1463,19 @@ extension _Proto_ShaderNodeGraph { } ) - let library = _Proto_ShaderNodeGraphLibrary.shared + let library = ShaderGraph.NodeLibrary(version: .materialX138) for bridgeNode in descriptor.nodes { switch bridgeNode.bridgeNodeType { case .constant: if let constant = bridgeNode.constant { - try graph.add(constant: fromWKBridgeConstant(constant), named: constant.name) + try graph.addConstant(fromWKBridgeConstant(constant), named: constant.name) } case .builtin: if let builtin = bridgeNode.builtin, !builtin.definition.isEmpty, let definition = library.definition(named: builtin.definition) { - try graph.add(node: .init(name: builtin.name, data: .definition(definition))) + try graph.addNode(.init(name: builtin.name, data: .definition(definition))) } case .arguments, .results: break @@ -1485,9 +1489,9 @@ extension _Proto_ShaderNodeGraph { do { try graph.connect( bridgeEdge.outputNode, - bridgeEdge.outputPort, + outputPort: bridgeEdge.outputPort, to: bridgeEdge.inputNode, - bridgeEdge.inputPort + inputPort: bridgeEdge.inputPort ) } catch { logError( @@ -1516,7 +1520,7 @@ extension _Proto_ShaderNodeGraph { } } -private func fromWKBridgeDataType(_ dataType: WKBridgeDataType) -> _Proto_ShaderDataType { +private func fromWKBridgeDataType(_ dataType: WKBridgeDataType) -> ShaderGraph.DataType { switch dataType { case .bool: .bool case .uchar: .uchar @@ -1528,6 +1532,8 @@ private func fromWKBridgeDataType(_ dataType: WKBridgeDataType) -> _Proto_Shader case .float: .float case .cgColor3: .cgColor3 case .cgColor4: .cgColor4 + case .color3h: .cgColor3 + case .color4h: .cgColor4 case .float2: .float2 case .float3: .float3 case .float4: .float4 @@ -1541,18 +1547,17 @@ private func fromWKBridgeDataType(_ dataType: WKBridgeDataType) -> _Proto_Shader case .matrix2h: .half2x2 case .matrix3h: .half3x3 case .matrix4h: .half4x4 - case .quat: .quaternion case .surfaceShader: .surfaceShader case .geometryModifier: .geometryModifier case .postLightingShader: .postLightingShader case .string: .string - case .token: .string // Map token to string - case .asset: .filename // Map asset to filename - @unknown default: .invalid + case .asset: .texture + case .token, .quat: .string + @unknown default: fatalError("fromWKBridgeDataType: unknown WKBridgeDataType") } } -private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Proto_ShaderGraphValue { +private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> ShaderGraph.Value { let values = constant.constantValues switch constant.constant { @@ -1570,7 +1575,7 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro return .uint(UInt32(v.number.uintValue)) case .half: guard let v = values.first else { fatalError("fromWKBridgeConstant: missing value for half constant '\(constant.name)'") } - return .half(v.number.uint16Value) + return .half(.init(bitPattern: v.number.uint16Value)) case .float: guard let v = values.first else { fatalError("fromWKBridgeConstant: missing value for float constant '\(constant.name)'") } return .float(v.number.floatValue) @@ -1620,9 +1625,9 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro fatalError("fromWKBridgeConstant: expected 2 values for half2 constant '\(constant.name)', got \(values.count)") } return .half2( - SIMD2( - values[0].number.uint16Value, - values[1].number.uint16Value + SIMD2( + .init(bitPattern: values[0].number.uint16Value), + .init(bitPattern: values[1].number.uint16Value) ) ) case .vector3h, .half3, .point3h, .normal3h, .texCoord3h: @@ -1631,10 +1636,10 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro fatalError("fromWKBridgeConstant: expected 3 values for half3 constant '\(constant.name)', got \(values.count)") } return .half3( - SIMD3( - values[0].number.uint16Value, - values[1].number.uint16Value, - values[2].number.uint16Value + SIMD3( + .init(bitPattern: values[0].number.uint16Value), + .init(bitPattern: values[1].number.uint16Value), + .init(bitPattern: values[2].number.uint16Value) ) ) case .half4: @@ -1642,11 +1647,11 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro fatalError("fromWKBridgeConstant: expected 4 values for half4 constant '\(constant.name)', got \(values.count)") } return .half4( - SIMD4( - values[0].number.uint16Value, - values[1].number.uint16Value, - values[2].number.uint16Value, - values[3].number.uint16Value + SIMD4( + .init(bitPattern: values[0].number.uint16Value), + .init(bitPattern: values[1].number.uint16Value), + .init(bitPattern: values[2].number.uint16Value), + .init(bitPattern: values[3].number.uint16Value) ) ) case .int2: @@ -1688,9 +1693,11 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro fatalError("fromWKBridgeConstant: expected 9 values for matrix3f constant '\(constant.name)', got \(values.count)") } return .float3x3( - SIMD3(values[0].number.floatValue, values[1].number.floatValue, values[2].number.floatValue), - SIMD3(values[3].number.floatValue, values[4].number.floatValue, values[5].number.floatValue), - SIMD3(values[6].number.floatValue, values[7].number.floatValue, values[8].number.floatValue) + .init( + SIMD3(values[0].number.floatValue, values[1].number.floatValue, values[2].number.floatValue), + SIMD3(values[3].number.floatValue, values[4].number.floatValue, values[5].number.floatValue), + SIMD3(values[6].number.floatValue, values[7].number.floatValue, values[8].number.floatValue) + ) ) case .matrix4f: // matrix4f maps to float4x4 - needs 16 values (4 columns of 4 rows each) @@ -1698,29 +1705,31 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro fatalError("fromWKBridgeConstant: expected 16 values for matrix4f constant '\(constant.name)', got \(values.count)") } return .float4x4( - SIMD4( - values[0].number.floatValue, - values[1].number.floatValue, - values[2].number.floatValue, - values[3].number.floatValue - ), - SIMD4( - values[4].number.floatValue, - values[5].number.floatValue, - values[6].number.floatValue, - values[7].number.floatValue - ), - SIMD4( - values[8].number.floatValue, - values[9].number.floatValue, - values[10].number.floatValue, - values[11].number.floatValue - ), - SIMD4( - values[12].number.floatValue, - values[13].number.floatValue, - values[14].number.floatValue, - values[15].number.floatValue + .init( + SIMD4( + values[0].number.floatValue, + values[1].number.floatValue, + values[2].number.floatValue, + values[3].number.floatValue + ), + SIMD4( + values[4].number.floatValue, + values[5].number.floatValue, + values[6].number.floatValue, + values[7].number.floatValue + ), + SIMD4( + values[8].number.floatValue, + values[9].number.floatValue, + values[10].number.floatValue, + values[11].number.floatValue + ), + SIMD4( + values[12].number.floatValue, + values[13].number.floatValue, + values[14].number.floatValue, + values[15].number.floatValue + ) ) ) case .quatf, .quath: @@ -1788,7 +1797,7 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro ] return .cgColor3(CGColor(red: components3f[0], green: components3f[1], blue: components3f[2], alpha: 1.0)) case .color3h: - // USD/MaterialX color3h — half-precision; represented as cgColor3 in _Proto_ShaderGraphValue. + // USD/MaterialX color3h — half-precision; represented as cgColor3 in ShaderGraph.Value. guard values.count >= 3 else { fatalError("fromWKBridgeConstant: expected 3 values for color3h constant '\(constant.name)', got \(values.count)") } @@ -1801,7 +1810,7 @@ private func fromWKBridgeConstant(_ constant: WKBridgeConstantContainer) -> _Pro ) ) case .color4h: - // USD/MaterialX color4h — half-precision; represented as cgColor4 in _Proto_ShaderGraphValue. + // USD/MaterialX color4h — half-precision; represented as cgColor4 in ShaderGraph.Value. guard values.count >= 4 else { fatalError("fromWKBridgeConstant: expected 4 values for color4h constant '\(constant.name)', got \(values.count)") } @@ -2109,13 +2118,13 @@ extension WKBridgeModelLoader { } private func makeFallBackTextureResource( - _ renderContext: any _Proto_LowLevelRenderContext_v1, + _ renderContext: any LowLevelRenderContext, commandQueue: any MTLCommandQueue, device: any MTLDevice, memoryOwner: task_id_token_t -) -> _Proto_LowLevelTextureResource_v1 { +) -> LowLevelTextureResource { // Create 1x1 white fallback texture - let fallbackDescriptor = _Proto_LowLevelTextureResource_v1.Descriptor( + let fallbackDescriptor = LowLevelTextureResource.Descriptor( textureType: .type2D, pixelFormat: .rgba8Unorm, width: 1, @@ -2134,7 +2143,7 @@ private func makeFallBackTextureResource( // Create command buffer to upload white pixel data // swift-format-ignore: NeverForceUnwrap let fallbackCommandBuffer = commandQueue.makeCommandBuffer()! - let fallbackMTLTexture = fallbackTexture.replace(using: fallbackCommandBuffer) + let fallbackMTLTexture = fallbackTexture.replace(commandBuffer: fallbackCommandBuffer) // Use blit encoder to copy from buffer to texture // swift-format-ignore: NeverForceUnwrap From 8f1f5a27db3d8bfef9db2aae60b6b841cb9dd8e1 Mon Sep 17 00:00:00 2001 From: Brent Fulgham Date: Fri, 15 May 2026 13:42:30 -0700 Subject: [PATCH 102/424] Build WebKit with ENABLE_USER_SCRIPT_SANDBOXING=YES https://bugs.webkit.org/show_bug.cgi?id=313032 Reviewed by Elliott Williams. Enable Xcode's user script sandboxing across WTF, JavaScriptCore, WebCore, WebKit, and WebKitLegacy. When sandboxing is enabled, Xcode can run Copy Headers phases in parallel with script phases instead of serializing them, improving build parallelism. The sandbox restricts script phases to reading declared inputs and writing declared outputs. This required three categories of fixes: 1. Script phases that need broad file system access (code generation, validation scripts, entitlement processing) are excluded from sandboxing by name using the EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES build setting. 2. Build rules that invoke `postprocess-header-rule` now declare their WebKitAdditions script dependencies (when present) as inputFiles, allowing the sandbox to grant read access. JavaScriptCore's LLInt-related targets (libJavaScriptCore, JSCLLIntSettingsExtractor, JSCLLIntOffsetsExtractor) require target-level sandbox overrides because their offline assembler build rules dynamically include generated .asm files from DerivedSources that cannot be enumerated as inputFiles. Additionally, undeclared script dependencies were added across all projects: * Tools/Scripts check scripts declared as inputPaths. * Scripts/check-xcfilelists.sh declared as inputPaths. * Cryptex symlink phases declare output paths. * WebCore's SourcesCocoaInternalSDK.txt declared as input to 'Generate Unified Sources'. * JSC's `postprocess-header-rule` declares its script in inputFiles. This change adopts $(WK_WEBKITADDITIONS_HEADERS_FOLDER_PATH), instead of the existing $(BUILT_PRODUCTS_DIR)$(WK_LIBRARY_HEADERS_FOLDER_PATH)/WebKitAdditions, since the former cleanly handles our Internal and Open Source build environments so that we can safely declare project dependencies. Finally, WebKitLegacy's header handling is modified to match the WebKit frameworks header processing. This allows us to opt it into the script sandbox, and to allow Xcode to handle dependency checks to decide which files need copying. * Configurations/CommonBase.xcconfig: * Configurations/WebKitProjectPaths.xcconfig: * Source/JavaScriptCore/Configurations/Base.xcconfig: * Source/JavaScriptCore/Configurations/LLIntExtractor.xcconfig: Added. * Source/JavaScriptCore/Configurations/libJavaScriptCore.xcconfig: * Source/JavaScriptCore/DerivedSources-input.xcfilelist: * Source/JavaScriptCore/DerivedSources.make: * Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj: * Source/WTF/Configurations/Base.xcconfig: * Source/WebCore/Configurations/Base.xcconfig: * Source/WebCore/WebCore.xcodeproj/project.pbxproj: * Source/WebGPU/WebGPU.xcodeproj/project.pbxproj: * Source/WebInspectorUI/Configurations/Base.xcconfig: * Source/WebKit/Configurations/Base.xcconfig: * Source/WebKit/WebKit.xcodeproj/project.pbxproj: * Source/WebKitLegacy/WebKitLegacy.xcodeproj/project.pbxproj: * Source/WebKitLegacy/mac/Configurations/Base.xcconfig: * Source/WebKitLegacy/scripts/postprocess-header-rule: Canonical link: https://commits.webkit.org/313328@main --- Configurations/CommonBase.xcconfig | 1 + Configurations/WebKitProjectPaths.xcconfig | 6 +++ .../Configurations/Base.xcconfig | 2 + .../Configurations/LLIntExtractor.xcconfig | 29 +++++++++++++ .../Configurations/libJavaScriptCore.xcconfig | 2 + .../DerivedSources-input.xcfilelist | 15 ++++++- Source/JavaScriptCore/DerivedSources.make | 13 ++++++ .../JavaScriptCore.xcodeproj/project.pbxproj | 34 +++++++++++---- Source/WTF/Configurations/Base.xcconfig | 2 + Source/WebCore/Configurations/Base.xcconfig | 2 + .../WebCore/WebCore.xcodeproj/project.pbxproj | 12 ++++++ .../WebGPU/WebGPU.xcodeproj/project.pbxproj | 5 +++ .../Configurations/Base.xcconfig | 2 + Source/WebKit/Configurations/Base.xcconfig | 2 + .../WebKit/WebKit.xcodeproj/project.pbxproj | 43 +++++++++++++++++++ .../WebKitLegacy.xcodeproj/project.pbxproj | 12 +++++- .../mac/Configurations/Base.xcconfig | 2 + .../scripts/postprocess-header-rule | 40 ++++++++--------- 18 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 Source/JavaScriptCore/Configurations/LLIntExtractor.xcconfig diff --git a/Configurations/CommonBase.xcconfig b/Configurations/CommonBase.xcconfig index 456e13fe9c4e..69a009f8ce2a 100644 --- a/Configurations/CommonBase.xcconfig +++ b/Configurations/CommonBase.xcconfig @@ -30,6 +30,7 @@ WK_ENABLE_SLOW_BUILD_VERIFICATION = $(WK_ENABLE_SLOW_BUILD_VERIFICATION_$(CONFIGURATION)); WK_ENABLE_SLOW_BUILD_VERIFICATION_Production = YES; +__ALLOW_EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES = YES; // Prefix Definitions // diff --git a/Configurations/WebKitProjectPaths.xcconfig b/Configurations/WebKitProjectPaths.xcconfig index fd3c0f886bc6..cc9974de1603 100644 --- a/Configurations/WebKitProjectPaths.xcconfig +++ b/Configurations/WebKitProjectPaths.xcconfig @@ -80,6 +80,12 @@ UMBRELLA_FRAMEWORKS_DIR_OVERRIDE_YES = $(WK_OVERRIDE_FRAMEWORKS_DIR); WK_WEBKITADDITIONS_HEADERS_FOLDER_PATH = $(BUILT_PRODUCTS_DIR)$(WK_LIBRARY_HEADERS_FOLDER_PATH)/WebKitAdditions; WK_WEBKITADDITIONS_HEADERS_FOLDER_PATH[config=Production] = $(SDK_DIR)$(WK_LIBRARY_HEADERS_FOLDER_PATH)/WebKitAdditions; +WK_WEBKITADDITIONS_POSTPROCESS_SCRIPTS_INPUT = $(WK_WEBKITADDITIONS_POSTPROCESS_SCRIPTS_INPUT_$(USE_INTERNAL_SDK)_$(WK_IS_INSTALLHDRS)); +WK_WEBKITADDITIONS_POSTPROCESS_SCRIPTS_INPUT_YES_NO = $(WK_WEBKITADDITIONS_HEADERS_FOLDER_PATH)/Scripts/postprocess-framework-headers-definitions; + +WK_WEBKITADDITIONS_BRANCH_CONFIG_INPUT = $(WK_WEBKITADDITIONS_BRANCH_CONFIG_INPUT_$(USE_INTERNAL_SDK)_$(WK_IS_INSTALLHDRS)); +WK_WEBKITADDITIONS_BRANCH_CONFIG_INPUT_YES_NO = $(WK_WEBKITADDITIONS_HEADERS_FOLDER_PATH)/Scripts/branch_config.json; + // The root directory of the workspace / WebKit repo is determined by how many directories up "Source" or "Tools" is from the SRCROOT. WK_WORKSPACE_DIR = $(WK_WORKSPACE_DIR_1_$(SRCROOT:dir:standardizepath:file):standardizepath:default=$(WK_WORKSPACE_DIR_2_$(SRCROOT:dir:standardizepath:dir:standardizepath:file):standardizepath:default=$(WK_WORKSPACE_DIR_3_$(SRCROOT:dir:standardizepath:dir:standardizepath:dir:standardizepath:file):standardizepath:default=$(WK_WORKSPACE_DIR_4_$(SRCROOT:dir:standardizepath:dir:standardizepath:dir:standardizepath:dir:standardizepath:file):standardizepath:default=$(WK_WORKSPACE_DIR_5_$(SRCROOT:dir:standardizepath:dir:standardizepath:dir:standardizepath:dir:standardizepath:dir:standardizepath:file):standardizepath:default=$(WK_WORKSPACE_DIR_6_$(SRCROOT:dir:standardizepath:dir:standardizepath:dir:standardizepath:dir:standardizepath:dir:standardizepath:dir:standardizepath:file):standardizepath)))))); WK_WORKSPACE_DIR_1_Source = $(SRCROOT)/../..; diff --git a/Source/JavaScriptCore/Configurations/Base.xcconfig b/Source/JavaScriptCore/Configurations/Base.xcconfig index 737a2c231bf6..17f29820ea2e 100644 --- a/Source/JavaScriptCore/Configurations/Base.xcconfig +++ b/Source/JavaScriptCore/Configurations/Base.xcconfig @@ -24,6 +24,8 @@ #include "../../../Configurations/CommonBase.xcconfig" ALWAYS_SEARCH_USER_PATHS = NO; +ENABLE_USER_SCRIPT_SANDBOXING = YES; +EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES = $(inherited) "Generate Unified Sources" "Generate Derived Sources" "Generate args" "Check For Inappropriate Files In Framework" "Check For Inappropriate Macros in External Headers" "Check For Weak VTables and Externals" "Check For Inappropriate Objective-C Class Names" "Check .xcfilelists" "Copy jsc into JavaScriptCore.framework" "Audit SPI use"; CLANG_CXX_LANGUAGE_STANDARD = c++2b; CLANG_CXX_LIBRARY = libc++; diff --git a/Source/JavaScriptCore/Configurations/LLIntExtractor.xcconfig b/Source/JavaScriptCore/Configurations/LLIntExtractor.xcconfig new file mode 100644 index 000000000000..ce0c691a66fd --- /dev/null +++ b/Source/JavaScriptCore/Configurations/LLIntExtractor.xcconfig @@ -0,0 +1,29 @@ +// Copyright (C) 2026 Apple Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "ToolExecutable.xcconfig" + +// The LLInt offline assembler build rules dynamically include generated .asm +// files from DerivedSources that cannot be enumerated as build rule inputFiles, +// making these targets incompatible with user script sandboxing. +ENABLE_USER_SCRIPT_SANDBOXING = NO; diff --git a/Source/JavaScriptCore/Configurations/libJavaScriptCore.xcconfig b/Source/JavaScriptCore/Configurations/libJavaScriptCore.xcconfig index 72d03cccc496..2f6a46065e4d 100644 --- a/Source/JavaScriptCore/Configurations/libJavaScriptCore.xcconfig +++ b/Source/JavaScriptCore/Configurations/libJavaScriptCore.xcconfig @@ -48,6 +48,8 @@ OTHER_LIPOFLAGS = $(inherited) -fat64; SKIP_INSTALL = YES; +ENABLE_USER_SCRIPT_SANDBOXING = NO; + STRIP_INSTALLED_PRODUCT = NO; EXCLUDED_SOURCE_FILE_NAMES = $(inherited); diff --git a/Source/JavaScriptCore/DerivedSources-input.xcfilelist b/Source/JavaScriptCore/DerivedSources-input.xcfilelist index 55eacb0d378f..39b3254ef8bf 100644 --- a/Source/JavaScriptCore/DerivedSources-input.xcfilelist +++ b/Source/JavaScriptCore/DerivedSources-input.xcfilelist @@ -124,6 +124,7 @@ $(PROJECT_DIR)/inspector/protocol/Worker.json $(PROJECT_DIR)/inspector/scripts/codegen/__init__.py $(PROJECT_DIR)/inspector/scripts/codegen/cpp_generator.py $(PROJECT_DIR)/inspector/scripts/codegen/cpp_generator_templates.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_cpp_alternate_backend_dispatcher_header.py $(PROJECT_DIR)/inspector/scripts/codegen/generate_cpp_backend_dispatcher_header.py $(PROJECT_DIR)/inspector/scripts/codegen/generate_cpp_backend_dispatcher_implementation.py $(PROJECT_DIR)/inspector/scripts/codegen/generate_cpp_frontend_dispatcher_header.py @@ -131,12 +132,25 @@ $(PROJECT_DIR)/inspector/scripts/codegen/generate_cpp_frontend_dispatcher_implem $(PROJECT_DIR)/inspector/scripts/codegen/generate_cpp_protocol_types_header.py $(PROJECT_DIR)/inspector/scripts/codegen/generate_cpp_protocol_types_implementation.py $(PROJECT_DIR)/inspector/scripts/codegen/generate_js_backend_commands.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_backend_dispatcher_header.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_backend_dispatcher_implementation.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_configuration_header.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_configuration_implementation.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_frontend_dispatcher_implementation.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_header.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_internal_header.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_protocol_type_conversions_header.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_protocol_type_conversions_implementation.py +$(PROJECT_DIR)/inspector/scripts/codegen/generate_objc_protocol_types_implementation.py $(PROJECT_DIR)/inspector/scripts/codegen/generator.py $(PROJECT_DIR)/inspector/scripts/codegen/generator_templates.py $(PROJECT_DIR)/inspector/scripts/codegen/models.py +$(PROJECT_DIR)/inspector/scripts/codegen/objc_generator.py +$(PROJECT_DIR)/inspector/scripts/codegen/objc_generator_templates.py $(PROJECT_DIR)/inspector/scripts/codegen/preprocess.pl $(PROJECT_DIR)/inspector/scripts/generate-inspector-protocol-bindings.py $(PROJECT_DIR)/parser/Keywords.table +$(PROJECT_DIR)/parser/generateLexerUnicodePropertyTables.py $(PROJECT_DIR)/runtime/ArrayConstructor.cpp $(PROJECT_DIR)/runtime/AsyncFromSyncIteratorPrototype.cpp $(PROJECT_DIR)/runtime/AsyncGeneratorPrototype.cpp @@ -255,6 +269,5 @@ $(PROJECT_DIR)/wasm/js/WebAssemblyTagPrototype.cpp $(PROJECT_DIR)/wasm/wasm.json $(PROJECT_DIR)/yarr/create_regex_tables $(PROJECT_DIR)/yarr/generateYarrCanonicalizeUnicode -$(PROJECT_DIR)/parser/generateLexerUnicodePropertyTables.py $(PROJECT_DIR)/yarr/generateYarrUnicodePropertyTables.py $(PROJECT_DIR)/yarr/hasher.py diff --git a/Source/JavaScriptCore/DerivedSources.make b/Source/JavaScriptCore/DerivedSources.make index d19d7a50b4f0..c3f42030cd1c 100644 --- a/Source/JavaScriptCore/DerivedSources.make +++ b/Source/JavaScriptCore/DerivedSources.make @@ -323,6 +323,7 @@ INSPECTOR_GENERATOR_SCRIPTS = \ $(JavaScriptCore)/inspector/scripts/codegen/__init__.py \ $(JavaScriptCore)/inspector/scripts/codegen/cpp_generator_templates.py \ $(JavaScriptCore)/inspector/scripts/codegen/cpp_generator.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_alternate_backend_dispatcher_header.py \ $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_backend_dispatcher_header.py \ $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_backend_dispatcher_implementation.py \ $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_frontend_dispatcher_header.py \ @@ -330,9 +331,21 @@ INSPECTOR_GENERATOR_SCRIPTS = \ $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_protocol_types_header.py \ $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_protocol_types_implementation.py \ $(JavaScriptCore)/inspector/scripts/codegen/generate_js_backend_commands.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_backend_dispatcher_header.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_backend_dispatcher_implementation.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_configuration_header.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_configuration_implementation.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_frontend_dispatcher_implementation.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_header.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_internal_header.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_protocol_type_conversions_header.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_protocol_type_conversions_implementation.py \ + $(JavaScriptCore)/inspector/scripts/codegen/generate_objc_protocol_types_implementation.py \ $(JavaScriptCore)/inspector/scripts/codegen/generator_templates.py \ $(JavaScriptCore)/inspector/scripts/codegen/generator.py \ $(JavaScriptCore)/inspector/scripts/codegen/models.py \ + $(JavaScriptCore)/inspector/scripts/codegen/objc_generator.py \ + $(JavaScriptCore)/inspector/scripts/codegen/objc_generator_templates.py \ $(JavaScriptCore)/inspector/scripts/generate-inspector-protocol-bindings.py \ $(JavaScriptCore_SCRIPTS_DIR)/generate-combined-inspector-json.py \ # diff --git a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj index 7d34156d8384..f82a00b8a9d4 100644 --- a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj +++ b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj @@ -2488,6 +2488,9 @@ filePatterns = "*.h"; fileType = pattern.proxy; inputFiles = ( + "$(SRCROOT)/Scripts/postprocess-header-rule", + "$(WK_WEBKITADDITIONS_POSTPROCESS_SCRIPTS_INPUT)", + "$(WK_WEBKITADDITIONS_BRANCH_CONFIG_INPUT)", ); isEditable = 1; outputFiles = ( @@ -5681,6 +5684,7 @@ ADE8029D1E08F2260058DE78 /* WebAssemblyLinkErrorConstructor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = WebAssemblyLinkErrorConstructor.cpp; path = js/WebAssemblyLinkErrorConstructor.cpp; sourceTree = ""; }; AEDA37D0C69F462084E47142 /* WeakInlinesLight.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WeakInlinesLight.h; sourceTree = ""; }; BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ToolExecutable.xcconfig; sourceTree = ""; }; + DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = LLIntExtractor.xcconfig; sourceTree = ""; }; BC02E9040E1839DB000F9297 /* ErrorConstructor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ErrorConstructor.cpp; sourceTree = ""; }; BC02E9050E1839DB000F9297 /* ErrorConstructor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ErrorConstructor.h; sourceTree = ""; }; BC02E9060E1839DB000F9297 /* ErrorPrototype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ErrorPrototype.cpp; sourceTree = ""; }; @@ -7926,6 +7930,7 @@ 1C9051430BA9E8A70081E9D0 /* JavaScriptCore.xcconfig */, 5DAFD6CB146B686300FBEFB4 /* JSC.xcconfig */, 44F93DFD2AE71EBD00FFA37C /* libJavaScriptCore.xcconfig */, + DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */, FEE0A12229FE250400CED5E4 /* TestExecutable.xcconfig */, BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */, ); @@ -13354,6 +13359,7 @@ ); name = "Add Symlink in /System/Library/PrivateFrameworks"; outputPaths = ( + "$(INSTALL_ROOT)/$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks/JavaScriptCore.framework", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; @@ -13366,6 +13372,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-objc-class-names", ); name = "Check For Inappropriate Objective-C Class Names"; outputPaths = ( @@ -13401,6 +13408,7 @@ ); inputPaths = ( "$(SRCROOT)/DerivedSources.make", + "$(SRCROOT)/Scripts/check-xcfilelists.sh", ); name = "Check .xcfilelists"; outputFileListPaths = ( @@ -13461,6 +13469,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-weak-vtables-and-externals", ); name = "Check For Weak VTables and Externals"; outputPaths = ( @@ -13510,11 +13519,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -13548,6 +13559,7 @@ ); inputPaths = ( "$(CPP_HEADERMAP_PRODUCT_HEADERS_VFS_FILE)", + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-macros-in-external-headers", ); name = "Check For Inappropriate Macros in External Headers"; outputPaths = ( @@ -13567,6 +13579,11 @@ ); inputPaths = ( "$(WTF_BUILD_SCRIPTS_DIR)/audit-spi-if-needed.sh", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/platform-enabled-swift-args.arm64.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/platform-enabled-swift-args.arm64_32.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/platform-enabled-swift-args.arm64e.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/platform-enabled-swift-args.armv7k.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/platform-enabled-swift-args.x86_64.resp", ); name = "Audit SPI use"; outputFileListPaths = ( @@ -13654,6 +13671,7 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-files-in-framework", ); name = "Check For Inappropriate Files In Framework"; outputFileListPaths = ( @@ -14557,28 +14575,28 @@ }; 0FF922CB14F46B130041A24E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { }; name = Debug; }; 0FF922CC14F46B130041A24E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { }; name = Release; }; 0FF922CD14F46B130041A24E /* Profiling */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { }; name = Profiling; }; 0FF922CE14F46B130041A24E /* Production */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { }; name = Production; @@ -14716,7 +14734,7 @@ }; 14BD688A215191310050DAFF /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -14724,7 +14742,7 @@ }; 14BD688B215191310050DAFF /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -14732,7 +14750,7 @@ }; 14BD688C215191310050DAFF /* Profiling */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -14740,7 +14758,7 @@ }; 14BD688D215191310050DAFF /* Production */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC021BF2136900C300FC5467 /* ToolExecutable.xcconfig */; + baseConfigurationReference = DD8A31502F17A00000000001 /* LLIntExtractor.xcconfig */; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; }; diff --git a/Source/WTF/Configurations/Base.xcconfig b/Source/WTF/Configurations/Base.xcconfig index 22be123a123a..819b54f990c0 100644 --- a/Source/WTF/Configurations/Base.xcconfig +++ b/Source/WTF/Configurations/Base.xcconfig @@ -26,6 +26,8 @@ CODE_SIGN_IDENTITY = -; ALWAYS_SEARCH_USER_PATHS = NO; +ENABLE_USER_SCRIPT_SANDBOXING = YES; +EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES = $(inherited) "Generate Unified Sources" "Generate Derived Sources" "Generate args" "Check For Inappropriate Files In Framework" "Check .xcfilelists" "Symlink public SDK headers"; CLANG_CXX_LANGUAGE_STANDARD = c++2b; CLANG_CXX_LIBRARY = libc++; diff --git a/Source/WebCore/Configurations/Base.xcconfig b/Source/WebCore/Configurations/Base.xcconfig index c2441a901234..9c09229b4b09 100644 --- a/Source/WebCore/Configurations/Base.xcconfig +++ b/Source/WebCore/Configurations/Base.xcconfig @@ -27,6 +27,8 @@ CODE_SIGN_IDENTITY = -; AD_HOC_CODE_SIGNING_ALLOWED = YES; ALWAYS_SEARCH_USER_PATHS = NO; +ENABLE_USER_SCRIPT_SANDBOXING = YES; +EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES = $(inherited) "Generate Unified Sources" "Generate Derived Sources" "Generate args" "Check For Inappropriate Files In Framework" "Check For Inappropriate Macros in External Headers" "Check For Weak VTables and Externals" "Check For Inappropriate Objective-C Class Names" "Check .xcfilelists" "Copy Profiling Data" "Audit SPI use"; CLANG_CXX_LANGUAGE_STANDARD = c++2b; CLANG_CXX_LIBRARY = libc++; diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj index bb1375d7909a..990d557654fe 100644 --- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj +++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj @@ -50137,6 +50137,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-objc-class-names", ); name = "Check For Inappropriate Objective-C Class Names"; outputPaths = ( @@ -50195,6 +50196,7 @@ "$(SRCROOT)/Scripts/generate-unified-sources.sh", "$(SRCROOT)/Sources.txt", "$(SRCROOT)/SourcesCocoa.txt", + "$(SRCROOT)/SourcesCocoaInternalSDK.txt", "$(SRCROOT)/platform/SourcesLibWebRTC.txt", "$(BUILT_PRODUCTS_DIR)$(WK_LIBRARY_HEADERS_FOLDER_PATH)/wtf/Scripts/generate-unified-source-bundles.py", ); @@ -50217,6 +50219,7 @@ ); inputPaths = ( "$(SRCROOT)/DerivedSources.make", + "$(SRCROOT)/Scripts/check-xcfilelists.sh", ); name = "Check .xcfilelists"; outputFileListPaths = ( @@ -50237,6 +50240,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-weak-vtables-and-externals", ); name = "Check For Weak VTables and Externals"; outputPaths = ( @@ -50253,6 +50257,7 @@ files = ( ); inputPaths = ( + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-files-in-framework", ); name = "Check For Inappropriate Files In Framework"; outputPaths = ( @@ -50269,11 +50274,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -50372,6 +50379,11 @@ ); inputPaths = ( "$(WTF_BUILD_SCRIPTS_DIR)/audit-spi-if-needed.sh", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/platform-enabled-swift-args.arm64.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/platform-enabled-swift-args.arm64_32.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/platform-enabled-swift-args.arm64e.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/platform-enabled-swift-args.armv7k.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/platform-enabled-swift-args.x86_64.resp", ); name = "Audit SPI use"; outputFileListPaths = ( diff --git a/Source/WebGPU/WebGPU.xcodeproj/project.pbxproj b/Source/WebGPU/WebGPU.xcodeproj/project.pbxproj index 305febfc0638..e37a60481498 100644 --- a/Source/WebGPU/WebGPU.xcodeproj/project.pbxproj +++ b/Source/WebGPU/WebGPU.xcodeproj/project.pbxproj @@ -1376,6 +1376,11 @@ ); inputPaths = ( "$(WTF_BUILD_SCRIPTS_DIR)/audit-spi-if-needed.sh", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebGPU/platform-enabled-swift-args.arm64.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebGPU/platform-enabled-swift-args.arm64_32.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebGPU/platform-enabled-swift-args.arm64e.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebGPU/platform-enabled-swift-args.armv7k.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebGPU/platform-enabled-swift-args.x86_64.resp", ); name = "Audit SPI use"; outputFileListPaths = ( diff --git a/Source/WebInspectorUI/Configurations/Base.xcconfig b/Source/WebInspectorUI/Configurations/Base.xcconfig index 482c53d3cdbb..7358f94b0363 100644 --- a/Source/WebInspectorUI/Configurations/Base.xcconfig +++ b/Source/WebInspectorUI/Configurations/Base.xcconfig @@ -30,6 +30,8 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf-with-dsym; ALWAYS_SEARCH_USER_PATHS = NO; +ENABLE_USER_SCRIPT_SANDBOXING = YES; +EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES = $(inherited) "Copy User Interface Resources"; SYSTEM_FRAMEWORK_SEARCH_PATHS = $(WK_QUOTED_OVERRIDE_FRAMEWORKS_DIR) $(inherited) $(WK_PRIVATE_SDK_DIR)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks; CLANG_CXX_LANGUAGE_STANDARD = c++2b; diff --git a/Source/WebKit/Configurations/Base.xcconfig b/Source/WebKit/Configurations/Base.xcconfig index 0217d3c1e141..37be32d7e045 100644 --- a/Source/WebKit/Configurations/Base.xcconfig +++ b/Source/WebKit/Configurations/Base.xcconfig @@ -27,6 +27,8 @@ ENABLE_DAEMON_SYMLINKS = $(WK_NOT_$(USE_STAGING_INSTALL_PATH)) ENABLE_DAEMON_SYMLINKS[sdk=embedded*] = NO ALWAYS_SEARCH_USER_PATHS = NO; +ENABLE_USER_SCRIPT_SANDBOXING = YES; +EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES = $(inherited) "Generate Unified Sources" "Generate Derived Sources" "Generate args" "Generate Swift platform args" "Generate Swift TBA availability macros" "Check For Inappropriate Files In Framework" "Check For Inappropriate Macros in External Headers" "Check For Framework Include Consistency" "Check For Weak VTables and Externals" "Check For Inappropriate Objective-C Class Names" "Check .xcfilelists" "Migrate WebCore and WebKitLegacy Headers" "Copy Custom WebContent Resources to Framework Private Headers" "Copy Profiling Data" "Process WebContent entitlements" "Process Network entitlements" "Process GPU entitlements" "Process Model entitlements" "Process webpushd Entitlements" "Process adattributiond Entitlements" "Process entitlements" "Audit SPI use"; CLANG_CXX_LANGUAGE_STANDARD = c++2b; CLANG_CXX_LIBRARY = libc++; diff --git a/Source/WebKit/WebKit.xcodeproj/project.pbxproj b/Source/WebKit/WebKit.xcodeproj/project.pbxproj index 5ec52b08f961..904b2fd51cb3 100644 --- a/Source/WebKit/WebKit.xcodeproj/project.pbxproj +++ b/Source/WebKit/WebKit.xcodeproj/project.pbxproj @@ -2799,6 +2799,8 @@ inputFiles = ( "$(SRCROOT)/Scripts/postprocess-header-rule", "$(SRCROOT)/mac/replace-webkit-additions-includes.py", + "$(WK_WEBKITADDITIONS_POSTPROCESS_SCRIPTS_INPUT)", + "$(WK_WEBKITADDITIONS_BRANCH_CONFIG_INPUT)", ); isEditable = 1; outputFiles = ( @@ -20452,11 +20454,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -20486,6 +20490,7 @@ ); name = "Add Symlink in /System/Library/PrivateFrameworks"; outputPaths = ( + "$(INSTALL_ROOT)/$(WK_ALTERNATE_FRAMEWORKS_DIR)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks/WebKit.framework", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; @@ -20530,11 +20535,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -20628,6 +20635,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-objc-class-names", ); name = "Check For Inappropriate Objective-C Class Names"; outputPaths = ( @@ -20797,6 +20805,7 @@ ); inputPaths = ( "$(SRCROOT)/DerivedSources.make", + "$(SRCROOT)/Scripts/check-xcfilelists.sh", ); name = "Check .xcfilelists"; outputFileListPaths = ( @@ -20837,11 +20846,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create symlink"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -20874,11 +20885,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -20975,6 +20988,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-weak-vtables-and-externals", ); name = "Check For Weak VTables and Externals"; outputPaths = ( @@ -20991,6 +21005,7 @@ ); inputPaths = ( "$(CPP_HEADERMAP_PRODUCT_HEADERS_VFS_FILE)", + "$(SRCROOT)/../../Tools/Scripts/check-for-webkit-framework-include-consistency", ); name = "Check For Framework Include Consistency"; outputPaths = ( @@ -21050,11 +21065,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21068,11 +21085,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21086,11 +21105,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21104,11 +21125,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21122,11 +21145,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21140,11 +21165,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21158,11 +21185,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create Symlink to Cryptex Path"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21300,6 +21329,7 @@ ); inputPaths = ( "$(CPP_HEADERMAP_PRODUCT_HEADERS_VFS_FILE)", + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-macros-in-external-headers", ); name = "Check For Inappropriate Macros in External Headers"; outputPaths = ( @@ -21427,6 +21457,11 @@ ); inputPaths = ( "$(WTF_BUILD_SCRIPTS_DIR)/audit-spi-if-needed.sh", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKit/platform-enabled-swift-args.arm64.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKit/platform-enabled-swift-args.arm64_32.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKit/platform-enabled-swift-args.arm64e.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKit/platform-enabled-swift-args.armv7k.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKit/platform-enabled-swift-args.x86_64.resp", ); name = "Audit SPI use"; outputFileListPaths = ( @@ -21559,11 +21594,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create symlink"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21577,11 +21614,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create symlink"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21595,11 +21634,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create symlink"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; @@ -21613,11 +21654,13 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/Scripts/create-symlink-to-cryptex.sh", ); name = "Create symlink"; outputFileListPaths = ( ); outputPaths = ( + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/zsh; diff --git a/Source/WebKitLegacy/WebKitLegacy.xcodeproj/project.pbxproj b/Source/WebKitLegacy/WebKitLegacy.xcodeproj/project.pbxproj index 22378979254a..e6c160756340 100644 --- a/Source/WebKitLegacy/WebKitLegacy.xcodeproj/project.pbxproj +++ b/Source/WebKitLegacy/WebKitLegacy.xcodeproj/project.pbxproj @@ -3248,6 +3248,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-objc-class-names", ); name = "Check For Inappropriate Objective-C Class Names"; outputPaths = ( @@ -3266,6 +3267,7 @@ inputFileListPaths = ( ); inputPaths = ( + "$(SRCROOT)/../../Tools/Scripts/check-for-inappropriate-files-in-framework", ); name = "Check For Inappropriate Files In Framework"; outputFileListPaths = ( @@ -3285,6 +3287,7 @@ ); inputPaths = ( "$(SRCROOT)/DerivedSources.make", + "$(SRCROOT)/scripts/check-xcfilelists.sh", ); name = "Check .xcfilelists"; outputFileListPaths = ( @@ -3342,6 +3345,7 @@ ); inputPaths = ( "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(SRCROOT)/../../Tools/Scripts/check-for-weak-vtables-and-externals", ); name = "Check For Weak VTables and Externals"; outputPaths = ( @@ -3364,7 +3368,7 @@ outputFileListPaths = ( ); outputPaths = ( - "${OUTPUT_CRYPTEX_SYMLINK_PATH}", + "$(OUTPUT_CRYPTEX_SYMLINK_PATH)", ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; @@ -3380,6 +3384,7 @@ inputPaths = ( "$(WTF_BUILD_SCRIPTS_DIR)/Preferences/UnifiedWebPreferences.yaml", "$(WTF_BUILD_SCRIPTS_DIR)/GeneratePreferences.rb", + "$(SRCROOT)/mac/Scripts/generate-preferences.sh", "$(SRCROOT)/mac/Scripts/PreferencesTemplates/WebPreferencesDefinitions.h.erb", "$(SRCROOT)/mac/Scripts/PreferencesTemplates/WebPreferencesExperimentalFeatures.mm.erb", "$(SRCROOT)/mac/Scripts/PreferencesTemplates/WebPreferencesInternalFeatures.mm.erb", @@ -3476,6 +3481,11 @@ ); inputPaths = ( "$(WTF_BUILD_SCRIPTS_DIR)/audit-spi-if-needed.sh", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKitLegacy/platform-enabled-swift-args.arm64.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKitLegacy/platform-enabled-swift-args.arm64_32.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKitLegacy/platform-enabled-swift-args.arm64e.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKitLegacy/platform-enabled-swift-args.armv7k.resp", + "$(BUILT_PRODUCTS_DIR)/DerivedSources/WebKitLegacy/platform-enabled-swift-args.x86_64.resp", ); name = "Audit SPI use"; outputFileListPaths = ( diff --git a/Source/WebKitLegacy/mac/Configurations/Base.xcconfig b/Source/WebKitLegacy/mac/Configurations/Base.xcconfig index 1c0f40d78b9d..dc57a639de36 100644 --- a/Source/WebKitLegacy/mac/Configurations/Base.xcconfig +++ b/Source/WebKitLegacy/mac/Configurations/Base.xcconfig @@ -27,6 +27,8 @@ CODE_SIGN_IDENTITY = -; AD_HOC_CODE_SIGNING_ALLOWED = YES; ALWAYS_SEARCH_USER_PATHS = NO; +ENABLE_USER_SCRIPT_SANDBOXING = YES; +EXCLUDED_USER_SCRIPT_SANDBOXING_PHASE_NAMES = $(inherited) "Generate Unified Sources" "Generate Derived Sources" "Generate args" "Check For Inappropriate Files In Framework" "Check For Inappropriate Macros in External Headers" "Check For Weak VTables and Externals" "Check For Inappropriate Objective-C Class Names" "Check .xcfilelists" "Migrate WebCore Headers and Generate Export List" "Generate Preferences" "Audit SPI use" "Work around rdar://109484516"; CLANG_CXX_LANGUAGE_STANDARD = c++2b; CLANG_CXX_LIBRARY = libc++; diff --git a/Source/WebKitLegacy/scripts/postprocess-header-rule b/Source/WebKitLegacy/scripts/postprocess-header-rule index 754602b3bee0..1fb17f98dcd3 100755 --- a/Source/WebKitLegacy/scripts/postprocess-header-rule +++ b/Source/WebKitLegacy/scripts/postprocess-header-rule @@ -28,24 +28,20 @@ if [[ -z "${SCRIPT_HEADER_VISIBILITY}" ]]; then exit 0 fi -SOURCE_FILE="${SCRIPT_INPUT_FILE}" -WORK_FILE="${SCRIPT_OUTPUT_FILE_0}.tmp" -DEST_FILE="${SCRIPT_OUTPUT_FILE_0}" - -ditto "${SOURCE_FILE}" "${WORK_FILE}" - if [[ ${USE_INTERNAL_SDK} == "YES" ]]; then USE_APPLE_INTERNAL_SDK=1 else USE_APPLE_INTERNAL_SDK=0 fi +UNIFDEF_OPTIONS=(-B) + if [[ ${WK_PLATFORM_NAME} == macosx ]]; then - unifdefOptions="-DTARGET_OS_IPHONE=0 -DTARGET_OS_SIMULATOR=0" + UNIFDEF_OPTIONS+=(-DTARGET_OS_IPHONE=0 -DTARGET_OS_SIMULATOR=0) elif [[ ${WK_PLATFORM_NAME} == *simulator* ]]; then - unifdefOptions="-DTARGET_OS_IPHONE=1 -DTARGET_OS_SIMULATOR=1 -DUSE_APPLE_INTERNAL_SDK=${USE_APPLE_INTERNAL_SDK}" + UNIFDEF_OPTIONS+=(-DTARGET_OS_IPHONE=1 -DTARGET_OS_SIMULATOR=1 "-DUSE_APPLE_INTERNAL_SDK=${USE_APPLE_INTERNAL_SDK}") else - unifdefOptions="-DTARGET_OS_IPHONE=1 -DTARGET_OS_SIMULATOR=0 -DUSE_APPLE_INTERNAL_SDK=${USE_APPLE_INTERNAL_SDK}" + UNIFDEF_OPTIONS+=(-DTARGET_OS_IPHONE=1 -DTARGET_OS_SIMULATOR=0 "-DUSE_APPLE_INTERNAL_SDK=${USE_APPLE_INTERNAL_SDK}") fi # FIXME: We should consider making this logic general purpose so as to support keeping or removing @@ -56,25 +52,27 @@ do # We assume a disabled feature is either undefined or has the empty string as its value. eval "isFeatureEnabled=\$$featureDefine" if [[ -z $isFeatureEnabled ]]; then - unifdefOptions="$unifdefOptions -D$featureDefine=0" + UNIFDEF_OPTIONS+=("-D$featureDefine=0") else - unifdefOptions="$unifdefOptions -D$featureDefine=1" + UNIFDEF_OPTIONS+=("-D$featureDefine=1") fi done -unifdef -B ${unifdefOptions} -o "${WORK_FILE}.unifdef" "${WORK_FILE}" -case $? in - 0) rm "${WORK_FILE}".unifdef ;; - 1) mv "${WORK_FILE}"{.unifdef,} ;; - *) exit 1 ;; -esac +SED_OPTIONS=() -HEADER_NAME=$(basename "${WORK_FILE}" '.tmp') +HEADER_NAME=$(basename "${SCRIPT_INPUT_FILE}") if [[ "${HEADER_NAME}" != "WebKitAvailability.h" ]]; then if [[ ${WK_PLATFORM_NAME} != macosx ]]; then - sed -i '' -E -e "s/ *WEBKIT_((CLASS_|ENUM_)?(AVAILABLE|DEPRECATED))_MAC\([^)]+\)//g" "${WORK_FILE}" + SED_OPTIONS+=(-e "s/ *WEBKIT_((CLASS_|ENUM_)?(AVAILABLE|DEPRECATED))_MAC\([^)]+\)//g") fi fi -cmp -s "${WORK_FILE}" "${DEST_FILE}" && rm -f "${WORK_FILE}" || mv "${WORK_FILE}" "${DEST_FILE}" -[[ "${SOURCE_FILE}" -nt "${DEST_FILE}" ]] && touch "${DEST_FILE}" || true +if [[ ${#SED_OPTIONS[@]} -gt 0 ]]; then + sed -E "${SED_OPTIONS[@]}" < "${SCRIPT_INPUT_FILE}" | + unifdef ${UNIFDEF_OPTIONS[@]} > "${SCRIPT_OUTPUT_FILE_0}" + exits=(${PIPESTATUS[@]}) + [ ${exits[0]} -eq 0 -a ${exits[1]} -lt 2 ] +else + unifdef ${UNIFDEF_OPTIONS[@]} < "${SCRIPT_INPUT_FILE}" > "${SCRIPT_OUTPUT_FILE_0}" + [ $? -lt 2 ] +fi From ffe69f5458b13f0741406f8425b0fe4f2da15203 Mon Sep 17 00:00:00 2001 From: Tyler Wilcock Date: Fri, 15 May 2026 14:22:24 -0700 Subject: [PATCH 103/424] AX: VoiceOver bounding boxes are wrong on cross-origin frames when isolated tree mode is off https://bugs.webkit.org/show_bug.cgi?id=314895 rdar://177171633 Reviewed by Dominic Mazzoni. When a WebPage is initialized in a WebProcess where accessibility was already enabled process-wide (e.g. via WebProcess::setEnhancedAccessibility before the WebPage existed), AXObjectCache::gAccessibilityMode has already transitioned to non-Off. Because that transition fired before WebPage::initialize ran, the sync callback registered by setSyncModeToOtherProcessesCallback wasn't installed yet, so the SetAccessibilityMode IPC was never sent to the UIProcess. The new WebPageProxy's m_accessibilityMode stays at Off, and every WebPageProxy::requestFrameScreenPosition for that frame early-returns. The AccessibilityScrollView's frame geometry never gets populated, and AccessibilityObject::convertFrameToSpace falls back to a default-constructed AXFrameGeometry with screenPosition=(0,0), producing incorrect screen-relative positions for every element in the frame. Fix it by syncing the current process-wide accessibility mode to the UIProcess WebPageProxy at the end of WebPage::initialize, immediately after registering the sync callback, when the mode is already non-Off. This is difficult to capture in a layout test, so no new test is added. Fix was confirmed manually. * Source/WebKit/WebProcess/WebPage/WebPage.cpp: Canonical link: https://commits.webkit.org/313329@main --- Source/WebKit/WebProcess/WebPage/WebPage.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp index 41b7742f7220..72aa1db3d0e1 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp @@ -1121,6 +1121,16 @@ WebPage::WebPage(PageIdentifier pageID, WebPageCreationParameters&& parameters) page->send(Messages::WebPageProxy::SetAccessibilityMode(mode)); }); + if (auto mode = WebCore::AXObjectCache::accessibilityMode(); !WebCore::isAccessibilityModeOff(mode)) { + // If accessibility was already enabled process-wide before this WebPage was + // created (e.g. WebProcess::setEnhancedAccessibility ran before initialize), + // the mode transition fired before the sync callback above was registered, so + // the new WebPageProxy on the UIProcess never learned about it. Sync the + // current mode now so requestFrameScreenPosition and other AX-mode-gated + // IPCs aren't dropped. + send(Messages::WebPageProxy::SetAccessibilityMode(mode)); + } + #if PLATFORM(MAC) if (WebCore::AXObjectCache::shouldForceAccessibilityEnabled()) WebCore::AXObjectCache::enableAccessibility(WebCore::AXObjectCache::ForceAXThreadMode::Yes); From c57f26e01334966d2cb89858e1cf54ed729b1f38 Mon Sep 17 00:00:00 2001 From: Nikolas Zimmermann Date: Fri, 15 May 2026 14:37:07 -0700 Subject: [PATCH 104/424] [GTK][WPE][Tools] container-sdk-autoenter fails on bots with /dev/console mount error https://bugs.webkit.org/show_bug.cgi?id=314907 Reviewed by Carlos Alberto Lopez Perez. In nested rootless setups (LXC, k8s pods, Docker/podman) crun aborts container startup with `mount /dev/pts/N to /dev/console: No such file or directory`. Root cause: binding the host's entire /dev over the container's (`-v /dev:/dev:rslave`) shadows the container's /dev, so under --tty crun cannot create /dev/console from the allocated pty -- a nested pod's /dev has none for it to bind onto. Generalize the prior LXC-only check to a `_running_inside_container()` helper and use it to skip only the wholesale host /dev bind in that case, letting crun own /dev. --tty and the container's own devpts are kept, so interactive shells still get a controlling terminal on bots. GPU (/dev/dri) and e.g. gamepads (/dev/input) are passed through explicitly. * Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py: (_running_inside_container): (_build_podman_run_args): (maybe_enter_webkit_container_sdk): Canonical link: https://commits.webkit.org/313330@main --- .../port/linux_container_sdk_utils.py | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py index 663cd2d60acc..901d438b991b 100644 --- a/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py +++ b/Tools/Scripts/webkitpy/port/linux_container_sdk_utils.py @@ -174,6 +174,25 @@ def _container_hostname(): return hostname +def _running_inside_container(): + """True if this process is itself already inside a container. Selects the + device-passthrough strategy in `_build_podman_run_args`: a nested rootless + pod's /dev has no /dev/console, so crun cannot create one when we bind the + host's entire /dev over the container's. Inside a container we instead let + crun own /dev (so --tty works) and re-expose host devices explicitly.""" + # Cheap marker-file check first; fall back to systemd-detect-virt for + # minimal pod images that omit it. + if os.path.exists('/run/.containerenv') or os.path.exists('/.dockerenv'): + return True + try: + virt = subprocess.run(['systemd-detect-virt', '--container'], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL).stdout.decode().strip() + except FileNotFoundError: + return False + return bool(virt) and virt != 'none' + + def _build_podman_run_args(pinned_version): """Static `podman run` flags for an ephemeral wkdev-build container. @@ -249,23 +268,19 @@ def _build_podman_run_args(pinned_version): if os.path.exists(src): args += _bind_mount(src, dst, options='ro') - # Give the container its own devpts so crun does not fail to chown the - # caller's host /dev/pts/N when setting up the controlling TTY in rootless - # mode. Skipped under LXC, which does not permit a nested devpts mount. - try: - virt = subprocess.run(['systemd-detect-virt'], stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL).stdout.decode().strip() - except FileNotFoundError: - virt = '' - if virt != 'lxc': - args += ['--mount', 'type=devpts,destination=/dev/pts'] - - # Devices: gamepads, GPU, NVIDIA via CDI when available - args += ['-v', '/dev:/dev:rslave'] + # Devices: gamepads, GPU, NVIDIA via CDI when available. Binding the host's + # entire /dev shadows the container's, so under --tty crun cannot create + # /dev/console from the pty in a nested rootless pod (the pod's /dev has + # none). Inside a container, let crun own /dev; GPU is passed explicitly. + if not _running_inside_container(): + args += ['-v', '/dev:/dev:rslave'] + args += ['--mount', 'type=devpts,destination=/dev/pts'] if os.path.isdir('/run/udev'): args += ['-v', '/run/udev:/run/udev'] if os.path.isdir('/dev/dri'): args += ['--device', '/dev/dri'] + if os.path.isdir('/dev/input'): + args += ['--device', '/dev/input'] if os.path.exists('/etc/cdi/nvidia.yaml') or os.path.exists('/var/run/cdi/nvidia.yaml'): args += ['--device', 'nvidia.com/gpu=all'] @@ -451,7 +466,7 @@ def maybe_enter_webkit_container_sdk(argv=None): container_command, ] + argv[1:] - print('Running inside WebKit Container SDK wkdev-sdk {}.'.format(pinned_version), file=sys.stderr) + print('Launching WebKit Container SDK wkdev-build {}.'.format(pinned_version), file=sys.stderr) sys.stdout.flush() sys.stderr.flush() os.execvp('podman', run_cmd) From afa23d7ec9e8ccdea4adba93bc10eb242049895c Mon Sep 17 00:00:00 2001 From: Alex Christensen Date: Fri, 15 May 2026 15:00:04 -0700 Subject: [PATCH 105/424] Remove unused ProvisionalFrameProxy.needsMainFrameObserver https://bugs.webkit.org/show_bug.cgi?id=314874 rdar://177133746 Reviewed by Chris Dumez. * Source/WebKit/UIProcess/ProvisionalPageProxy.cpp: (WebKit::ProvisionalPageProxy::ProvisionalPageProxy): (WebKit::ProvisionalPageProxy::initializeWebPage): * Source/WebKit/UIProcess/ProvisionalPageProxy.h: Canonical link: https://commits.webkit.org/313331@main --- Source/WebKit/UIProcess/ProvisionalPageProxy.cpp | 3 --- Source/WebKit/UIProcess/ProvisionalPageProxy.h | 2 -- 2 files changed, 5 deletions(-) diff --git a/Source/WebKit/UIProcess/ProvisionalPageProxy.cpp b/Source/WebKit/UIProcess/ProvisionalPageProxy.cpp index 32777660d76a..c0e3398f891b 100644 --- a/Source/WebKit/UIProcess/ProvisionalPageProxy.cpp +++ b/Source/WebKit/UIProcess/ProvisionalPageProxy.cpp @@ -130,7 +130,6 @@ ProvisionalPageProxy::ProvisionalPageProxy(WebPageProxy& page, Ref suspendedPage->unsuspend(navigation.targetItem()->mainFrameItem().identifier()); m_mainFrame = suspendedPage->mainFrame(); m_mainFrame->updateReferrerPolicy(ReferrerPolicy::EmptyString); - m_needsMainFrameObserver = true; } else if (m_shouldReuseMainFrame) { m_mainFrame = page.mainFrame(); m_mainFrame->updateReferrerPolicy(ReferrerPolicy::EmptyString); @@ -139,7 +138,6 @@ ProvisionalPageProxy::ProvisionalPageProxy(WebPageProxy& page, Ref // as some clients may rely on it until the next load is committed. Ref mainFrame = WebFrameProxy::create(page, m_frameProcess, generateFrameIdentifier(), previousMainFrame->effectiveSandboxFlags(), ReferrerPolicy::EmptyString, previousMainFrame->scrollingMode(), nullptr, nullptr, IsMainFrame::Yes, previousMainFrame->url()); m_mainFrame = mainFrame.copyRef(); - m_needsMainFrameObserver = true; previousMainFrame->transferNavigationCallbackToFrame(mainFrame); } @@ -273,7 +271,6 @@ void ProvisionalPageProxy::initializeWebPage(RefPtr&& webs if (m_shouldReuseMainFrame) { m_webPageID = existingRemotePageProxy->pageID(); m_mainFrame = existingRemotePageProxy->page()->mainFrame(); - m_needsMainFrameObserver = false; m_messageReceiverRegistration.stopReceivingMessages(); m_messageReceiverRegistration.transferMessageReceivingFrom(existingRemotePageProxy->messageReceiverRegistration(), *this, *this); existingRemotePageProxy->setDrawingArea(nullptr); diff --git a/Source/WebKit/UIProcess/ProvisionalPageProxy.h b/Source/WebKit/UIProcess/ProvisionalPageProxy.h index 9788035d76c9..8144b56253b7 100644 --- a/Source/WebKit/UIProcess/ProvisionalPageProxy.h +++ b/Source/WebKit/UIProcess/ProvisionalPageProxy.h @@ -155,7 +155,6 @@ class ProvisionalPageProxy final : public IPC::MessageReceiver, public IPC::Mess WebPageProxyMessageReceiverRegistration& messageReceiverRegistration() LIFETIME_BOUND { return m_messageReceiverRegistration; } - bool needsMainFrameObserver() const { return m_needsMainFrameObserver; } void updateFrameProcess(); private: @@ -232,7 +231,6 @@ class ProvisionalPageProxy final : public IPC::MessageReceiver, public IPC::Mess bool m_needsCookieAccessAddedInNetworkProcess { false }; bool m_needsDidStartProvisionalLoad { true }; bool m_shouldClosePage { true }; - bool m_needsMainFrameObserver { false }; bool m_didFailProvisionalLoad { false }; std::optional m_deferredRemoteTransitionSite; URL m_provisionalLoadURL; From 6ada7c61fd44a8919473d8e7a12875b8f4790298 Mon Sep 17 00:00:00 2001 From: Alex Christensen Date: Fri, 15 May 2026 15:01:34 -0700 Subject: [PATCH 106/424] testRunner.getApplicationManifestThen should return whether fetch was successful https://bugs.webkit.org/show_bug.cgi?id=314744 rdar://176994174 Reviewed by Chris Dumez. This is breaking off a piece of https://github.com/WebKit/WebKit/pull/64581 into its own smaller PR. Some of the tests for application manifest fetching used testRunner.dumpResourceLoadCallbacks as a way of indicating whether the fetch was successful or hit an error. This works with injected bundle load callbacks, but doesn't work in a non-flaky way with UI process callbacks because it hits cases where NetworkResourceLoader::didRetrieveCacheEntry fails a load sometimes, resulting in non-deterministic calls to the _WKResourceLoadDelegate. Rather than doing some invasive changes in behavior that could affect actual loading, for this test-only behavior I just expose whether the fetch was successful and log that in the test ouptut. The tests continue to test the success or failure of the load, but now the test output is deterministic even when the load delegate callbacks will be moved to the UI process. * LayoutTests/applicationmanifest/developer-warnings.html: * LayoutTests/applicationmanifest/multiple-links-expected.txt: * LayoutTests/applicationmanifest/multiple-links.html: * LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed-expected.txt: * LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed.html: * LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked-expected.txt: * LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked.html: * LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub-expected.txt: * LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub.html: * LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-expected.txt: * LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked.html: * Source/WebKit/UIProcess/API/C/WKPage.cpp: (WKPageGetApplicationManifest): * Source/WebKit/UIProcess/API/C/WKPagePrivate.h: * Tools/WebKitTestRunner/TestController.cpp: Canonical link: https://commits.webkit.org/313332@main --- .../applicationmanifest/developer-warnings.html | 5 +++-- .../applicationmanifest/multiple-links-expected.txt | 5 +---- LayoutTests/applicationmanifest/multiple-links.html | 10 ++++++---- .../manifest-src-allowed-expected.txt | 2 +- .../contentSecurityPolicy/manifest-src-allowed.html | 7 ++++--- .../manifest-src-blocked-expected.txt | 2 +- .../contentSecurityPolicy/manifest-src-blocked.html | 7 ++++--- ...dbox-manifest-blocked-cross-origin.sub-expected.txt | 4 +--- .../sandbox-manifest-blocked-cross-origin.sub.html | 9 +++++---- .../sandbox-manifest-blocked-expected.txt | 2 +- .../sandbox-manifest-blocked.html | 8 +++++--- Source/WebKit/UIProcess/API/C/WKPage.cpp | 4 ++-- Source/WebKit/UIProcess/API/C/WKPagePrivate.h | 2 +- Tools/WebKitTestRunner/TestController.cpp | 10 +++++++--- 14 files changed, 42 insertions(+), 35 deletions(-) diff --git a/LayoutTests/applicationmanifest/developer-warnings.html b/LayoutTests/applicationmanifest/developer-warnings.html index 3e5bc77bf6c7..53427d0aaee7 100644 --- a/LayoutTests/applicationmanifest/developer-warnings.html +++ b/LayoutTests/applicationmanifest/developer-warnings.html @@ -3,9 +3,10 @@ if (window.testRunner) { testRunner.dumpAsText(); testRunner.waitUntilDone(); - testRunner.getApplicationManifestThen(function() { + (async () => { + await testRunner.getApplicationManifest(); alert("Pass"); testRunner.notifyDone(); - }); + })(); } diff --git a/LayoutTests/applicationmanifest/multiple-links-expected.txt b/LayoutTests/applicationmanifest/multiple-links-expected.txt index 853fdb07491e..020927d250c6 100644 --- a/LayoutTests/applicationmanifest/multiple-links-expected.txt +++ b/LayoutTests/applicationmanifest/multiple-links-expected.txt @@ -1,4 +1 @@ -urlscheme://2 - willSendRequest redirectResponse (null) -multiple-links.html - didFinishLoading -urlscheme://2 - didFailLoadingWithError: - +manifest fetch success: false diff --git a/LayoutTests/applicationmanifest/multiple-links.html b/LayoutTests/applicationmanifest/multiple-links.html index 37d7f0725599..710d5311f68c 100644 --- a/LayoutTests/applicationmanifest/multiple-links.html +++ b/LayoutTests/applicationmanifest/multiple-links.html @@ -1,6 +1,5 @@ \ No newline at end of file + +
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed-expected.txt b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed-expected.txt index 3bf829ab74cd..7ecd7ee4f04b 100644 --- a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed-expected.txt +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed-expected.txt @@ -1,2 +1,2 @@ -ALERT: Pass +ALERT: manifest fetch success: true diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed.html b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed.html index ef5e10aedb80..4426e163d7de 100644 --- a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed.html +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-allowed.html @@ -4,9 +4,10 @@ if (window.testRunner) { testRunner.dumpAsText(); testRunner.waitUntilDone(); - testRunner.getApplicationManifestThen(function() { - alert("Pass"); + (async () => { + const success = await testRunner.getApplicationManifest(); + alert("manifest fetch success: " + success); testRunner.notifyDone(); - }); + })(); } diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked-expected.txt b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked-expected.txt index 2f651c6d8ec7..97eae748afb2 100644 --- a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked-expected.txt +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked-expected.txt @@ -1,3 +1,3 @@ CONSOLE MESSAGE: Refused to load http://127.0.0.1:8000/security/contentSecurityPolicy/manifest.test/manifest.json because it does not appear in the manifest-src directive of the Content Security Policy. -ALERT: Pass +ALERT: manifest fetch success: false diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked.html b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked.html index 2cd230236a41..33b4f65d955a 100644 --- a/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked.html +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/manifest-src-blocked.html @@ -8,9 +8,10 @@ diff --git a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub-expected.txt b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub-expected.txt index 765e578c8114..da7d3ddaba3c 100644 --- a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub-expected.txt +++ b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub-expected.txt @@ -1,3 +1 @@ -http://localhost:8800/WebKit/content-security-policy/sandbox-manifest-blocked-cross-origin.sub.html - didFinishLoading -http://not-web-platform.test:8800/WebKit/content-security-policy/manifest.json - didFailLoadingWithError: - +Fetched manifest: http://not-web-platform.test:8800/WebKit/content-security-policy/manifest.json, success: false diff --git a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub.html b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub.html index 0cea9f085abb..01aa6c159df9 100644 --- a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub.html +++ b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-cross-origin.sub.html @@ -6,11 +6,12 @@ +
diff --git a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-expected.txt b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-expected.txt index 90868cb11166..9e272b81ac10 100644 --- a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-expected.txt +++ b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked-expected.txt @@ -1,3 +1,3 @@ http://localhost:8800/WebKit/content-security-policy/sandbox-manifest-blocked.html - didFinishLoading http://localhost:8800/WebKit/content-security-policy/manifest.json - didFailLoadingWithError: - +Fetched manifest: http://localhost:8800/WebKit/content-security-policy/manifest.json, success: false diff --git a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked.html b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked.html index 597d01c4e0ff..9a2d36bcd91c 100644 --- a/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked.html +++ b/LayoutTests/http/wpt/content-security-policy/sandbox-manifest-blocked.html @@ -5,9 +5,11 @@ testRunner?.dumpAsText(); testRunner?.dumpResourceLoadCallbacks(); testRunner?.waitUntilDone(); - testRunner?.getApplicationManifestThen(() => { + (async () => { + const success = await testRunner.getApplicationManifest(); const elem = document.querySelector("link"); - console.log(`Fetched manifest: ${elem.href}`); + outputdiv.innerHTML = `Fetched manifest: ${elem.href}, success: ${success}`; testRunner.notifyDone(); - }); + })(); +
diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp index 353eaf0c764c..859d27360b08 100644 --- a/Source/WebKit/UIProcess/API/C/WKPage.cpp +++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp @@ -3206,11 +3206,11 @@ void WKPageGetApplicationManifest(WKPageRef pageRef, void* context, WKPageGetApp CRASH_IF_SUSPENDED; #if ENABLE(APPLICATION_MANIFEST) protect(toImpl(pageRef))->getApplicationManifest([function, context](const std::optional& manifest) { - function(context); + function(context, !!manifest); }); #else UNUSED_PARAM(pageRef); - function(context); + function(context, false); #endif } diff --git a/Source/WebKit/UIProcess/API/C/WKPagePrivate.h b/Source/WebKit/UIProcess/API/C/WKPagePrivate.h index 8a60a2bd1cde..0461232fc855 100644 --- a/Source/WebKit/UIProcess/API/C/WKPagePrivate.h +++ b/Source/WebKit/UIProcess/API/C/WKPagePrivate.h @@ -175,7 +175,7 @@ WK_EXPORT void WKPageSetUseDarkAppearanceForTesting(WKPageRef pageRef, bool useD WK_EXPORT WKProcessID WKPageGetProcessIdentifier(WKPageRef page); WK_EXPORT WKProcessID WKPageGetGPUProcessIdentifier(WKPageRef page); -typedef void (*WKPageGetApplicationManifestFunction)(void* functionContext); +typedef void (*WKPageGetApplicationManifestFunction)(void* functionContext, bool success); WK_EXPORT void WKPageGetApplicationManifest(WKPageRef page, void* context, WKPageGetApplicationManifestFunction block); typedef void (*WKPageDumpPrivateClickMeasurementFunction)(WKStringRef privateClickMeasurementRepresentation, void* functionContext); diff --git a/Tools/WebKitTestRunner/TestController.cpp b/Tools/WebKitTestRunner/TestController.cpp index e7d94484e2a6..e493aa6591a4 100644 --- a/Tools/WebKitTestRunner/TestController.cpp +++ b/Tools/WebKitTestRunner/TestController.cpp @@ -2008,7 +2008,7 @@ if (window.testRunner) { testRunner.findString = (target, options) => post(['FindString', target, options]); testRunner.runUIScript = (script, callback) => post(['RunUIScript', script, createHandle(callback)]); testRunner.runUIScriptImmediately = (script, callback) => post(['RunUIScriptImmediately', script, createHandle(callback)]); - testRunner.getApplicationManifestThen = async (callback) => { await post(['GetApplicationManifest']); callback() }; // NOLINT + testRunner.getApplicationManifest = () => post(['GetApplicationManifest']); testRunner.scrollDuringEnterFullscreen = () => post(['ScrollDuringEnterFullscreen']); testRunner.waitBeforeFinishingFullscreenExit = () => post(['WaitBeforeFinishingFullscreenExit']); testRunner.finishFullscreenExit = () => post(['FinishFullscreenExit']); @@ -2570,8 +2570,12 @@ void TestController::didReceiveScriptMessage(WKScriptMessageRef message, Complet return completionHandler(nullptr); } - if (WKStringIsEqualToUTF8CString(command, "GetApplicationManifest")) - return WKPageGetApplicationManifest(mainWebView()->page(), completionHandler.leak(), adoptAndCallCompletionHandler); + if (WKStringIsEqualToUTF8CString(command, "GetApplicationManifest")) { + return WKPageGetApplicationManifest(mainWebView()->page(), completionHandler.leak(), [] (void* context, bool success) { + auto completionHandler = WTF::adopt(static_cast::Impl*>(context)); + completionHandler(adoptWK(WKBooleanCreate(success)).get()); + }); + } if (WKStringIsEqualToUTF8CString(command, "IndicateFindMatch")) { auto index = static_cast(WKDoubleGetValue(static_cast(argument))); From 99f0b44bc5cec7e8e2c46fada62bc0167dc7f6b2 Mon Sep 17 00:00:00 2001 From: Geoffrey Garen Date: Fri, 15 May 2026 15:07:29 -0700 Subject: [PATCH 107/424] [CMake] Some Linux builds with precompiled headers enabled are broken https://bugs.webkit.org/show_bug.cgi?id=314763 rdar://177014051 Reviewed by Claudio Saavedra. In some versions of clang on Linux, when a child PCH is built with -Xclang -include-pch .pch, consumers of the child PCH see the parent's #pragma once / include-guard state but not any of its defined symbols. So the symbols are not present and also cannot be made present with an #include statement. I am inferring this explanation by experimental observation. Workaround: Only do -include-pch in Apple clang, where we know it works. Add an explicit #include in the child prefix header to catch the platforms that don't do -include-pch. Also fix two issues exposed by enabling PCH on Linux clang: * Source/JavaScriptCore/JavaScriptCoreJITPrefix.h: * Source/JavaScriptCore/JavaScriptCorePrefix.h: * Source/WebCore/WebCoreDOMAndRenderingPrefix.h: * Source/WebCore/WebCoreInspectorPrefix.h: * Source/WebCore/WebCoreJSBindingsPrefix.h: * Source/WebCore/WebCorePrefix.h: * Source/WebKit/WebKitMessageReceiversPrefix.h: * Source/WebKit/WebKitPrefix.h: * Source/WebKit/WebKitUIProcessPrefix.h: * Source/WebKit/WebKitWebProcessPrefix.h: Child prefix headers #include their parent; parent prefix headers gain * Source/cmake/WebKitMacros.cmake: (WEBKIT_ADD_PREFIX_HEADER_WITH_PARENT): Only inject the parent -include-pch on Apple platforms. * Source/ThirdParty/libwebrtc/CMakeLists.txt: Set SKIP_PRECOMPILE_HEADERS on the AVX2/FMA and NEON source groups because the compilation flags don't match. * Source/WebKit/UIProcess/API/glib/APIContentRuleListStoreGLib.cpp: Resolve pre-existing ambiguity in the name 'String'. Canonical link: https://commits.webkit.org/313333@main --- .../JavaScriptCore/JavaScriptCoreJITPrefix.h | 6 +---- Source/JavaScriptCore/JavaScriptCorePrefix.h | 2 ++ Source/ThirdParty/libwebrtc/CMakeLists.txt | 8 ++++-- Source/WebCore/WebCoreDOMAndRenderingPrefix.h | 5 +--- Source/WebCore/WebCoreInspectorPrefix.h | 4 +-- Source/WebCore/WebCoreJSBindingsPrefix.h | 6 +---- Source/WebCore/WebCorePrefix.h | 2 ++ .../API/glib/APIContentRuleListStoreGLib.cpp | 4 +-- Source/WebKit/WebKitMessageReceiversPrefix.h | 5 +--- Source/WebKit/WebKitPrefix.h | 2 ++ Source/WebKit/WebKitUIProcessPrefix.h | 4 +-- Source/WebKit/WebKitWebProcessPrefix.h | 4 +-- Source/cmake/WebKitMacros.cmake | 25 +++++++++++-------- 13 files changed, 36 insertions(+), 41 deletions(-) diff --git a/Source/JavaScriptCore/JavaScriptCoreJITPrefix.h b/Source/JavaScriptCore/JavaScriptCoreJITPrefix.h index 8e334feb8c45..97133607551e 100644 --- a/Source/JavaScriptCore/JavaScriptCoreJITPrefix.h +++ b/Source/JavaScriptCore/JavaScriptCoreJITPrefix.h @@ -23,11 +23,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE) -#include "cmakeconfig.h" -#endif - -#include +#include "JavaScriptCorePrefix.h" #ifdef __cplusplus #undef new diff --git a/Source/JavaScriptCore/JavaScriptCorePrefix.h b/Source/JavaScriptCore/JavaScriptCorePrefix.h index 79b7d1d78a97..14c9153ea965 100644 --- a/Source/JavaScriptCore/JavaScriptCorePrefix.h +++ b/Source/JavaScriptCore/JavaScriptCorePrefix.h @@ -23,6 +23,8 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#pragma once + #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE) #include "cmakeconfig.h" #endif diff --git a/Source/ThirdParty/libwebrtc/CMakeLists.txt b/Source/ThirdParty/libwebrtc/CMakeLists.txt index 24094f8dedb4..7db23b04272f 100644 --- a/Source/ThirdParty/libwebrtc/CMakeLists.txt +++ b/Source/ThirdParty/libwebrtc/CMakeLists.txt @@ -1819,7 +1819,9 @@ if (WTF_CPU_X86_64 OR WTF_CPU_X86) Source/webrtc/modules/audio_processing/aec3/vector_math_avx2.cc Source/webrtc/modules/audio_processing/agc2/rnn_vad/vector_math_avx2.cc ) - set_source_files_properties(${webrtc_avx_SOURCES} PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma") + set_source_files_properties(${webrtc_avx_SOURCES} PROPERTIES + COMPILE_OPTIONS "-mavx2;-mfma" + SKIP_PRECOMPILE_HEADERS ON) list(APPEND webrtc_SOURCES ${webrtc_avx_SOURCES}) add_definitions(-DWEBRTC_ENABLE_AVX2) endif() @@ -2009,7 +2011,9 @@ if (WTF_CPU_ARM) Source/webrtc/modules/audio_processing/aec3/suppression_filter.cc Source/webrtc/modules/audio_processing/aec3/suppression_gain.cc ) - set_source_files_properties(${webrtc_neon_SOURCES} PROPERTIES COMPILE_OPTIONS "-mfpu=neon;-mfloat-abi=hard") + set_source_files_properties(${webrtc_neon_SOURCES} PROPERTIES + COMPILE_OPTIONS "-mfpu=neon;-mfloat-abi=hard" + SKIP_PRECOMPILE_HEADERS ON) endif () if (WTF_CPU_MIPS) diff --git a/Source/WebCore/WebCoreDOMAndRenderingPrefix.h b/Source/WebCore/WebCoreDOMAndRenderingPrefix.h index 27a4780975f4..c189672fc6f9 100644 --- a/Source/WebCore/WebCoreDOMAndRenderingPrefix.h +++ b/Source/WebCore/WebCoreDOMAndRenderingPrefix.h @@ -23,10 +23,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/* Chained on top of WebCorePrefix.h; do not include that file here -- the - base PCH supplies it. Anchors below are headers that >=80% of the - dom/html/page/rendering/accessibility/animation/style TUs include and that - are not already in the base PCH closure. */ +#include "WebCorePrefix.h" #ifdef __cplusplus #undef new diff --git a/Source/WebCore/WebCoreInspectorPrefix.h b/Source/WebCore/WebCoreInspectorPrefix.h index 77bc8eedc387..c992a5059abb 100644 --- a/Source/WebCore/WebCoreInspectorPrefix.h +++ b/Source/WebCore/WebCoreInspectorPrefix.h @@ -23,9 +23,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/* Chained on top of WebCorePrefix.h; do not include that file here -- the - base PCH supplies it. Anchors below are headers that >=80% of inspector/ - TUs include and that are not already in the base PCH closure. */ +#include "WebCorePrefix.h" #ifdef __cplusplus #undef new diff --git a/Source/WebCore/WebCoreJSBindingsPrefix.h b/Source/WebCore/WebCoreJSBindingsPrefix.h index 1b4ae1863a74..30d73e05dd61 100644 --- a/Source/WebCore/WebCoreJSBindingsPrefix.h +++ b/Source/WebCore/WebCoreJSBindingsPrefix.h @@ -23,11 +23,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE) -#include "cmakeconfig.h" -#endif - -#include +#include "WebCorePrefix.h" #ifdef __cplusplus #undef new diff --git a/Source/WebCore/WebCorePrefix.h b/Source/WebCore/WebCorePrefix.h index 5291704cce56..2546c6dbcdb9 100644 --- a/Source/WebCore/WebCorePrefix.h +++ b/Source/WebCore/WebCorePrefix.h @@ -18,6 +18,8 @@ * */ +#pragma once + /* This prefix file should contain only: * 1) files to precompile for faster builds * 2) in one case at least: OS-X-specific performance bug workarounds diff --git a/Source/WebKit/UIProcess/API/glib/APIContentRuleListStoreGLib.cpp b/Source/WebKit/UIProcess/API/glib/APIContentRuleListStoreGLib.cpp index 01ce1e902bee..e78fae27ce9e 100644 --- a/Source/WebKit/UIProcess/API/glib/APIContentRuleListStoreGLib.cpp +++ b/Source/WebKit/UIProcess/API/glib/APIContentRuleListStoreGLib.cpp @@ -29,10 +29,10 @@ namespace API { #if ENABLE(CONTENT_EXTENSIONS) -String ContentRuleListStore::defaultStorePath() +WTF::String ContentRuleListStore::defaultStorePath() { ASSERT_NOT_REACHED(); - return String(); + return { }; } #endif diff --git a/Source/WebKit/WebKitMessageReceiversPrefix.h b/Source/WebKit/WebKitMessageReceiversPrefix.h index 33073fb514ca..efeedadfb77a 100644 --- a/Source/WebKit/WebKitMessageReceiversPrefix.h +++ b/Source/WebKit/WebKitMessageReceiversPrefix.h @@ -23,10 +23,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/* Chained on top of WebKitPrefix.h; do not include that file here -- the - base PCH supplies it. Anchors below are headers that >=80% of generated - DerivedSources message-receiver/serializer TUs include and that are not - already in the base PCH closure. */ +#include "WebKitPrefix.h" #ifdef __cplusplus #undef new diff --git a/Source/WebKit/WebKitPrefix.h b/Source/WebKit/WebKitPrefix.h index fcdc160fa650..8a65c85ebc70 100644 --- a/Source/WebKit/WebKitPrefix.h +++ b/Source/WebKit/WebKitPrefix.h @@ -24,6 +24,8 @@ * THE POSSIBILITY OF SUCH DAMAGE. */ +#pragma once + #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE) #include "cmakeconfig.h" #endif diff --git a/Source/WebKit/WebKitUIProcessPrefix.h b/Source/WebKit/WebKitUIProcessPrefix.h index a9731df2302f..739efdaabde2 100644 --- a/Source/WebKit/WebKitUIProcessPrefix.h +++ b/Source/WebKit/WebKitUIProcessPrefix.h @@ -23,9 +23,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/* Chained on top of WebKitPrefix.h; do not include that file here -- the - base PCH supplies it. Anchors below are headers that >=80% of UIProcess/ - TUs include and that are not already in the base PCH closure. */ +#include "WebKitPrefix.h" #ifdef __cplusplus #undef new diff --git a/Source/WebKit/WebKitWebProcessPrefix.h b/Source/WebKit/WebKitWebProcessPrefix.h index 1a0afd429c68..a3c3eeb999aa 100644 --- a/Source/WebKit/WebKitWebProcessPrefix.h +++ b/Source/WebKit/WebKitWebProcessPrefix.h @@ -23,9 +23,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/* Chained on top of WebKitPrefix.h; do not include that file here -- the - base PCH supplies it. Anchors below are headers that >=80% of WebProcess/ - TUs include and that are not already in the base PCH closure. */ +#include "WebKitPrefix.h" #ifdef __cplusplus #undef new diff --git a/Source/cmake/WebKitMacros.cmake b/Source/cmake/WebKitMacros.cmake index cf622e4ab5b6..2529664349e9 100644 --- a/Source/cmake/WebKitMacros.cmake +++ b/Source/cmake/WebKitMacros.cmake @@ -210,16 +210,21 @@ function(WEBKIT_ADD_PREFIX_HEADER_WITH_PARENT _target _base_target _header _pare string(JOIN "," _lang_genex ${ARGN}) target_precompile_headers(${_target} PRIVATE "$<$:${CMAKE_CURRENT_SOURCE_DIR}/${_header}>") - get_target_property(_base_bin_dir ${_base_target} BINARY_DIR) - get_target_property(_chain_bin_dir ${_target} BINARY_DIR) - foreach (_lang ${ARGN}) - _WEBKIT_PCH_PATHS_FOR_LANGUAGE(${_lang} _src_ext _stub_ext _pch_stem) - set(_base_pch "${_base_bin_dir}/CMakeFiles/${_base_target}.dir/${_pch_stem}.pch") - set(_chain_stub "${_chain_bin_dir}/CMakeFiles/${_target}.dir/${_pch_stem}.${_stub_ext}") - set_source_files_properties(${_chain_stub} PROPERTIES - COMPILE_OPTIONS "-Xclang;-include-pch;-Xclang;${_base_pch}" - OBJECT_DEPENDS "${_base_pch}") - endforeach () + if (APPLE) + # FIXME: Upstream clang does not appear to propagate parent-PCH state to consumers + # of the child PCH (webkit.org/b/314763). Until that is root-caused, build the child + # prefix as a standalone PCH on non-Apple clang; the child header #includes its parent. + get_target_property(_base_bin_dir ${_base_target} BINARY_DIR) + get_target_property(_chain_bin_dir ${_target} BINARY_DIR) + foreach (_lang ${ARGN}) + _WEBKIT_PCH_PATHS_FOR_LANGUAGE(${_lang} _src_ext _stub_ext _pch_stem) + set(_base_pch "${_base_bin_dir}/CMakeFiles/${_base_target}.dir/${_pch_stem}.pch") + set(_chain_stub "${_chain_bin_dir}/CMakeFiles/${_target}.dir/${_pch_stem}.${_stub_ext}") + set_source_files_properties(${_chain_stub} PROPERTIES + COMPILE_OPTIONS "-Xclang;-include-pch;-Xclang;${_base_pch}" + OBJECT_DEPENDS "${_base_pch}") + endforeach () + endif () _WEBKIT_ADD_PCH_OBJECT(${_target} PREFIX_NO_CODEGEN PREFIX_LANGUAGES ${ARGN}) else () WEBKIT_ADD_PREFIX_HEADER(${_target} ${_parent_header} PREFIX_LANGUAGES ${ARGN}) From 6bd49ed6463b56a0ddf86b11d9f2ef5e4183acc7 Mon Sep 17 00:00:00 2001 From: Ryosuke Niwa Date: Fri, 15 May 2026 15:16:14 -0700 Subject: [PATCH 108/424] ASSERTION FAILED: !shadowRoot() || shadowRoot()->mode() == ShadowRootMode::UserAgent https://bugs.webkit.org/show_bug.cgi?id=314791 Reviewed by Alan Baradlay. Some callers of userAgentShadowRoot were expecting the function to check shadow root's type and return nullptr when it's not an user-agent shadow root. Do that instead of merely asserting in debug builds. Test: fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash.html * LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash-expected.txt: Added. * LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash.html: Added. * Source/WebCore/dom/Element.cpp: (WebCore::Element::userAgentShadowRoot const): Canonical link: https://commits.webkit.org/313334@main --- ...with-author-shadow-root-crash-expected.txt | 4 ++ ...element-with-author-shadow-root-crash.html | 38 +++++++++++++++++++ Source/WebCore/dom/Element.cpp | 4 +- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash-expected.txt create mode 100644 LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash.html diff --git a/LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash-expected.txt b/LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash-expected.txt new file mode 100644 index 000000000000..02e87eaa2e92 --- /dev/null +++ b/LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash-expected.txt @@ -0,0 +1,4 @@ +This tests that style invalidation does not crash when a slotted element has its own author shadow root. +WebKit should not hit any assertion or crash and you should see PASS below. + +PASS - WebKit did not crash diff --git a/LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash.html b/LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash.html new file mode 100644 index 000000000000..d52f81e54015 --- /dev/null +++ b/LayoutTests/fast/shadow-dom/style-invalidation-slotted-element-with-author-shadow-root-crash.html @@ -0,0 +1,38 @@ + + + +

This tests that style invalidation does not crash when a slotted element has its own author shadow root.
+WebKit should not hit any assertion or crash and you should see PASS below.

+
+ slotted +
+ + + diff --git a/Source/WebCore/dom/Element.cpp b/Source/WebCore/dom/Element.cpp index c5e589af11a7..3bd3322c11d3 100644 --- a/Source/WebCore/dom/Element.cpp +++ b/Source/WebCore/dom/Element.cpp @@ -3570,8 +3570,8 @@ RefPtr Element::retargetReferenceTargetForBindings(RefPtr elem ShadowRoot* Element::userAgentShadowRoot() const { - ASSERT(!shadowRoot() || shadowRoot()->mode() == ShadowRootMode::UserAgent); - return shadowRoot(); + auto* root = shadowRoot(); + return root && root->mode() == ShadowRootMode::UserAgent ? root : nullptr; } ShadowRoot& Element::ensureUserAgentShadowRoot() From fb6c928ed4b3f9bca2ad6db1d86ceaa21ae3c27b Mon Sep 17 00:00:00 2001 From: Robert Jenner Date: Fri, 15 May 2026 15:19:41 -0700 Subject: [PATCH 109/424] [ tvOS watchOS ] OpenSource build tweaks for Xcode 26.4 rdar://177192814 https://bugs.webkit.org/show_bug.cgi?id=314916 Reviewed by Tim Horton. WebKit fails to compile after Xcode 26.4 for tvOS and watchOS. * Source/WTF/wtf/PlatformHave.h: Canonical link: https://commits.webkit.org/313335@main --- Source/WTF/wtf/PlatformHave.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/WTF/wtf/PlatformHave.h b/Source/WTF/wtf/PlatformHave.h index 7cb116369418..26b125f6d983 100644 --- a/Source/WTF/wtf/PlatformHave.h +++ b/Source/WTF/wtf/PlatformHave.h @@ -1444,7 +1444,9 @@ #if !defined(HAVE_XPC_API) \ && (PLATFORM(MAC) \ || PLATFORM(IOS) || PLATFORM(MACCATALYST) \ - || (PLATFORM(VISION) && __VISION_OS_VERSION_MIN_REQUIRED >= 260400)) + || (PLATFORM(VISION) && __VISION_OS_VERSION_MIN_REQUIRED >= 260400) \ + || (PLATFORM(WATCHOS) && __WATCH_OS_VERSION_MIN_REQUIRED >= 260400) \ + || (PLATFORM(APPLETV) && __TV_OS_VERSION_MIN_REQUIRED >= 260400)) #define HAVE_XPC_API 1 #endif From 2b952923a66cd56a0acd48875418f742e995b4fc Mon Sep 17 00:00:00 2001 From: Ryosuke Niwa Date: Fri, 15 May 2026 15:21:06 -0700 Subject: [PATCH 110/424] REGRESSION(310826@main): "Zhuyin - Traditional" input method stalls for multiple seconds when composing a mail https://bugs.webkit.org/show_bug.cgi?id=314888 rdar://177042301 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Wenson Hsieh. WebKit on macOS holds an m_interpretKeyEventHoldingTank to serialize keyboard events while a previous handleEventByInputMethod: call is in flight. This was added in 297270@main to ensure DOM event ordering (keydown before compositionstart/update) for input methods like Devanagari on docs.google.com. There was an unintended consequence, however. While the input method is processing keydown N, keydown N+1 is parked in WebKit and never reaches the IM's XPC queue. TCIM (Traditional Chinese - Zhuyin) appears to rely on incoming events to nudge its main thread back from auxiliary work (menu rebuilds, settings checks). With nothing in its incoming XPC queue, TCIM's main thread drifts off and gets wedged for 1–20 seconds — a system-wide input method stall (other apps also can't switch input sources during it). Eventually, TCIM "wakes up" with handled=YES but sends no commands to WebKit; WebKit then drains the held events as a burst, often producing visible garbage. Disabling inputMethodUsesCorrectKeyEventOrder made the hang go away, even though the state WebKit reports to the input method was identical in both modes. Through narrowing experiments (bypassing command queueing -> still hangs; bypassing concurrency control -> no hang), we confirmed the trigger was the holding tank's delay of subsequent keydowns reaching the input method, not the per-command queueing. This PR fixes the bug by immediately sending keydown to input methods, and only putting keyup events in the holding tank. More precisely, it implements the following fixes: - Replaced std::optional> m_collectedKeypressCommands with Deque> so each in-flight keydown gets its own queue (a Vector). The input methods serializes its handleEventByInputMethod: processing on its main thread, so its setMarkedText:/insertText:/doCommandBySelector: callbacks for keydown N arrive before it moves on to keydown N+1 — the front of the deque is always the queue for the keydown the input method is currently processing. - Keydowns no longer go into the holding tank. They dispatch handleEventByInputMethod: immediately, so their handleEvent: lands in the input method's XPC queue right away - TCIM's runloop has events to look at and doesn't drift. - Keyups still go into the tank when a keydown is in flight. Each keydown completion releases one keyup from the tank front via takeFirst()(). The released keyup dispatches handleEventByInputMethod: directly with an inline completion (bypassing the tank-check) so it doesn't re-tank when other keydowns are still in flight. This preserves the keydown -> keyup IPC ordering to the web process. - The 297270@main DOM event ordering invariant (keydown before compositionstart) is preserved for every key, including concurrent ones, because each keydown's commands replay during its own keydown default handler in the web process. Test: TestWebKitAPI.WKWebViewMacEditingTests.ConcurrentInputMethodKeyDownsScopeQueueingPerKey * Source/WebKit/UIProcess/mac/WebViewImpl.h: * Source/WebKit/UIProcess/mac/WebViewImpl.mm: (WebKit::WebViewImpl::collectKeyboardLayoutCommandsForEvent): (WebKit::WebViewImpl::interpretKeyEvent): (WebKit::WebViewImpl::doCommandBySelector): (WebKit::WebViewImpl::insertText): (WebKit::WebViewImpl::selectedRangeWithCompletionHandler): (WebKit::WebViewImpl::setMarkedText): * Tools/Scripts/webkitpy/api_tests/allowlist.txt: * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm: (-[MockTextInputContext handleEventByInputMethod:completionHandler:]): (TestWebKitAPI::TEST(WKWebViewMacEditingTests, ConcurrentInputMethodKeyDownsScopeQueueingPerKey)): Canonical link: https://commits.webkit.org/313336@main --- Source/WebKit/UIProcess/mac/WebViewImpl.h | 5 +- Source/WebKit/UIProcess/mac/WebViewImpl.mm | 85 ++++++++++++------- .../Scripts/webkitpy/api_tests/allowlist.txt | 1 + .../WKWebView/mac/WKWebViewMacEditingTests.mm | 46 ++++++++++ 4 files changed, 104 insertions(+), 33 deletions(-) diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.h b/Source/WebKit/UIProcess/mac/WebViewImpl.h index f2c08430051c..56fad696bb4b 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.h +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.h @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -1121,9 +1122,9 @@ ALLOW_DEPRECATED_DECLARATIONS_END // that has been already sent to WebCore. RetainPtr m_keyDownEventBeingResent; - std::optional> m_collectedKeypressCommands; std::optional m_stagedMarkedRange; - Vector> m_interpretKeyEventHoldingTank; + Deque> m_collectedKeypressCommands; + Deque> m_interpretKeyEventHoldingTank; String m_lastStringForCandidateRequest; NSInteger m_lastCandidateRequestSequenceNumber; diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.mm b/Source/WebKit/UIProcess/mac/WebViewImpl.mm index d6fd7aa34da0..6f98067352aa 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.mm +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.mm @@ -5539,16 +5539,17 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) if ([event type] != NSEventTypeKeyDown) return { }; - ASSERT(!m_collectedKeypressCommands); - m_collectedKeypressCommands = Vector { }; + // Push a fresh queue at the FRONT of the deque so the synchronous interpretKeyEvents + // call below routes our doCommandBySelector:/insertText: callbacks into our queue, + // even if other (IM-driven) keys are also in flight at the back. + m_collectedKeypressCommands.prepend(Vector { }); if (RetainPtr context = inputContext()) [context handleEventByKeyboardLayout:event]; else [m_view.get() interpretKeyEvents:@[event]]; - auto commands = WTF::move(*m_collectedKeypressCommands); - m_collectedKeypressCommands = std::nullopt; + auto commands = m_collectedKeypressCommands.takeFirst(); if (RetainPtr menu = NSApp.mainMenu; event.modifierFlags & NSEventModifierFlagFunction && [menu respondsToSelector:@selector(_containsItemMatchingEvent:includingDisabledItems:)] && [menu _containsItemMatchingEvent:event includingDisabledItems:YES]) { @@ -5570,18 +5571,43 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) #if PLATFORM(MAC) if (m_page->editorState().inputMethodUsesCorrectKeyEventOrder) { - if (m_collectedKeypressCommands) { + // Keydowns flow through to the IM directly. Each gets its own queue at the BACK + // of the deque; the IM serializes its handleEventByInputMethod: processing on its + // main thread, so its setMarkedText:/insertText:/doCommandBySelector: callbacks + // arrive in dispatch order — the front of the deque is always the queue for the + // keydown the IM is currently processing. Concurrent keydowns reach the IM's XPC + // queue immediately, which avoids starving TCIM's runloop and triggering the + // Zhuyin Traditional stall in Mail (rdar://177042301). + // + // Keyups instead wait in m_interpretKeyEventHoldingTank until the in-flight + // keydown's IM completion has fired (and dispatched its keyboard event IPC). + // Each keydown completion releases one keyup from the tank front, preserving + // the strict keydown→keyup→keydown→keyup IPC ordering to the web process; the + // released keyup dispatches handleEventByInputMethod: directly (bypassing the + // tank-check) so it doesn't re-tank when other keydowns are still in flight. + if ([event type] == NSEventTypeKeyDown) + m_collectedKeypressCommands.append(Vector { }); + else if (!m_collectedKeypressCommands.isEmpty()) { m_interpretKeyEventHoldingTank.append([weakThis = WeakPtr { *this }, capturedEvent = retainPtr(event), capturedBlock = makeBlockPtr(completionHandler)] { CheckedPtr checkedThis = weakThis.get(); - if (!checkedThis) + if (!checkedThis) { capturedBlock(NO, { }); - else - checkedThis->interpretKeyEvent(capturedEvent.get(), capturedBlock.get()); + return; + } + LOG(TextInput, "-> handleEventByInputMethod:%p %@ (released keyup)", capturedEvent.get(), capturedEvent.get()); + RetainPtr inputContext { checkedThis->inputContext() }; + [inputContext handleEventByInputMethod:capturedEvent completionHandler:[weakThis, capturedEvent, capturedBlock](BOOL handled) mutable { + if (!weakThis.get()) { + capturedBlock(NO, { }); + return; + } + // Released keyups don't take from the deque or produce commands; they + // just need their keyboard-event IPC dispatched. + capturedBlock(handled, { }); + }]; }); return; } - - m_collectedKeypressCommands = Vector { }; } #endif @@ -5596,9 +5622,9 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) Vector commands; #if PLATFORM(MAC) - if (checkedThis->m_page->editorState().inputMethodUsesCorrectKeyEventOrder) { - commands = WTF::move(*checkedThis->m_collectedKeypressCommands); - checkedThis->m_collectedKeypressCommands = std::nullopt; + if (checkedThis->m_page->editorState().inputMethodUsesCorrectKeyEventOrder && [capturedEvent type] == NSEventTypeKeyDown) { + ASSERT(!checkedThis->m_collectedKeypressCommands.isEmpty()); + commands = checkedThis->m_collectedKeypressCommands.takeFirst(); checkedThis->m_stagedMarkedRange = std::nullopt; } #endif @@ -5627,22 +5653,19 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) LOG(TextInput, "... handleEventByInputMethod%s handled", handled ? "" : " not"); if (handled) { capturedBlock(YES, WTF::move(commands)); - auto holdingTank = WTF::move(checkedThis->m_interpretKeyEventHoldingTank); - for (auto& function : holdingTank) - function(); + // Release the next held keyup (if any) so it follows this keydown's IPC, + // preserving sequential keydown→keyup ordering to the web process. + if ([capturedEvent type] == NSEventTypeKeyDown && !checkedThis->m_interpretKeyEventHoldingTank.isEmpty()) + checkedThis->m_interpretKeyEventHoldingTank.takeFirst()(); return; } - auto additionalCommands = checkedThis->collectKeyboardLayoutCommandsForEvent(capturedEvent.get()); + auto additionalCommands = checkedThis->collectKeyboardLayoutCommandsForEvent(capturedEvent); if (!hasInsertText) commands.appendVector(additionalCommands); capturedBlock(NO, commands); -#if PLATFORM(MAC) - ASSERT(checkedThis->m_page->editorState().inputMethodUsesCorrectKeyEventOrder || checkedThis->m_interpretKeyEventHoldingTank.isEmpty()); -#endif - auto holdingTank = WTF::move(checkedThis->m_interpretKeyEventHoldingTank); - for (auto& function : holdingTank) - function(); + if ([capturedEvent type] == NSEventTypeKeyDown && !checkedThis->m_interpretKeyEventHoldingTank.isEmpty()) + checkedThis->m_interpretKeyEventHoldingTank.takeFirst()(); }]; } @@ -5650,9 +5673,9 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) { LOG(TextInput, "doCommandBySelector:\"%s\"", sel_getName(selector)); - if (m_collectedKeypressCommands) { + if (!m_collectedKeypressCommands.isEmpty()) { WebCore::KeypressCommand command(NSStringFromSelector(selector)); - m_collectedKeypressCommands->append(command); + m_collectedKeypressCommands.first().append(command); LOG(TextInput, "...stored"); m_page->registerKeypressCommandName(command.commandName); } else { @@ -5704,7 +5727,7 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) // - If it's from an input method, then we should insert the text now. // - If it's sent outside of keyboard event processing (e.g. from Character Viewer, or when confirming an inline input area with a mouse), // then we also execute it immediately, as there will be no other chance. - if (m_collectedKeypressCommands && !m_isTextInsertionReplacingSoftSpace) { + if (!m_collectedKeypressCommands.isEmpty() && !m_isTextInsertionReplacingSoftSpace) { // Modeless input methods (Vietnamese Simple Telex, Korean Hangul) call insertText: with a // replacementRange during their handleEventByInputMethod: callback to commit a previously // inserted character into a longer sequence (e.g. replace 'v' with 'vi'). Queue it with @@ -5712,7 +5735,7 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) // the keydown default handler, after keydown has fired but before composition/input — // matching the event ordering 297270@main set up for plain insertText:. WebCore::KeypressCommand command("insertText:"_s, text.get(), { }, { }, { }, EditingRange { replacementRange }.toCharacterRange()); - m_collectedKeypressCommands->append(command); + m_collectedKeypressCommands.first().append(command); LOG(TextInput, "...stored"); m_page->registerKeypressCommandName(command.commandName); return; @@ -5749,10 +5772,10 @@ static bool eventKeyCodeIsZeroOrNumLockOrFn(NSEvent *event) // insertText:; if setMarkedText: is in the mix, the IME is already in composition mode and // the unstaged selectedRange is the right answer. size_t stagedInsertLength = 0; - if (m_collectedKeypressCommands && !m_collectedKeypressCommands->isEmpty()) { + if (!m_collectedKeypressCommands.isEmpty() && !m_collectedKeypressCommands.first().isEmpty()) { bool onlyInsertText = true; size_t total = 0; - for (auto& command : *m_collectedKeypressCommands) { + for (auto& command : m_collectedKeypressCommands.first()) { if (command.commandName != "insertText:"_s) { onlyInsertText = false; break; @@ -6075,10 +6098,10 @@ static BOOL shouldUseHighlightsForMarkedText(NSAttributedString *string) } #if PLATFORM(MAC) - if (m_page->editorState().inputMethodUsesCorrectKeyEventOrder && m_collectedKeypressCommands) { + if (m_page->editorState().inputMethodUsesCorrectKeyEventOrder && !m_collectedKeypressCommands.isEmpty()) { WebCore::KeypressCommand command("setMarkedText:"_s, text.get(), WTF::move(underlines), WTF::move(highlights), EditingRange { selectedRange }.toCharacterRange(), EditingRange { replacementRange }.toCharacterRange()); - m_collectedKeypressCommands->append(command); + m_collectedKeypressCommands.first().append(command); m_stagedMarkedRange = selectedRange; LOG(TextInput, "...stored"); m_page->registerKeypressCommandName(command.commandName); diff --git a/Tools/Scripts/webkitpy/api_tests/allowlist.txt b/Tools/Scripts/webkitpy/api_tests/allowlist.txt index caa9fada2faa..968bc0c8de99 100644 --- a/Tools/Scripts/webkitpy/api_tests/allowlist.txt +++ b/Tools/Scripts/webkitpy/api_tests/allowlist.txt @@ -3292,6 +3292,7 @@ TestWebKitAPI.WKWebViewEditActions.SetAlignment TestWebKitAPI.WKWebViewEditActions.SetFontFamily TestWebKitAPI.WKWebViewEditActions.SetTextColor TestWebKitAPI.WKWebViewEditActions.ToggleStrikeThrough +TestWebKitAPI.WKWebViewMacEditingTests.ConcurrentInputMethodKeyDownsScopeQueueingPerKey TestWebKitAPI.WKWebViewMacEditingTests.DoNotCrashWhenInterpretingKeyEventWhileDeallocatingView TestWebKitAPI.WKWebViewMacEditingTests.DoNotRenderInlinePredictionsForRegularMarkedText TestWebKitAPI.WKWebViewMacEditingTests.DoubleClickDoesNotSelectTrailingSpace diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm index 9bb09a6814a9..f6db20e46a36 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/mac/WKWebViewMacEditingTests.mm @@ -131,6 +131,7 @@ - (instancetype)initWithInsertText:(NSString *)insertedText replacementRange:(NS @interface MockTextInputContext : NSTextInputContext @property (nonatomic, assign) NSMutableArray *actions; +@property (nonatomic, assign) NSMutableArray *eventLog; @end @implementation MockTextInputContext @@ -144,8 +145,11 @@ - (void)handleEventByInputMethod:(NSEvent *)event completionHandler:(void(^)(BOO } MockTextInputContextAction *lastItem = _actions.firstObject; [_actions removeObjectAtIndex:0]; + NSString *label = lastItem.markedText ?: lastItem.insertedText; + [_eventLog addObject:[NSString stringWithFormat:@"received_%@", label]]; double delay = lastItem ? lastItem.delay : 10; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), mainDispatchQueueSingleton(), ^{ + [_eventLog addObject:[NSString stringWithFormat:@"fired_%@", label]]; if (lastItem.markedText) [self.client setMarkedText:lastItem.markedText selectedRange:lastItem.selectedRange replacementRange:lastItem.replacementRange]; else @@ -474,6 +478,48 @@ - (NSRange)range [webView stringByEvaluatingJavaScript:@"window.events.join(',')"].UTF8String); } +TEST(WKWebViewMacEditingTests, ConcurrentInputMethodKeyDownsScopeQueueingPerKey) +{ + RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); + for (_WKFeature *feature in WKPreferences._features) { + NSString *key = feature.key; + if ([key isEqualToString:@"InputMethodUsesCorrectKeyEventOrder"]) + [[configuration preferences] _setEnabled:YES forFeature:feature]; + } + + RetainPtr webView = adoptNS([[TestWebViewWithMockTextInputContext alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]); + // Two keydowns dispatched back-to-back. The mock's 0.1s delay on each keydown action + // means both keydowns must reach the IM (super.handleEventByInputMethod:) before + // either action fires. With the rdar://177042301 fix (Zhuyin Traditional stall), + // keydowns flow through to the IM directly and each gets its own deque entry, so + // both are claimed before action 1 runs. Without the fix, the second keydown is + // held in WebKit until the first keydown's IM completion fires (after action 1 + // runs), serializing the events. + RetainPtr action1 = adoptNS([[MockTextInputContextAction alloc] initWithMarkedText:@"a" selectedRange:NSMakeRange(0, 1) replacementRange:NSMakeRange(NSNotFound, 0)]); + [action1 setDelay:0.1]; + RetainPtr action2 = adoptNS([[MockTextInputContextAction alloc] initWithMarkedText:@"ab" selectedRange:NSMakeRange(0, 2) replacementRange:NSMakeRange(NSNotFound, 0)]); + [action2 setDelay:0.1]; + [webView _web_superInputContext].actions = [@[action1.get(), action2.get()].mutableCopy autorelease]; + [webView _web_superInputContext].eventLog = [NSMutableArray array]; + + [webView synchronouslyLoadHTMLString:@""]; + [webView stringByEvaluatingJavaScript:@"document.body.focus();"]; + [webView waitForNextPresentationUpdate]; + [webView removeFromSuperview]; + + [webView typeCharacter:'a']; + [webView typeCharacter:'b']; + Util::runFor(2_s); + + EXPECT_STREQ("ab", [webView stringByEvaluatingJavaScript:@"document.body.textContent"].UTF8String); + // The mock log records "received_" when super.handleEventByInputMethod: completes + // for a keydown (the IM has been told about it) and "fired_" when the action's + // dispatch_after block runs (the IM responds with setMarkedText:). With per-key queue + // scoping, both keydowns are received before either action fires. + EXPECT_STREQ("received_a,received_ab,fired_a,fired_ab", + [[[webView _web_superInputContext].eventLog componentsJoinedByString:@","] UTF8String]); +} + TEST(WKWebViewMacEditingTests, DoNotCrashWhenInterpretingKeyEventWhileDeallocatingView) { __block bool isDone = false; From 66e0cef1596ad3b300e466d9cdbb147d0ea87412 Mon Sep 17 00:00:00 2001 From: Diego De La Toba Date: Fri, 15 May 2026 15:28:28 -0700 Subject: [PATCH 111/424] [Gardening]REGRESSION(313283@main)?: [macOS] media/modern-media-controls/scrubber-support/scrubber-support-drag.html is a constant text failure. https://bugs.webkit.org/show_bug.cgi?id=314920 rdar://177199198 Unreviewed test gardening. * LayoutTests/platform/mac-wk2/TestExpectations: Canonical link: https://commits.webkit.org/313337@main --- LayoutTests/platform/mac-wk2/TestExpectations | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LayoutTests/platform/mac-wk2/TestExpectations b/LayoutTests/platform/mac-wk2/TestExpectations index ff2e5f07f769..2f45ff852da5 100644 --- a/LayoutTests/platform/mac-wk2/TestExpectations +++ b/LayoutTests/platform/mac-wk2/TestExpectations @@ -2268,6 +2268,8 @@ webkit.org/b/308165 [ Debug ] scrollingcoordinator/mac/latching/simple-page-rubb webkit.org/b/311759 [ Release arm64 ] imported/w3c/web-platform-tests/event-timing/keydown.html [ Pass Failure ] +webkit.org/b/314920 media/modern-media-controls/scrubber-support/scrubber-support-drag.html [ Failure ] + webkit.org/b/308325 [ Debug ] http/wpt/cache-storage/cache-in-stopped-context.html [ Pass Failure ] webkit.org/b/309173 imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/modulepreload-referrerpolicy.html [ Pass Failure ] From 8cc222c1c235ca073af5707fc14a9221b76cd3f0 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Fri, 15 May 2026 15:54:03 -0700 Subject: [PATCH 112/424] [Site Isolation] http/tests/security/cross-origin-local-storage.html is failing https://bugs.webkit.org/show_bug.cgi?id=314859 rdar://177116495 Reviewed by Sihui Liu. When site isolation is enabled http/tests/security/cross-origin-local-storage.html fails with the following diff compared to with site isolation disabled: ``` -No value +Got value: value ``` This bug is due to the fact that the test calls internals.settings.setStorageBlockingPolicy('BlockThirdParty'); which doesn't propagate across process boundaries. This setting controls if third party storage accesses go through a blocked/partitioned storage or the real persistent storage. This setting is defined in Source/WebCore/page/Settings.yaml and has its C++ setter generated by Source/WebCore/Scripts/GenerateSettings.rb. This setting is per-page and doesn't propagate to other processes. So, in this test the setting was set in the main frame's web process which didn't propogate to the cross-origin iframe's web process with site isolation enabled. This patch modifies this test to set this setting via the following WKTR header: ``` ``` which is equivalent to internals.settings.setStorageBlockingPolicy('BlockThirdParty'); except that it directly modifies WebPreferences which gets inherited by new processes that spawn up. This patch fixes http/tests/security/cross-origin-local-storage.html with site isolation enabled. * LayoutTests/http/tests/security/cross-origin-local-storage.html: * LayoutTests/platform/ios-site-isolation/TestExpectations: * LayoutTests/platform/mac-site-isolation/TestExpectations: Canonical link: https://commits.webkit.org/313338@main --- LayoutTests/http/tests/security/cross-origin-local-storage.html | 2 +- LayoutTests/platform/ios-site-isolation/TestExpectations | 1 - LayoutTests/platform/mac-site-isolation/TestExpectations | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/LayoutTests/http/tests/security/cross-origin-local-storage.html b/LayoutTests/http/tests/security/cross-origin-local-storage.html index 68b4496299b9..35f2a1bd2c18 100644 --- a/LayoutTests/http/tests/security/cross-origin-local-storage.html +++ b/LayoutTests/http/tests/security/cross-origin-local-storage.html @@ -1,10 +1,10 @@ + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any-expected.txt new file mode 100644 index 000000000000..9fcf4b0746df --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any-expected.txt @@ -0,0 +1,21 @@ + +PASS idl_test setup +PASS idl_test validation +PASS Partial interface Navigator: member names are unique +PASS Partial interface mixin NavigatorID: member names are unique +PASS Navigator includes GlobalPrivacyControl: member names are unique +PASS WorkerNavigator includes GlobalPrivacyControl: member names are unique +PASS Navigator includes NavigatorID: member names are unique +PASS Navigator includes NavigatorLanguage: member names are unique +PASS Navigator includes NavigatorOnLine: member names are unique +PASS Navigator includes NavigatorContentUtils: member names are unique +PASS Navigator includes NavigatorCookies: member names are unique +PASS Navigator includes NavigatorPlugins: member names are unique +PASS Navigator includes NavigatorConcurrentHardware: member names are unique +PASS WorkerNavigator includes NavigatorID: member names are unique +PASS WorkerNavigator includes NavigatorLanguage: member names are unique +PASS WorkerNavigator includes NavigatorOnLine: member names are unique +PASS WorkerNavigator includes NavigatorConcurrentHardware: member names are unique +PASS Navigator interface: attribute globalPrivacyControl +PASS Navigator interface: navigator must inherit property "globalPrivacyControl" with the proper type + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.html b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.html new file mode 100644 index 000000000000..2382913528e6 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.js b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.js new file mode 100644 index 000000000000..7a7305eb6235 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +idl_test( + ['gpc'], + ['html'], + idl_array => { + if (self.Window) { + idl_array.add_objects({ Navigator: ['navigator'] }); + } else { + idl_array.add_objects({ WorkerNavigator: ['navigator'] }); + } + } +); diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.serviceworker-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.serviceworker-expected.txt new file mode 100644 index 000000000000..af9c3834df7f --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.serviceworker-expected.txt @@ -0,0 +1,21 @@ + +PASS idl_test setup +PASS idl_test validation +PASS Partial interface Navigator: member names are unique +PASS Partial interface mixin NavigatorID: member names are unique +PASS Navigator includes GlobalPrivacyControl: member names are unique +PASS WorkerNavigator includes GlobalPrivacyControl: member names are unique +PASS Navigator includes NavigatorID: member names are unique +PASS Navigator includes NavigatorLanguage: member names are unique +PASS Navigator includes NavigatorOnLine: member names are unique +PASS Navigator includes NavigatorContentUtils: member names are unique +PASS Navigator includes NavigatorCookies: member names are unique +PASS Navigator includes NavigatorPlugins: member names are unique +PASS Navigator includes NavigatorConcurrentHardware: member names are unique +PASS WorkerNavigator includes NavigatorID: member names are unique +PASS WorkerNavigator includes NavigatorLanguage: member names are unique +PASS WorkerNavigator includes NavigatorOnLine: member names are unique +PASS WorkerNavigator includes NavigatorConcurrentHardware: member names are unique +PASS WorkerNavigator interface: attribute globalPrivacyControl +PASS WorkerNavigator interface: navigator must inherit property "globalPrivacyControl" with the proper type + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.serviceworker.html b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.serviceworker.html new file mode 100644 index 000000000000..2382913528e6 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.serviceworker.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.sharedworker-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.sharedworker-expected.txt new file mode 100644 index 000000000000..af9c3834df7f --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.sharedworker-expected.txt @@ -0,0 +1,21 @@ + +PASS idl_test setup +PASS idl_test validation +PASS Partial interface Navigator: member names are unique +PASS Partial interface mixin NavigatorID: member names are unique +PASS Navigator includes GlobalPrivacyControl: member names are unique +PASS WorkerNavigator includes GlobalPrivacyControl: member names are unique +PASS Navigator includes NavigatorID: member names are unique +PASS Navigator includes NavigatorLanguage: member names are unique +PASS Navigator includes NavigatorOnLine: member names are unique +PASS Navigator includes NavigatorContentUtils: member names are unique +PASS Navigator includes NavigatorCookies: member names are unique +PASS Navigator includes NavigatorPlugins: member names are unique +PASS Navigator includes NavigatorConcurrentHardware: member names are unique +PASS WorkerNavigator includes NavigatorID: member names are unique +PASS WorkerNavigator includes NavigatorLanguage: member names are unique +PASS WorkerNavigator includes NavigatorOnLine: member names are unique +PASS WorkerNavigator includes NavigatorConcurrentHardware: member names are unique +PASS WorkerNavigator interface: attribute globalPrivacyControl +PASS WorkerNavigator interface: navigator must inherit property "globalPrivacyControl" with the proper type + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.sharedworker.html b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.sharedworker.html new file mode 100644 index 000000000000..2382913528e6 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.sharedworker.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.worker-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.worker-expected.txt new file mode 100644 index 000000000000..af9c3834df7f --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.worker-expected.txt @@ -0,0 +1,21 @@ + +PASS idl_test setup +PASS idl_test validation +PASS Partial interface Navigator: member names are unique +PASS Partial interface mixin NavigatorID: member names are unique +PASS Navigator includes GlobalPrivacyControl: member names are unique +PASS WorkerNavigator includes GlobalPrivacyControl: member names are unique +PASS Navigator includes NavigatorID: member names are unique +PASS Navigator includes NavigatorLanguage: member names are unique +PASS Navigator includes NavigatorOnLine: member names are unique +PASS Navigator includes NavigatorContentUtils: member names are unique +PASS Navigator includes NavigatorCookies: member names are unique +PASS Navigator includes NavigatorPlugins: member names are unique +PASS Navigator includes NavigatorConcurrentHardware: member names are unique +PASS WorkerNavigator includes NavigatorID: member names are unique +PASS WorkerNavigator includes NavigatorLanguage: member names are unique +PASS WorkerNavigator includes NavigatorOnLine: member names are unique +PASS WorkerNavigator includes NavigatorConcurrentHardware: member names are unique +PASS WorkerNavigator interface: attribute globalPrivacyControl +PASS WorkerNavigator interface: navigator must inherit property "globalPrivacyControl" with the proper type + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.worker.html b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.worker.html new file mode 100644 index 000000000000..2382913528e6 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.worker.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/navigator-globalPrivacyControl.https-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/gpc/navigator-globalPrivacyControl.https-expected.txt new file mode 100644 index 000000000000..e6b82faef5c1 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/navigator-globalPrivacyControl.https-expected.txt @@ -0,0 +1,6 @@ + +PASS Expected navigator.globalPrivacyControl value (true) is read from the window +PASS Expected navigator.globalPrivacyControl value (true) is read from the dedicated worker +PASS Expected navigator.globalPrivacyControl value (false) is read from the window +PASS Expected navigator.globalPrivacyControl value (false) is read from the dedicated worker + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/navigator-globalPrivacyControl.https.html b/LayoutTests/imported/w3c/web-platform-tests/gpc/navigator-globalPrivacyControl.https.html new file mode 100644 index 000000000000..6ddc7ffca419 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/navigator-globalPrivacyControl.https.html @@ -0,0 +1,33 @@ + + +Primary navigator.globalPrivacyControl test window + + + + + + + +
+ + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/sec-gpc.https-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/gpc/sec-gpc.https-expected.txt new file mode 100644 index 000000000000..8431837dcf5d --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/sec-gpc.https-expected.txt @@ -0,0 +1,14 @@ + +PASS Expected Sec-GPC value (true) is on the document fetch +PASS Expected Sec-GPC value (true) is on the image fetch +PASS Expected Sec-GPC value (true) is on the script fetch +PASS Expected Sec-GPC value (true) is on the iframe fetch +PASS Expected Sec-GPC value (true) is on the framed image fetch +PASS Expected Sec-GPC value (true) is on the framed script fetch +PASS Expected Sec-GPC value (false) is on the document fetch +PASS Expected Sec-GPC value (false) is on the image fetch +PASS Expected Sec-GPC value (false) is on the script fetch +PASS Expected Sec-GPC value (false) is on the iframe fetch +PASS Expected Sec-GPC value (false) is on the framed image fetch +PASS Expected Sec-GPC value (false) is on the framed script fetch + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/sec-gpc.https.html b/LayoutTests/imported/w3c/web-platform-tests/gpc/sec-gpc.https.html new file mode 100644 index 000000000000..6acc350d241c --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/sec-gpc.https.html @@ -0,0 +1,33 @@ + + +Primary Sec-GPC test window + + + + + + + +
+ + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/support/getGPC.py b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/getGPC.py new file mode 100644 index 000000000000..1d280a6cafce --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/getGPC.py @@ -0,0 +1,95 @@ +import base64 + +def maybeBoolToJavascriptLiteral(value): + if value == None: + return "undefined" + if value == True: + return "true" + if value == False: + return "false" + raise ValueError("Expected bool or None") + +def main(request, response): + destination = request.headers.get("sec-fetch-dest").decode("utf-8") + gpcValue = request.headers.get("sec-gpc") == b'1' + expectedGPCValue = request.GET.get(b"gpc") == b"true" + inFrame = request.GET.get(b"framed") != None + destinationDescription = "framed " + destination if inFrame else destination + if destination == "document" or destination == "iframe": + response.headers.set('Content-Type', 'text/html'); + return f""" + + +Sec-GPC {destination} + + + + +
+ + +""" + (f""" + + """ if destination == "document" else "") + f""" + + + +""" + elif destination == "image": + if gpcValue == expectedGPCValue: + return (200, [(b"Content-Type", b"image/png")], base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEUlEQVR42mP8nzaTAQQYYQwALssD/5ca+r8AAAAASUVORK5CYII=")) + return (400, [], "") + elif destination == "script": + response.headers.set('Content-Type', 'application/javascript'); + return f""" +debugger; +test(function(t) {{ + assert_equals({maybeBoolToJavascriptLiteral(gpcValue)}, {maybeBoolToJavascriptLiteral(expectedGPCValue)}, "Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch"); +}}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch`); +""" + elif destination == "worker" or destination == "sharedworker" or destination == "serviceworker": + response.headers.set('Content-Type', 'application/javascript'); + return f""" +importScripts("/resources/testharness.js"); +test(function(t) {{ + assert_equals({maybeBoolToJavascriptLiteral(gpcValue)}, {maybeBoolToJavascriptLiteral(expectedGPCValue)}, "Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch"); +}}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch`); +done(); +""" + raise ValueError("Unexpected destination") diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.html b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.html new file mode 100644 index 000000000000..b59e344da42a --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.html @@ -0,0 +1,25 @@ + + +navigator.globalPrivacyControl Window + + + + +
+ + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.js b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.js new file mode 100644 index 000000000000..beb9ea372cd4 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.js @@ -0,0 +1,11 @@ +importScripts("/resources/testharness.js"); + +const queryString = self.location.search; +const urlParams = new URLSearchParams(queryString); +const expectedValue = urlParams.has("gpc", "true"); +const workerType = urlParams.get("workerType"); +test(function(t) { + assert_equals(navigator.globalPrivacyControl, expectedValue, "Expected navigator.globalPrivacyControl value is read from the worker"); +}, `Expected navigator.globalPrivacyControl value (${expectedValue}) is read from the ${workerType} worker`); + +done(); diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/support/w3c-import.log b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/w3c-import.log new file mode 100644 index 000000000000..3b304f989f35 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/support/w3c-import.log @@ -0,0 +1,19 @@ +The tests in this directory were imported from the W3C repository. +Do NOT modify these tests directly in WebKit. +Instead, create a pull request on the WPT github: + https://github.com/web-platform-tests/wpt + +Then run the Tools/Scripts/import-w3c-tests in WebKit to reimport + +Do NOT modify or remove this file. + +------------------------------------------------------------------------ +Properties requiring vendor prefixes: +None +Property values requiring vendor prefixes: +None +------------------------------------------------------------------------ +List of files: +/LayoutTests/imported/w3c/web-platform-tests/gpc/support/getGPC.py +/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.html +/LayoutTests/imported/w3c/web-platform-tests/gpc/support/navigator-globalPrivacyControl.js diff --git a/LayoutTests/imported/w3c/web-platform-tests/gpc/w3c-import.log b/LayoutTests/imported/w3c/web-platform-tests/gpc/w3c-import.log new file mode 100644 index 000000000000..a359ccc5e41e --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/gpc/w3c-import.log @@ -0,0 +1,22 @@ +The tests in this directory were imported from the W3C repository. +Do NOT modify these tests directly in WebKit. +Instead, create a pull request on the WPT github: + https://github.com/web-platform-tests/wpt + +Then run the Tools/Scripts/import-w3c-tests in WebKit to reimport + +Do NOT modify or remove this file. + +------------------------------------------------------------------------ +Properties requiring vendor prefixes: +None +Property values requiring vendor prefixes: +None +------------------------------------------------------------------------ +List of files: +/LayoutTests/imported/w3c/web-platform-tests/gpc/META.yml +/LayoutTests/imported/w3c/web-platform-tests/gpc/WEB_FEATURES.yml +/LayoutTests/imported/w3c/web-platform-tests/gpc/global_privacy_control.testdriver.html +/LayoutTests/imported/w3c/web-platform-tests/gpc/idlharness.any.js +/LayoutTests/imported/w3c/web-platform-tests/gpc/navigator-globalPrivacyControl.https.html +/LayoutTests/imported/w3c/web-platform-tests/gpc/sec-gpc.https.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/resources/testdriver.js b/LayoutTests/imported/w3c/web-platform-tests/resources/testdriver.js index 5b390dedeb72..1d07c560d332 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/resources/testdriver.js +++ b/LayoutTests/imported/w3c/web-platform-tests/resources/testdriver.js @@ -2169,6 +2169,25 @@ */ clear_display_features: function(context=null) { return window.test_driver_internal.clear_display_features(context); + }, + + /** + * Gets the current Global Privacy Control (GPC) setting. + * + * @returns {Promise<{gpc: boolean}>} Fulfilled with the current GPC state. + */ + get_global_privacy_control: function(context=null) { + return window.test_driver_internal.get_global_privacy_control(context); + }, + + /** + * Sets the Global Privacy Control (GPC) setting. + * + * @param {boolean} value - The GPC value to set. + * @returns {Promise<{gpc: boolean}>} Fulfilled with the new GPC state. + */ + set_global_privacy_control: function(value, context=null) { + return window.test_driver_internal.set_global_privacy_control(value, context); } }; diff --git a/LayoutTests/platform/gtk/fast/dom/navigator-detached-no-crash-expected.txt b/LayoutTests/platform/gtk/fast/dom/navigator-detached-no-crash-expected.txt index 9fbf6c26afa2..3985408085b2 100644 --- a/LayoutTests/platform/gtk/fast/dom/navigator-detached-no-crash-expected.txt +++ b/LayoutTests/platform/gtk/fast/dom/navigator-detached-no-crash-expected.txt @@ -7,6 +7,7 @@ navigator.clearAppBadge() is OK navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.javaEnabled() is OK @@ -43,6 +44,7 @@ navigator.clearAppBadge() is OK navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.javaEnabled() is OK diff --git a/LayoutTests/platform/mac-wk2/fast/dom/navigator-detached-no-crash-expected.txt b/LayoutTests/platform/mac-wk2/fast/dom/navigator-detached-no-crash-expected.txt index a22c11487597..85db7c482b97 100644 --- a/LayoutTests/platform/mac-wk2/fast/dom/navigator-detached-no-crash-expected.txt +++ b/LayoutTests/platform/mac-wk2/fast/dom/navigator-detached-no-crash-expected.txt @@ -10,6 +10,7 @@ navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK navigator.credentials is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.isLoggedIn() is OK @@ -52,6 +53,7 @@ navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK navigator.credentials is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.isLoggedIn() is OK diff --git a/LayoutTests/platform/win/fast/dom/navigator-detached-no-crash-expected.txt b/LayoutTests/platform/win/fast/dom/navigator-detached-no-crash-expected.txt index 3ec50471e3cc..c4876969d6e4 100644 --- a/LayoutTests/platform/win/fast/dom/navigator-detached-no-crash-expected.txt +++ b/LayoutTests/platform/win/fast/dom/navigator-detached-no-crash-expected.txt @@ -7,6 +7,7 @@ navigator.clearAppBadge() is OK navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.javaEnabled() is OK @@ -40,6 +41,7 @@ navigator.clearAppBadge() is OK navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.javaEnabled() is OK diff --git a/LayoutTests/platform/wpe/fast/dom/navigator-detached-no-crash-expected.txt b/LayoutTests/platform/wpe/fast/dom/navigator-detached-no-crash-expected.txt index 9fbf6c26afa2..3985408085b2 100644 --- a/LayoutTests/platform/wpe/fast/dom/navigator-detached-no-crash-expected.txt +++ b/LayoutTests/platform/wpe/fast/dom/navigator-detached-no-crash-expected.txt @@ -7,6 +7,7 @@ navigator.clearAppBadge() is OK navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.javaEnabled() is OK @@ -43,6 +44,7 @@ navigator.clearAppBadge() is OK navigator.clipboard is OK navigator.contacts is OK navigator.cookieEnabled is OK +navigator.globalPrivacyControl is OK navigator.gpu is OK navigator.hardwareConcurrency is OK navigator.javaEnabled() is OK diff --git a/LayoutTests/resources/testdriver-vendor.js b/LayoutTests/resources/testdriver-vendor.js index 66ca7a28970d..ead081987d5f 100644 --- a/LayoutTests/resources/testdriver-vendor.js +++ b/LayoutTests/resources/testdriver-vendor.js @@ -725,3 +725,21 @@ window.test_driver_internal.set_storage_access = async function (origin, embeddi context = context ?? window; await context.testRunner.setStorageAccess(blocked); } + +/** + * + * @returns {Promise} + */ +window.test_driver_internal.get_global_privacy_control = function() { + return Promise.resolve({ gpc: testRunner.getGlobalPrivacyControl() }); +} + +/** + * + * @param {value} bool + * @returns {Promise} + */ +window.test_driver_internal.set_global_privacy_control = function(value) { + testRunner.setGlobalPrivacyControl(value); + return Promise.resolve({ gpc: value }); +} diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml index 0756b4b9e7e4..c459036f3edd 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml @@ -3413,6 +3413,20 @@ GetUserMediaRequiresFocus: WebCore: default: true +GlobalPrivacyControlEnabled: + type: bool + status: testable + category: privacy + humanReadableName: "Global Privacy Control" + humanReadableDescription: "Enable Global Privacy Control (GPC) signal" + defaultValue: + WebKitLegacy: + default: false + WebKit: + default: false + WebCore: + default: false + GoogleAntiFlickerOptimizationQuirkEnabled: type: bool status: mature diff --git a/Source/WebCore/CMakeLists.txt b/Source/WebCore/CMakeLists.txt index 374a9fa3f945..0ace8730ba5d 100644 --- a/Source/WebCore/CMakeLists.txt +++ b/Source/WebCore/CMakeLists.txt @@ -2113,6 +2113,7 @@ set(WebCore_SUPPLEMENTAL_IDL_FILES page/DOMWindow+VisualViewport.idl page/Navigator+UserActivation.idl page/NavigatorCookies.idl + page/NavigatorGlobalPrivacyControl.idl page/NavigatorID.idl page/NavigatorLanguage.idl page/NavigatorOnLine.idl diff --git a/Source/WebCore/DerivedSources-input.xcfilelist b/Source/WebCore/DerivedSources-input.xcfilelist index 2b14c5afe918..880bddf65122 100644 --- a/Source/WebCore/DerivedSources-input.xcfilelist +++ b/Source/WebCore/DerivedSources-input.xcfilelist @@ -1928,6 +1928,7 @@ $(PROJECT_DIR)/page/NavigationNavigationType.idl $(PROJECT_DIR)/page/NavigationTransition.idl $(PROJECT_DIR)/page/Navigator+LoginStatus.idl $(PROJECT_DIR)/page/Navigator+UserActivation.idl +$(PROJECT_DIR)/page/NavigatorGlobalPrivacyControl.idl $(PROJECT_DIR)/page/Navigator.idl $(PROJECT_DIR)/page/NavigatorCookies.idl $(PROJECT_DIR)/page/NavigatorID.idl diff --git a/Source/WebCore/DerivedSources-output.xcfilelist b/Source/WebCore/DerivedSources-output.xcfilelist index 32166c01615e..8ffbd1e610b1 100644 --- a/Source/WebCore/DerivedSources-output.xcfilelist +++ b/Source/WebCore/DerivedSources-output.xcfilelist @@ -2099,6 +2099,8 @@ $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigator+Geolocation.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigator+Geolocation.h $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigator+LoginStatus.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigator+LoginStatus.h +$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigatorGlobalPrivacyControl.cpp +$(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigatorGlobalPrivacyControl.h $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigator+MediaCapabilities.cpp $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigator+MediaCapabilities.h $(BUILT_PRODUCTS_DIR)/DerivedSources/WebCore/JSNavigator+MediaDevices.cpp diff --git a/Source/WebCore/DerivedSources.make b/Source/WebCore/DerivedSources.make index f5c727aab58a..224cc28cbc17 100644 --- a/Source/WebCore/DerivedSources.make +++ b/Source/WebCore/DerivedSources.make @@ -1584,6 +1584,7 @@ JS_BINDING_IDLS := \ $(WebCore)/page/NavigatorUABrandVersion.idl \ $(WebCore)/page/NavigatorUAData.idl \ $(WebCore)/page/Navigator+LoginStatus.idl \ + $(WebCore)/page/NavigatorGlobalPrivacyControl.idl \ $(WebCore)/page/Navigator+UserActivation.idl \ $(WebCore)/page/NavigatorCookies.idl \ $(WebCore)/page/NavigatorID.idl \ diff --git a/Source/WebCore/Sources.txt b/Source/WebCore/Sources.txt index 29de6b9de4c7..d20fb01dfe36 100644 --- a/Source/WebCore/Sources.txt +++ b/Source/WebCore/Sources.txt @@ -2217,6 +2217,7 @@ page/NavigationHistoryEntry.cpp page/NavigationTransition.cpp page/Navigator.cpp page/NavigatorBase.cpp +page/NavigatorGlobalPrivacyControl.cpp page/NavigatorLoginStatus.cpp page/NavigatorUAData.cpp page/OpportunisticTaskScheduler.cpp diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj index cabae6f3b723..9b9e538c5ffd 100644 --- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj +++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj @@ -2503,6 +2503,7 @@ 55F1F4A427BC993300132A6B /* WebAssemblyCachedScriptSourceProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 55F1F4A227BC993200132A6B /* WebAssemblyCachedScriptSourceProvider.h */; }; 55FA7FFB2110ECD7005AEFE7 /* SVGZoomAndPanType.h in Headers */ = {isa = PBXBuildFile; fileRef = 55FA7FFA2110ECD7005AEFE7 /* SVGZoomAndPanType.h */; }; 55FD9FC127B20D520045DA1F /* WebAssemblyScriptSourceCode.h in Headers */ = {isa = PBXBuildFile; fileRef = 55FD9FBF27B20D520045DA1F /* WebAssemblyScriptSourceCode.h */; }; + 564EF2812FA167E3008E9EC3 /* NavigatorGlobalPrivacyControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 564EF27D2FA05074008E9EC3 /* NavigatorGlobalPrivacyControl.h */; }; 5704405A1E53936200356601 /* JSAesCbcCfbParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 570440591E53936200356601 /* JSAesCbcCfbParams.h */; }; 5706A6981DDE5E4600A03B14 /* JSRsaOaepParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 5706A6971DDE5E4600A03B14 /* JSRsaOaepParams.h */; }; 57093D1324C97C8F0086C52D /* FetchResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 413015D61C7B570400091C6E /* FetchResponse.h */; }; @@ -13237,6 +13238,9 @@ 55FA7FFA2110ECD7005AEFE7 /* SVGZoomAndPanType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SVGZoomAndPanType.h; sourceTree = ""; }; 55FD9FBF27B20D520045DA1F /* WebAssemblyScriptSourceCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebAssemblyScriptSourceCode.h; sourceTree = ""; }; 560A7F202DB83BDE00B69AF8 /* SVGFilterEffectGraph.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SVGFilterEffectGraph.h; sourceTree = ""; }; + 564EF27C2FA04E9E008E9EC3 /* NavigatorGlobalPrivacyControl.idl */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NavigatorGlobalPrivacyControl.idl"; sourceTree = ""; }; + 564EF27D2FA05074008E9EC3 /* NavigatorGlobalPrivacyControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NavigatorGlobalPrivacyControl.h; sourceTree = ""; }; + 564EF27E2FA05074008E9EC3 /* NavigatorGlobalPrivacyControl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = NavigatorGlobalPrivacyControl.cpp; sourceTree = ""; }; 565E7E6449095E4933B5F978 /* Volume2.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = Volume2.svg; sourceTree = ""; }; 56FFE14C3114000000000000 /* StyleLocalPropertyRegistry.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = StyleLocalPropertyRegistry.cpp; sourceTree = ""; }; 570440571E53851600356601 /* CryptoAlgorithmAESCFBCocoa.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CryptoAlgorithmAESCFBCocoa.cpp; sourceTree = ""; }; @@ -31055,6 +31059,9 @@ E12719C90EEEC21300F61213 /* NavigatorBase.cpp */, E12719C60EEEC16800F61213 /* NavigatorBase.h */, 7C2E0BD92511800E005F3C87 /* NavigatorCookies.idl */, + 564EF27E2FA05074008E9EC3 /* NavigatorGlobalPrivacyControl.cpp */, + 564EF27D2FA05074008E9EC3 /* NavigatorGlobalPrivacyControl.h */, + 564EF27C2FA04E9E008E9EC3 /* NavigatorGlobalPrivacyControl.idl */, 7C5BEA3A1E9EE77100CC517B /* NavigatorID.idl */, 7C5BEA3B1E9EE77100CC517B /* NavigatorLanguage.idl */, 6D61E3762C2E016F00F1C6AA /* NavigatorLoginStatus.cpp */, @@ -47244,6 +47251,7 @@ 0753E09A2E595C9F00A52914 /* NavigatorEME.h in Headers */, 513EB5F22BF2D9B800BC98B1 /* NavigatorGamepad.h in Headers */, 9711460414EF009A00674FD9 /* NavigatorGeolocation.h in Headers */, + 564EF2812FA167E3008E9EC3 /* NavigatorGlobalPrivacyControl.h in Headers */, 5EA725D61ACABD5700EAD17B /* NavigatorMediaDevices.h in Headers */, 077BA572260FA0140072F19F /* NavigatorMediaSession.h in Headers */, 93B0A65326CDD14100AA21E4 /* NavigatorPermissions.h in Headers */, diff --git a/Source/WebCore/page/Navigator.idl b/Source/WebCore/page/Navigator.idl index 16f0485fe16c..7284c98993dc 100644 --- a/Source/WebCore/page/Navigator.idl +++ b/Source/WebCore/page/Navigator.idl @@ -40,4 +40,5 @@ Navigator includes NavigatorServiceWorker; Navigator includes NavigatorShare; Navigator includes NavigatorStorage; Navigator includes NavigatorGPU; +Navigator includes NavigatorGlobalPrivacyControl; [EnabledByQuirk=needsNavigatorUserAgentData] Navigator includes NavigatorUA; diff --git a/Source/WebCore/page/NavigatorGlobalPrivacyControl.cpp b/Source/WebCore/page/NavigatorGlobalPrivacyControl.cpp new file mode 100644 index 000000000000..a7b75c902303 --- /dev/null +++ b/Source/WebCore/page/NavigatorGlobalPrivacyControl.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019-2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "NavigatorGlobalPrivacyControl.h" + +#include "Navigator.h" +#include "WorkerNavigator.h" + +namespace WebCore { + +bool NavigatorGlobalPrivacyControl::globalPrivacyControlEnabled(Navigator& navigator) +{ + auto* frame = navigator.frame(); + if (!frame) + return false; + return frame->settings().globalPrivacyControlEnabled(); +} + +bool NavigatorGlobalPrivacyControl::globalPrivacyControlEnabled(WorkerNavigator& navigator) +{ + RefPtr scope = navigator.scriptExecutionContext(); + if (!scope) + return false; + return scope->settingsValues().globalPrivacyControlEnabled; +} + +} // namespace WebCore diff --git a/Source/WebCore/page/NavigatorGlobalPrivacyControl.h b/Source/WebCore/page/NavigatorGlobalPrivacyControl.h new file mode 100644 index 000000000000..cc2ce967b11a --- /dev/null +++ b/Source/WebCore/page/NavigatorGlobalPrivacyControl.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019-2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +namespace WebCore { + +class Navigator; +class WorkerNavigator; + +class NavigatorGlobalPrivacyControl { +public: + static bool globalPrivacyControlEnabled(Navigator&); + static bool globalPrivacyControlEnabled(WorkerNavigator&); +}; +} diff --git a/Source/WebCore/page/NavigatorGlobalPrivacyControl.idl b/Source/WebCore/page/NavigatorGlobalPrivacyControl.idl new file mode 100644 index 000000000000..ad4328c71eb2 --- /dev/null +++ b/Source/WebCore/page/NavigatorGlobalPrivacyControl.idl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +// https://www.w3.org/TR/gpc/ +[ + ImplementedBy=NavigatorGlobalPrivacyControl +] interface mixin NavigatorGlobalPrivacyControl { + [ImplementedAs=globalPrivacyControlEnabled] readonly attribute boolean globalPrivacyControl; +}; diff --git a/Source/WebCore/page/WorkerNavigator.idl b/Source/WebCore/page/WorkerNavigator.idl index fa063e644888..d3c25610ba3f 100644 --- a/Source/WebCore/page/WorkerNavigator.idl +++ b/Source/WebCore/page/WorkerNavigator.idl @@ -43,3 +43,4 @@ WorkerNavigator includes NavigatorServiceWorker; WorkerNavigator includes NavigatorStorage; WorkerNavigator includes NavigatorGPU; WorkerNavigator includes NavigatorUA; +WorkerNavigator includes NavigatorGlobalPrivacyControl; diff --git a/Source/WebCore/platform/network/HTTPHeaderNames.in b/Source/WebCore/platform/network/HTTPHeaderNames.in index 594c9745d3c2..487383021aa9 100644 --- a/Source/WebCore/platform/network/HTTPHeaderNames.in +++ b/Source/WebCore/platform/network/HTTPHeaderNames.in @@ -90,6 +90,7 @@ Reporting-Endpoints Sec-Fetch-Dest Sec-Fetch-Mode Sec-Fetch-Site +Sec-GPC Sec-Purpose Sec-Speculation-Tags Speculation-Rules diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h index e6b489f9cc24..633f69dc9635 100644 --- a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h +++ b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h @@ -131,6 +131,7 @@ struct NetworkResourceLoadParameters { bool isInitiatorPrefetch { false }; bool isInitiatedByDedicatedWorker { false }; + bool globalPrivacyControlEnabled { false }; }; } // namespace WebKit diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in index 4de61368ff40..1b777b9e98fd 100644 --- a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in +++ b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in @@ -110,6 +110,7 @@ enum class WebKit::NavigatingToAppBoundDomain : bool; bool isInitiatorPrefetch; bool isInitiatedByDedicatedWorker; + bool globalPrivacyControlEnabled; }; using WebCore::FetchingWorkerIdentifier = Variant; diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp index a95dc270c0ad..799dce053d70 100644 --- a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp +++ b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp @@ -443,6 +443,12 @@ void NetworkResourceLoader::startNetworkLoad(ResourceRequest&& request, FirstLoa if (networkSession->shouldSendPrivateTokenIPCForTesting()) connectionToWebProcess().networkProcess().parentProcessConnection()->send(Messages::NetworkProcessProxy::DidAllowPrivateTokenUsageByThirdPartyForTesting(sessionID(), request.isPrivateTokenUsageByThirdPartyAllowed(), request.url()), 0); + if (m_parameters.globalPrivacyControlEnabled) { + auto requestOrigin = SecurityOrigin::create(request.url()); + if (requestOrigin->isPotentiallyTrustworthy()) + request.addHTTPHeaderFieldIfNotPresent(HTTPHeaderName::SecGPC, "1"_s); + } + parameters.request = WTF::move(request); parameters.isNavigatingToAppBoundDomain = m_parameters.isNavigatingToAppBoundDomain; m_networkLoad = NetworkLoad::create(*this, WTF::move(parameters), *networkSession); diff --git a/Source/WebKit/UIProcess/API/C/WKPreferences.cpp b/Source/WebKit/UIProcess/API/C/WKPreferences.cpp index f4dbced352cf..2d44e7589598 100644 --- a/Source/WebKit/UIProcess/API/C/WKPreferences.cpp +++ b/Source/WebKit/UIProcess/API/C/WKPreferences.cpp @@ -1620,6 +1620,11 @@ bool WKPreferencesGetPunchOutWhiteBackgroundsInDarkMode(WKPreferencesRef prefere return protect(toImpl(preferencesRef))->punchOutWhiteBackgroundsInDarkMode(); } +bool WKPreferencesGetBoolValueForKeyForTesting(WKPreferencesRef preferencesRef, WKStringRef key) +{ + return toImpl(preferencesRef)->store().getBoolValueForKey(toWTFString(key)); +} + void WKPreferencesSetCaptureAudioInUIProcessEnabled(WKPreferencesRef, bool) { } diff --git a/Source/WebKit/UIProcess/API/C/WKPreferencesRefPrivate.h b/Source/WebKit/UIProcess/API/C/WKPreferencesRefPrivate.h index 03e448d86680..1666625d06f6 100644 --- a/Source/WebKit/UIProcess/API/C/WKPreferencesRefPrivate.h +++ b/Source/WebKit/UIProcess/API/C/WKPreferencesRefPrivate.h @@ -457,6 +457,7 @@ WK_EXPORT bool WKPreferencesGetColorFilterEnabled(WKPreferencesRef); WK_EXPORT void WKPreferencesSetPunchOutWhiteBackgroundsInDarkMode(WKPreferencesRef, bool flag); WK_EXPORT bool WKPreferencesGetPunchOutWhiteBackgroundsInDarkMode(WKPreferencesRef); +WK_EXPORT bool WKPreferencesGetBoolValueForKeyForTesting(WKPreferencesRef, WKStringRef key); // The following are all deprecated and do nothing. They should be removed when possible. diff --git a/Source/WebKit/WebKit.xcodeproj/project.pbxproj b/Source/WebKit/WebKit.xcodeproj/project.pbxproj index 904b2fd51cb3..55a2fe41884e 100644 --- a/Source/WebKit/WebKit.xcodeproj/project.pbxproj +++ b/Source/WebKit/WebKit.xcodeproj/project.pbxproj @@ -682,7 +682,6 @@ 2ADBEA4C2F358BEF00EDB398 /* WKWebView+WKBannerViewOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ADBEA4B2F358BEF00EDB398 /* WKWebView+WKBannerViewOverlay.swift */; }; 2AE6D2B72F501404002BE327 /* WebViewRepresentable+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ADBEA4B2F358BEF00EDB399 /* WebViewRepresentable+Extras.swift */; }; 2BBCCE242EB2B3F500FE7E5A /* RemotePageScreenOrientationManagerProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 2BBCCE232EB2B3F500FE7E5A /* RemotePageScreenOrientationManagerProxy.h */; }; - F5AE074483234F46B7D93F3F /* RemotePageWebAuthenticatorCoordinatorProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 729F145827DA4BFB83A7C750 /* RemotePageWebAuthenticatorCoordinatorProxy.h */; }; 2D0C56FD229F1DEA00BD33E7 /* DeviceManagementSoftLink.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0C56FB229F1DEA00BD33E7 /* DeviceManagementSoftLink.h */; }; 2D0C56FE229F1DEA00BD33E7 /* DeviceManagementSoftLink.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2D0C56FC229F1DEA00BD33E7 /* DeviceManagementSoftLink.mm */; }; 2D0FD45E2F75127900B1A53C /* SwipeProgressTrackerMac.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0FD45C2F75126800B1A53C /* SwipeProgressTrackerMac.h */; }; @@ -2710,6 +2709,7 @@ F4FE87B72E2F25030065A3F5 /* _WKTextExtractionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = F4FE87B62E2F25030065A3F5 /* _WKTextExtractionInternal.h */; }; F4FE87B82E2F25030065A3F5 /* _WKTextExtraction.h in Headers */ = {isa = PBXBuildFile; fileRef = F4FE87B42E2F25030065A3F5 /* _WKTextExtraction.h */; settings = {ATTRIBUTES = (Private, ); }; }; F4FE87B92E2F25030065A3F5 /* _WKTextExtraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FE87B52E2F25030065A3F5 /* _WKTextExtraction.swift */; }; + F5AE074483234F46B7D93F3F /* RemotePageWebAuthenticatorCoordinatorProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 729F145827DA4BFB83A7C750 /* RemotePageWebAuthenticatorCoordinatorProxy.h */; }; F6113E25126CE1820057D0A7 /* APIUserContentURLPattern.h in Headers */ = {isa = PBXBuildFile; fileRef = F6113E24126CE1820057D0A7 /* APIUserContentURLPattern.h */; }; F6113E29126CE19B0057D0A7 /* WKUserContentURLPattern.h in Headers */ = {isa = PBXBuildFile; fileRef = F6113E27126CE19B0057D0A7 /* WKUserContentURLPattern.h */; settings = {ATTRIBUTES = (Private, ); }; }; F634445612A885C8000612D8 /* APISecurityOrigin.h in Headers */ = {isa = PBXBuildFile; fileRef = F634445512A885C8000612D8 /* APISecurityOrigin.h */; }; @@ -4880,8 +4880,6 @@ 2B1B1C3C2E3A86F400D7D9ED /* UnretainedLocalVarsCheckerExpectations */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnretainedLocalVarsCheckerExpectations; sourceTree = ""; }; 2BBCCE222EB2B3DA00FE7E5A /* RemotePageScreenOrientationManagerProxy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RemotePageScreenOrientationManagerProxy.cpp; sourceTree = ""; }; 2BBCCE232EB2B3F500FE7E5A /* RemotePageScreenOrientationManagerProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemotePageScreenOrientationManagerProxy.h; sourceTree = ""; }; - 729F145827DA4BFB83A7C750 /* RemotePageWebAuthenticatorCoordinatorProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemotePageWebAuthenticatorCoordinatorProxy.h; sourceTree = ""; }; - 729F145827DA4BFB83A7C751 /* RemotePageWebAuthenticatorCoordinatorProxy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RemotePageWebAuthenticatorCoordinatorProxy.cpp; sourceTree = ""; }; 2D0035221BC7414800DA8716 /* PDFPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PDFPlugin.h; path = PDF/PDFPlugin.h; sourceTree = ""; }; 2D0035231BC7414800DA8716 /* PDFPlugin.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = PDFPlugin.mm; path = PDF/PDFPlugin.mm; sourceTree = ""; }; 2D0440922D58810D00E98A2E /* RemoteImageBufferSetConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemoteImageBufferSetConfiguration.h; sourceTree = ""; }; @@ -6970,6 +6968,8 @@ 727A7F492408AEE6004D2931 /* RemoteImageBufferProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemoteImageBufferProxy.h; sourceTree = ""; }; 728E86EF1795188C0087879E /* WebColorPickerMac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebColorPickerMac.h; sourceTree = ""; }; 728E86F01795188C0087879E /* WebColorPickerMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebColorPickerMac.mm; sourceTree = ""; }; + 729F145827DA4BFB83A7C750 /* RemotePageWebAuthenticatorCoordinatorProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemotePageWebAuthenticatorCoordinatorProxy.h; sourceTree = ""; }; + 729F145827DA4BFB83A7C751 /* RemotePageWebAuthenticatorCoordinatorProxy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RemotePageWebAuthenticatorCoordinatorProxy.cpp; sourceTree = ""; }; 72A87BBB287678A700289098 /* RemoteRenderingBackend.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RemoteRenderingBackend.cpp; sourceTree = ""; }; 72B53157253C1E4D0049295A /* RemoteResourceCacheProxy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RemoteResourceCacheProxy.cpp; sourceTree = ""; }; 72B53158253C1E4D0049295A /* RemoteResourceCacheProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemoteResourceCacheProxy.h; sourceTree = ""; }; diff --git a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp index fcc9ba2089f1..f9e0b62dc43d 100644 --- a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp +++ b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp @@ -402,6 +402,7 @@ static void addParametersShared(const LocalFrame* frame, NetworkResourceLoadPara parameters.isClearSiteDataExecutionContextEnabled = document->settings().clearSiteDataExecutionContextsSupportEnabled(); parameters.mayBlockNetworkRequest = (!isMainFrameNavigation && document->settings().scriptTrackingPrivacyNetworkRequestBlockingLatchEnabled()) ? std::optional { WebProcess::singleton().shouldBlockRequest(parameters.request.url(), protect(document->topOrigin())) } : std::nullopt; + parameters.globalPrivacyControlEnabled = document->settings().globalPrivacyControlEnabled(); } if (RefPtr page = frame->page()) { diff --git a/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl b/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl index f30dced17fc6..2c3ff7db6277 100644 --- a/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl +++ b/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl @@ -206,6 +206,10 @@ interface TestRunner { // Screen Wake Lock undefined setScreenWakeLockPermission(boolean value); + // Global Privacy Control + undefined setGlobalPrivacyControl(boolean value); + boolean getGlobalPrivacyControl(); + // MediaStream undefined setCameraPermission(boolean value); undefined setMicrophonePermission(boolean value); diff --git a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp index 8b7fefa51e66..2d8662695588 100644 --- a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp +++ b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp @@ -1117,6 +1117,16 @@ bool TestRunner::hasStatisticsIsolatedSession(JSStringRef hostName) return postSynchronousPageMessageReturningBoolean("HasStatisticsIsolatedSession", hostName); } +void TestRunner::setGlobalPrivacyControl(bool value) +{ + postSynchronousMessageWithReturnValue("SetGlobalPrivacyControl", adoptWK(WKBooleanCreate(value))); +} + +bool TestRunner::getGlobalPrivacyControl() +{ + return postSynchronousMessageReturningBoolean("GetGlobalPrivacyControl"); +} + void TestRunner::installTextDidChangeInTextFieldCallback(JSContextRef context, JSValueRef callback) { cacheTestRunnerCallback(context, TextDidChangeInTextFieldCallbackID, callback); diff --git a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h index 1da72641c8fb..19ec947ce32e 100644 --- a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h +++ b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h @@ -403,6 +403,9 @@ class TestRunner : public JSWrappable { void setStatisticsCacheMaxAgeCap(double seconds); bool hasStatisticsIsolatedSession(JSStringRef hostName); + void setGlobalPrivacyControl(bool); + bool getGlobalPrivacyControl(); + // Injected bundle form client. void installTextDidChangeInTextFieldCallback(JSContextRef, JSValueRef callback); void textDidChangeInTextFieldCallback(); diff --git a/Tools/WebKitTestRunner/TestController.cpp b/Tools/WebKitTestRunner/TestController.cpp index e493aa6591a4..0d6aa6ad1a9f 100644 --- a/Tools/WebKitTestRunner/TestController.cpp +++ b/Tools/WebKitTestRunner/TestController.cpp @@ -1424,6 +1424,7 @@ void TestController::resetPreferencesToConsistentValues(const TestOptions& optio WKPreferencesSetMinimumFontSize(preferences, 0); WKPreferencesSetBoolValueForKeyForTesting(preferences, options.allowTestOnlyIPC(), toWK("AllowTestOnlyIPC").get()); + WKPreferencesSetBoolValueForKeyForTesting(preferences, false, toWK("GlobalPrivacyControlEnabled").get()); for (const auto& [key, value] : options.boolWebPreferenceFeatures()) WKPreferencesSetBoolValueForKeyForTesting(preferences, value, toWK(key).get()); diff --git a/Tools/WebKitTestRunner/TestInvocation.cpp b/Tools/WebKitTestRunner/TestInvocation.cpp index 9272ec6494c7..d82a18162793 100644 --- a/Tools/WebKitTestRunner/TestInvocation.cpp +++ b/Tools/WebKitTestRunner/TestInvocation.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -1323,6 +1324,16 @@ WKRetainPtr TestInvocation::didReceiveSynchronousMessageFromInjectedB return nullptr; } + if (WKStringIsEqualToUTF8CString(messageName, "GetGlobalPrivacyControl")) { + bool value = WKPreferencesGetBoolValueForKeyForTesting(TestController::singleton().platformPreferences(), toWK("GlobalPrivacyControlEnabled").get()); + return adoptWK(WKBooleanCreate(value)).leakRef(); + } + + if (WKStringIsEqualToUTF8CString(messageName, "SetGlobalPrivacyControl")) { + WKPreferencesSetBoolValueForKeyForTesting(TestController::singleton().platformPreferences(), booleanValue(messageBody), toWK("GlobalPrivacyControlEnabled").get()); + return nullptr; + } + #if ENABLE(MODEL_ELEMENT_IMMERSIVE) if (WKStringIsEqualToUTF8CString(messageName, "ExitImmersive")) { TestController::singleton().exitImmersive(); From 433bc2667c27215659742c8d5ac7703f7f1a2707 Mon Sep 17 00:00:00 2001 From: Vitor Roriz Date: Fri, 15 May 2026 19:32:12 -0700 Subject: [PATCH 122/424] Resync `css/cssom` from WPT Upstream https://bugs.webkit.org/show_bug.cgi?id=314901 rdar://177175427 Reviewed by Brandon Stewart. Upstream commit: https://github.com/web-platform-tests/wpt/commit/8440c03c3d9da98b80534a837a918edb6c328e65 * LayoutTests/imported/w3c/resources/resource-files.json: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-template-adoption.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/WEB_FEATURES.yml: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/adoptedstylesheets-observablearray-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/adoptedstylesheets-observablearray.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/all-initial-csstext-expected.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/all-initial-csstext-ref.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/all-initial-csstext.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/caretPositionFromPoint.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/caretRangeFromPoint-replace-document.tentative.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/caretRangeFromPoint-textarea-transform.tentative-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/caretRangeFromPoint-textarea-transform.tentative.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/caretRangeFromPoint.tentative-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/caretRangeFromPoint.tentative.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-all-shorthand-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-all-shorthand.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-nested-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-nested.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-removeProperty-all-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-removeProperty-all.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/font-family-serialization-001-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/font-family-serialization-001.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/invalid-pseudo-elements.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/selectorText-modification-restyle-002.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/serialize-values-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/serialize-values.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/set-selector-text-detached-rule-delete-crash.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/set-selector-text-detached-rule-replace-crash.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/shorthand-values-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/shorthand-values.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/stylesheet-cross-origin-redirect-quirks.sub-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/stylesheet-cross-origin-redirect-quirks.sub.html: Added. * LayoutTests/imported/w3c/web-platform-tests/css/cssom/stylesheet-same-origin.sub-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/stylesheet-same-origin.sub.html: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/w3c-import.log: * LayoutTests/platform/glib/imported/w3c/web-platform-tests/css/cssom/stylesheet-same-origin.sub-expected.txt: Removed. * Source/WebCore/css/CSSProperties.json: Canonical link: https://commits.webkit.org/313348@main --- .../w3c/resources/resource-files.json | 1 + .../CSSStyleDeclaration-iterator-expected.txt | 3 + .../cssom/CSSStyleDeclaration-iterator.html | 12 ++ ...onstructable-replace-thenable-expected.txt | 3 + ...eSheet-constructable-replace-thenable.html | 43 +++++ .../CSSStyleSheet-template-adoption.html | 3 + .../css/cssom/WEB_FEATURES.yml | 85 +++++++++ ...edstylesheets-observablearray-expected.txt | 1 + .../adoptedstylesheets-observablearray.html | 11 ++ .../cssom/all-initial-csstext-expected.html | 20 +++ .../css/cssom/all-initial-csstext-ref.html | 20 +++ .../css/cssom/all-initial-csstext.html | 29 ++++ ...eFromPoint-replace-document.tentative.html | 4 +- ...-textarea-transform.tentative-expected.txt | 2 +- ...romPoint-textarea-transform.tentative.html | 2 +- ...caretRangeFromPoint.tentative-expected.txt | 13 +- .../cssom/caretRangeFromPoint.tentative.html | 31 ++-- ...tyledeclaration-all-shorthand-expected.txt | 21 +++ .../cssstyledeclaration-all-shorthand.html | 163 ++++++++++++++++++ .../cssstyledeclaration-nested-expected.txt | 4 + .../css/cssom/cssstyledeclaration-nested.html | 26 +++ ...eclaration-removeProperty-all-expected.txt | 1 + ...ssstyledeclaration-removeProperty-all.html | 7 + ...font-family-serialization-001-expected.txt | 28 ++- .../cssom/font-family-serialization-001.html | 111 +++++++++++- .../css/cssom/invalid-pseudo-elements.html | 1 + ...selectorText-modification-restyle-002.html | 2 +- .../css/cssom/serialize-values-expected.txt | 40 +++-- .../css/cssom/serialize-values.html | 20 ++- ...ector-text-detached-rule-delete-crash.html | 14 ++ ...ctor-text-detached-rule-replace-crash.html | 14 ++ .../css/cssom/shorthand-values-expected.txt | 2 +- .../css/cssom/shorthand-values.html | 2 +- ...ss-origin-redirect-quirks.sub-expected.txt | 6 + ...heet-cross-origin-redirect-quirks.sub.html | 78 +++++++++ .../stylesheet-same-origin.sub-expected.txt | 1 + .../css/cssom/stylesheet-same-origin.sub.html | 7 + .../css/cssom/w3c-import.log | 13 ++ .../stylesheet-same-origin.sub-expected.txt | 8 - 39 files changed, 794 insertions(+), 58 deletions(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator-expected.txt create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable-expected.txt create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/all-initial-csstext-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/all-initial-csstext-ref.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/all-initial-csstext.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-nested-expected.txt create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/cssstyledeclaration-nested.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/set-selector-text-detached-rule-delete-crash.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/set-selector-text-detached-rule-replace-crash.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/stylesheet-cross-origin-redirect-quirks.sub-expected.txt create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/cssom/stylesheet-cross-origin-redirect-quirks.sub.html delete mode 100644 LayoutTests/platform/glib/imported/w3c/web-platform-tests/css/cssom/stylesheet-same-origin.sub-expected.txt diff --git a/LayoutTests/imported/w3c/resources/resource-files.json b/LayoutTests/imported/w3c/resources/resource-files.json index e0cc312b7ed5..29256483914b 100644 --- a/LayoutTests/imported/w3c/resources/resource-files.json +++ b/LayoutTests/imported/w3c/resources/resource-files.json @@ -10347,6 +10347,7 @@ "web-platform-tests/css/cssom/CSSStyleSheet-constructable-concat-ref.html", "web-platform-tests/css/cssom/CSSStyleSheet-constructable-insertRule-base-uri-ref.html", "web-platform-tests/css/cssom/HTMLLinkElement-disabled-alternate-ref.html", + "web-platform-tests/css/cssom/all-initial-csstext-ref.html", "web-platform-tests/css/cssom/insertRule-from-script-ref.html", "web-platform-tests/css/cssom/medialist-dynamic-001-ref.html", "web-platform-tests/css/cssom/selectorText-modification-restyle-001-ref.html", diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator-expected.txt new file mode 100644 index 000000000000..61518390f2b5 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator-expected.txt @@ -0,0 +1,3 @@ + +PASS CSSStyleDeclaration should expose Symbol.iterator + diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator.html b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator.html new file mode 100644 index 000000000000..08175f1d2f6a --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleDeclaration-iterator.html @@ -0,0 +1,12 @@ + + +CSSStyleDeclaration should expose Symbol.iterator + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable-expected.txt new file mode 100644 index 000000000000..d438401db578 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable-expected.txt @@ -0,0 +1,3 @@ + +PASS CSSStyleSheet.replace/replaceSync() when stylesheet is a thenable + diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable.html b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable.html new file mode 100644 index 000000000000..22c6a093a5e7 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-constructable-replace-thenable.html @@ -0,0 +1,43 @@ + + +CSSStyleSheet.replace/replaceSync() when stylesheet is a thenable + + + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-template-adoption.html b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-template-adoption.html index e014627ed8be..1abd7a6aee01 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-template-adoption.html +++ b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/CSSStyleSheet-template-adoption.html @@ -19,6 +19,7 @@ let root = host.attachShadow({ mode: "open" }); root.innerHTML = `
`; root.adoptedStyleSheets = [sheet]; + let adoptedStyleSheets = root.adoptedStyleSheets; function assertAdoptedStyleSheet() { assert_equals(host.ownerDocument, root.firstChild.ownerDocument, "Shadow root was not adopted?"); @@ -27,6 +28,7 @@ if (root.ownerDocument == document) { assert_equals(getComputedStyle(root.firstChild).color, "rgb(0, 0, 255)", "Sheet should apply"); } + assert_equals(adoptedStyleSheets, root.adoptedStyleSheets, "adoptedStyleSheets should be the same array"); } assertAdoptedStyleSheet(); @@ -54,5 +56,6 @@ document.body.appendChild(iframe); iframe.contentDocument.body.appendChild(host); assert_equals(root.adoptedStyleSheets.length, 0); + assert_equals(adoptedStyleSheets, root.adoptedStyleSheets, "adoptedStyleSheets should be the same array"); }, "adoptedStyleSheets won'te be cleared when adopting into/from