From 0496e02cbc5a0bedecce934796a8999a176c88f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 19 Sep 2026 17:09:46 +0200 Subject: [PATCH] Bound embedded source literals for MSVC (#690) --- .../msvc-embedded-source-literals-690.md | 24 ++++++++++ .../native/webscene_embedded_source.h | 48 +++++++++++++++++++ .../webscene_stylesheet_cssom_compatibility.h | 25 ++++++---- .../webscene_v8_runtime_cache_and_frames.inc | 19 ++++---- ...webscene_v8_runtime_file_system_access.inc | 17 ++++--- ...scene_v8_runtime_web_api_compatibility.inc | 4 +- .../stylesheet-cssom.test.mjs | 7 ++- 7 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 docs/validation/msvc-embedded-source-literals-690.md create mode 100644 experiments/WebScene.NativeEngine.Probe/native/webscene_embedded_source.h diff --git a/docs/validation/msvc-embedded-source-literals-690.md b/docs/validation/msvc-embedded-source-literals-690.md new file mode 100644 index 000000000..6027ac69e --- /dev/null +++ b/docs/validation/msvc-embedded-source-literals-690.md @@ -0,0 +1,24 @@ +# MSVC embedded source literal portability + +Issue #690 repairs the Windows NuGet run 35449781793 failures where MSVC +reported C2026 for hand-authored JavaScript embedded in the stylesheet CSSOM, +cache/frame and File System Access sources. Their largest individual raw +literals were 83,590, 16,956 and 38,859 bytes respectively, above MSVC's +16,380-byte string-literal limit. + +Each program is now an ordered array of raw-literal views whose individual +parts stay below a conservative 15,000-byte ceiling. The reusable source +contract checks that ceiling at compile time. The join helper reserves the +exact total size and concatenates each array once at realm installation, +without separators, so V8 receives the same program bytes in one compile and +run operation. The split does not add per-frame work or retained duplicate +payloads. + +The stylesheet CSSOM JavaScript contract reconstructs the array in declaration +order before exercising the existing browser-facing behavior. Future edits +must add another part before any literal crosses the shared ceiling. The +compiler-generated-file incident from the same Windows job remains outside +this issue. + +Only `git diff --check` was run under the directed fast policy. Windows package +compilation and the existing runtime contracts remain for CI. diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_embedded_source.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_embedded_source.h new file mode 100644 index 000000000..2a9b081d6 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_embedded_source.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace webscene_native { + +// MSVC rejects a single string literal above 16,380 bytes. Keep enough +// headroom for source encoding while allowing larger programs to be assembled +// once, in declaration order, without separators. +inline constexpr std::size_t maximum_portable_embedded_source_literal_bytes = + 15'000U; + +template +constexpr bool embedded_source_parts_are_portable( + const std::array& parts) noexcept +{ + for (const auto part : parts) { + if (part.size() > maximum_portable_embedded_source_literal_bytes) { + return false; + } + } + return true; +} + +template +std::string join_embedded_source_parts( + const std::array& parts) +{ + std::size_t source_size = 0U; + for (const auto part : parts) { + if (part.size() > std::numeric_limits::max() - source_size) { + throw std::length_error("embedded source size overflow"); + } + source_size += part.size(); + } + + std::string source; + source.reserve(source_size); + for (const auto part : parts) source.append(part); + return source; +} + +} // namespace webscene_native diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h index 20c50612a..e162d5873 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h @@ -1,5 +1,5 @@ #pragma once -#include +#include "webscene_embedded_source.h" namespace webscene_native { // The pinned WebScene exposes a stable native HTMLStyleElement.sheet object, @@ -10,7 +10,7 @@ namespace webscene_native { // Imported sheets use the host stylesheet loader and keep their authored CSSOM // tree separate from the flattened native publication payload. // Semantics: https://www.w3.org/TR/cssom-1/ -inline constexpr std::string_view cssCompatibilityScript = R"JS( +inline constexpr std::array cssCompatibilityScriptParts{R"JS( // SCENETECH_CSS_COMPATIBILITY_V1 (() => { 'use strict'; @@ -255,7 +255,8 @@ inline constexpr std::string_view cssCompatibilityScript = R"JS( cursor = index + 1; continue; } - if (!atEnd && character === '{') { +)JS", +R"JS( if (!atEnd && character === '{') { const prelude = source.slice(cursor, index).trim(); if (!prelude) exception('A nested rule requires a prelude', 'SyntaxError'); flushDeclarations(); @@ -527,7 +528,8 @@ inline constexpr std::string_view cssCompatibilityScript = R"JS( else if (character === quote) quote = ''; continue; } - if (character === '"' || character === "'") { quote = character; continue; } +)JS", +R"JS( if (character === '"' || character === "'") { quote = character; continue; } if (character === '(') parentheses++; else if (character === ')' && parentheses) parentheses--; else if (character === '[') brackets++; @@ -821,7 +823,8 @@ inline constexpr std::string_view cssCompatibilityScript = R"JS( const identifier = /^(-?[_a-zA-Z][_a-zA-Z0-9-]*)\s+/.exec(source); if (identifier) { prefix = identifier[1]; - source = source.slice(identifier[0].length).trim(); +)JS", +R"JS( source = source.slice(identifier[0].length).trim(); } let namespaceURI = ''; if (source[0] === '"' || source[0] === "'") { @@ -1098,7 +1101,8 @@ inline constexpr std::string_view cssCompatibilityScript = R"JS( } } if (loaded?.url) resolvedHref = String(loaded.url); - ancestors.add(resolvedHref); +)JS", +R"JS( ancestors.add(resolvedHref); childState = { sheet: childSheet, owner: null, @@ -1382,7 +1386,8 @@ inline constexpr std::string_view cssCompatibilityScript = R"JS( if (attached()) publish(state); return index; } }, - deleteRule: { writable: true, value(index) { +)JS", +R"JS( deleteRule: { writable: true, value(index) { if (arguments.length === 0) throw new TypeError('A rule index is required'); synchronize(state); index = Number(index) >>> 0; @@ -1677,7 +1682,8 @@ inline constexpr std::string_view cssCompatibilityScript = R"JS( } } }, - insertRule: { configurable: true, writable: true, value(rule, index = 0) { +)JS", +R"JS( insertRule: { configurable: true, writable: true, value(rule, index = 0) { if (arguments.length === 0) throw new TypeError('A CSS rule is required'); const current = stateFor(this); index = Number(index) >>> 0; @@ -1950,5 +1956,6 @@ inline constexpr std::string_view cssCompatibilityScript = R"JS( value: augment, configurable: true }); })(); -)JS"; +)JS"}; +static_assert(embedded_source_parts_are_portable(cssCompatibilityScriptParts)); } diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc index eddc42a10..d89cf48c3 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc @@ -1529,7 +1529,7 @@ // realm-local and route delivery through this frame's setTimeout so a // posted message is a serialized browser task rather than a nested V8 // call. This mirrors the working managed V8 iframe implementation. - constexpr std::string_view message_channel_source = R"JS( + constexpr std::array message_channel_source_parts{R"JS( (() => { class MessagePort { constructor() { @@ -1735,8 +1735,8 @@ `${encode(name)}=${encode(value)}`).join('&'); } } - )JS"; - constexpr std::string_view message_channel_source_suffix = R"JS( + )JS", + R"JS( class WebSceneFormData { constructor(form = undefined, submitter = null) { __webSceneRecordWebApi( @@ -1945,7 +1945,8 @@ HierarchyRequestError: 3, WrongDocumentError: 4, InvalidCharacterError: 5, - NoModificationAllowedError: 7, +)JS", + R"JS( NoModificationAllowedError: 7, NotFoundError: 8, NotSupportedError: 9, InUseAttributeError: 10, @@ -2046,12 +2047,10 @@ } }); })(); - )JS"; - std::string message_channel_bootstrap; - message_channel_bootstrap.reserve( - message_channel_source.size() + message_channel_source_suffix.size()); - message_channel_bootstrap.append(message_channel_source); - message_channel_bootstrap.append(message_channel_source_suffix); + )JS"}; + static_assert(embedded_source_parts_are_portable(message_channel_source_parts)); + auto message_channel_bootstrap = + join_embedded_source_parts(message_channel_source_parts); auto message_channel_script = v8::Script::Compile( local_context, js_string(isolate, message_channel_bootstrap.c_str())).ToLocalChecked(); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc index 44aa4af5b..753a1fa8a 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc @@ -1335,7 +1335,7 @@ js_string(isolate, "__webSceneFileSystemHandleState"), v8::Function::New(local_context, file_system_handle_state) .ToLocalChecked()).Check(); - constexpr std::string_view source = R"JS( + constexpr std::array source_parts{R"JS( (() => { const requestPanel = globalThis.__webSceneRequestFilePanel; const compareGrants = globalThis.__webSceneCompareFileGrants; @@ -1564,7 +1564,8 @@ }; const queueWriterCommand = (stream, command) => { if (!writerStates.has(stream)) { - return Promise.reject(new TypeError('Illegal invocation')); +)JS", + R"JS( return Promise.reject(new TypeError('Illegal invocation')); } let writer; try { writer = WritableStream.prototype.getWriter.call(stream); } @@ -1819,7 +1820,8 @@ value: keys }, values: { enumerable: true, configurable: true, writable: true, value: values }, - [Symbol.asyncIterator]: { configurable: true, writable: true, +)JS", + R"JS( [Symbol.asyncIterator]: { configurable: true, writable: true, value: entries }, getFileHandle: { enumerable: true, configurable: true, writable: true, value: getFileHandle }, @@ -2065,7 +2067,8 @@ return { description: String(type.description || ''), accept }; }); }; - const normalize = (kind, options) => { +)JS", + R"JS( const normalize = (kind, options) => { if (options == null) options = {}; if (typeof options !== 'object') throw new TypeError('Picker options must be an object'); const types = kind === 2 ? [] : normalizeTypes(options.types); @@ -2117,10 +2120,12 @@ writable: true, value: showSaveFilePicker } }); })(); - )JS"; + )JS"}; + static_assert(embedded_source_parts_are_portable(source_parts)); + auto source = join_embedded_source_parts(source_parts); auto script = v8::Script::Compile( local_context, - js_string(isolate, std::string(source).c_str())).ToLocalChecked(); + js_string(isolate, source.c_str())).ToLocalChecked(); script->Run(local_context).ToLocalChecked(); } static constexpr std::size_t maximum_file_system_handle_bindings = 16384U; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_web_api_compatibility.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_web_api_compatibility.inc index ecc2341fe..e8507bd82 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_web_api_compatibility.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_web_api_compatibility.inc @@ -55,8 +55,10 @@ local_context, "FileReader", fileReaderCompatibilityScript); + const auto css_compatibility_script = + join_embedded_source_parts(cssCompatibilityScriptParts); install_web_api_compatibility_script( local_context, "stylesheet CSSOM", - cssCompatibilityScript); + css_compatibility_script); } diff --git a/tests/RuntimeCompatibility/stylesheet-cssom.test.mjs b/tests/RuntimeCompatibility/stylesheet-cssom.test.mjs index d57080472..c9367c3df 100644 --- a/tests/RuntimeCompatibility/stylesheet-cssom.test.mjs +++ b/tests/RuntimeCompatibility/stylesheet-cssom.test.mjs @@ -5,8 +5,11 @@ import { performance as nodePerformance } from 'node:perf_hooks'; import { createContext, runInContext } from 'node:vm'; const header = readFileSync(new URL('../../experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h', import.meta.url), 'utf8'); -const script = header.match(/cssCompatibilityScript = R"JS\(([\s\S]*?)\)JS";/)?.[1]; -assert.ok(script, 'native CSS compatibility script must be embedded'); +const scriptDeclaration = header.match( + /cssCompatibilityScriptParts\{([\s\S]*?)\};\nstatic_assert/)?.[1]; +assert.ok(scriptDeclaration, 'native CSS compatibility script parts must be embedded'); +const script = Array.from(scriptDeclaration.matchAll(/R"JS\(([\s\S]*?)\)JS"/g), match => match[1]).join(''); +assert.ok(script, 'native CSS compatibility script must be reconstructable'); assert.equal(/<\/script/i.test(script), false, 'script must be safe to embed in HTML'); function setup() {