Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/validation/msvc-embedded-source-literals-690.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

#include <array>
#include <cstddef>
#include <limits>
#include <stdexcept>
#include <string>
#include <string_view>

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<std::size_t Count>
constexpr bool embedded_source_parts_are_portable(
const std::array<std::string_view, Count>& parts) noexcept
{
for (const auto part : parts) {
if (part.size() > maximum_portable_embedded_source_literal_bytes) {
return false;
}
}
return true;
}

template<std::size_t Count>
std::string join_embedded_source_parts(
const std::array<std::string_view, Count>& parts)
{
std::size_t source_size = 0U;
for (const auto part : parts) {
if (part.size() > std::numeric_limits<std::size_t>::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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#pragma once
#include <string_view>
#include "webscene_embedded_source.h"

namespace webscene_native {
// The pinned WebScene exposes a stable native HTMLStyleElement.sheet object,
Expand All @@ -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<std::string_view, 7> cssCompatibilityScriptParts{R"JS(
// SCENETECH_CSS_COMPATIBILITY_V1
(() => {
'use strict';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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++;
Expand Down Expand Up @@ -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] === "'") {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string_view, 3> message_channel_source_parts{R"JS(
(() => {
class MessagePort {
constructor() {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1945,7 +1945,8 @@
HierarchyRequestError: 3,
WrongDocumentError: 4,
InvalidCharacterError: 5,
NoModificationAllowedError: 7,
)JS",
R"JS( NoModificationAllowedError: 7,
NotFoundError: 8,
NotSupportedError: 9,
InUseAttributeError: 10,
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string_view, 4> source_parts{R"JS(
(() => {
const requestPanel = globalThis.__webSceneRequestFilePanel;
const compareGrants = globalThis.__webSceneCompareFileGrants;
Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
7 changes: 5 additions & 2 deletions tests/RuntimeCompatibility/stylesheet-cssom.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading