fix(worker): select module source format from response MIME

Key worker module maps by the requested JavaScript-or-Wasm or JSON type,
then select the compiler from HTTP and data URL response MIME. Preserve
Wasm bytes, decode text modules as UTF-8, and accept text/json consistently.

Reject source imports of compiled JavaScript without poisoning evaluation
or holding the graph borrow during promise rejection. Record the actual
Service Worker resource kind and cover suffixes, encodings, import
attributes, root reuse, and concurrent cache access.
This commit is contained in:
ldm0
2026-09-27 06:43:24 +08:00
parent 1d5862edf4
commit fb09450d8a
10 changed files with 620 additions and 221 deletions
+6 -1
View File
@@ -4385,11 +4385,16 @@ mod tests {
r#"{"ok":true}"#,
&expectation,
));
assert!(!json_path_equals(
assert!(json_path_equals(
&[("content-type".to_owned(), b"text/json".to_vec())],
r#"{"ok":true}"#,
&expectation,
));
assert!(!json_path_equals(
&[("content-type".to_owned(), b"text/plain".to_vec())],
r#"{"ok":true}"#,
&expectation,
));
let regex_expectation = SubresourceJsonPathRegex {
path: vec!["data".to_owned(), "url".to_owned()],
+7 -1
View File
@@ -18,7 +18,13 @@ pub(crate) fn ensure_worker_wasm_module_mime(
pub(crate) fn ensure_worker_json_module_mime(
response: &moli_fetch::Response,
) -> Result<(), String> {
let content_type = worker_module_response_content_type(&response.headers);
ensure_worker_json_module_mime_from_headers(&response.headers)
}
pub(super) fn ensure_worker_json_module_mime_from_headers(
headers: &[(String, String)],
) -> Result<(), String> {
let content_type = worker_module_response_content_type(headers);
let Some(content_type) = content_type else {
return Err(
"non-JSON module response for JSON import attribute: missing Content-Type".to_owned(),
+173 -162
View File
@@ -20,7 +20,7 @@ use tokio::sync::mpsc;
use url::Url;
use super::global_scope::worker_current_script_url;
use super::handle::{WorkerParentErrorEventKind, WorkerScriptResource};
use super::handle::{WorkerParentErrorEventKind, WorkerScriptResource, WorkerScriptResourceKind};
use crate::content_security_policy::ContentSecurityPolicyUrlViolation;
pub(super) type WorkerBootstrapError = (
@@ -279,6 +279,24 @@ impl WorkerModuleFetchedSource {
}
impl WorkerModuleSource {
pub(super) fn from_response_parts(
module_type: WorkerModuleType,
url: &Url,
headers: &[(String, String)],
bytes: Vec<u8>,
) -> Result<Self, String> {
let kind = module_type.response_kind(url, headers, &bytes)?;
Ok(Self::from_bytes(kind, bytes))
}
pub(super) fn from_bytes(kind: WorkerScriptResourceKind, bytes: Vec<u8>) -> Self {
if kind == WorkerScriptResourceKind::WebAssemblyModule {
Self::binary(bytes)
} else {
Self::text(moli_encoding::decode_utf8(&bytes))
}
}
pub(super) fn text(source: String) -> Self {
Self::Text(source)
}
@@ -1053,12 +1071,6 @@ fn advance_worker_dynamic_module_import(
let module_url = resolve_worker_module_specifier(&job.specifier, &job.base_url)?;
let module_key = worker_module_key_for_attributes(&module_url, &job.attributes)
.map_err(|message| format!("{message} for dynamic import `{}`", job.specifier))?;
if job.phase == ModuleImportPhase::Source && module_key.kind != WorkerModuleKind::WebAssembly {
return Err(WorkerDynamicModuleImportError::syntax_error(format!(
"source-phase dynamic import `{}` does not resolve to a WebAssembly module",
job.specifier
)));
}
if let Some(error) = graph.borrow().source_fetch_failure(&module_key) {
return Err(WorkerDynamicModuleImportError::type_error(format!(
"Failed to dynamically import module worker dependency `{module_url}`: {error}"
@@ -1082,7 +1094,7 @@ fn advance_worker_dynamic_module_import(
let inherited_referrer_policy = graph.borrow().referrer_policy_for_url(&job.base_url);
let root_entry = match existing_root_entry {
Some(entry) => entry,
None => match load_worker_static_module_dependency(module_url)? {
None => match load_worker_static_module_dependency(module_url, module_key.module_type)? {
WorkerModuleDependencyLoad::Source { url, source } => {
let key =
worker_module_key_for_attributes(&url, &job.attributes).map_err(|message| {
@@ -1389,23 +1401,28 @@ fn resolve_single_worker_dynamic_module_import(
match job.phase {
ModuleImportPhase::Evaluation => {
let module = v8::Local::new(scope, &record.module);
Some(module.get_module_namespace())
}
ModuleImportPhase::Source => {
let Some(wasm_record) = record.wasm_module.as_ref() else {
reject_worker_dynamic_module_import(
scope,
job,
WorkerDynamicModuleImportError::syntax_error(
"source-phase dynamic import did not resolve to a WebAssembly module",
),
);
return;
};
wasm_record
.source_module(scope)
.map(v8::Local::<v8::Value>::from)
Ok(Some(module.get_module_namespace()))
}
ModuleImportPhase::Source => record
.wasm_module
.as_ref()
.ok_or_else(|| {
WorkerDynamicModuleImportError::syntax_error(
"source-phase dynamic import did not resolve to a WebAssembly module",
)
})
.map(|wasm_record| {
wasm_record
.source_module(scope)
.map(v8::Local::<v8::Value>::from)
}),
}
};
let resolved_value = match resolved_value {
Ok(value) => value,
Err(error) => {
reject_worker_dynamic_module_import(scope, job, error);
return;
}
};
let Some(resolved_value) = resolved_value else {
@@ -1508,7 +1525,7 @@ impl WorkerModuleBootstrapJob {
}
fn advance(&mut self, scope: &mut v8::PinScope<'_, '_>) -> WorkerModuleAdvance {
let root_key = worker_module_root_key_for_source(&self.root_url, &self.source);
let root_key = WorkerModuleKey::javascript_or_wasm(self.root_url.clone());
let root_referrer_policy = self
.runtime
.graph
@@ -1591,7 +1608,7 @@ impl WorkerModuleBootstrapJob {
if let Err(error) = self.finish_fetch(scope, request, completion) {
return WorkerModuleAdvance::Failed(error);
}
let root_key = worker_module_root_key_for_source(&self.root_url, &self.source);
let root_key = WorkerModuleKey::javascript_or_wasm(self.root_url.clone());
let root_entry = match self.runtime.graph.borrow().entry_for_key(&root_key) {
Some(entry) => entry,
None => {
@@ -1623,13 +1640,6 @@ impl WorkerModuleBootstrapJob {
}
}
fn worker_module_root_key_for_source(url: &Url, source: &WorkerModuleSource) -> WorkerModuleKey {
match source {
WorkerModuleSource::Text(_) => WorkerModuleKey::java_script(url.clone()),
WorkerModuleSource::Binary(_) => WorkerModuleKey::webassembly(url.clone()),
}
}
enum WorkerModuleAdvance {
ReadyToInstantiate { root_entry: usize },
NeedFetches(WorkerModuleGraphFetchBatch),
@@ -1754,12 +1764,8 @@ impl WorkerModuleGraphFetchRequest {
self.csp_source
}
pub(super) fn module_type(&self) -> Option<&str> {
self.attributes.module_type()
}
pub(super) fn kind(&self) -> WorkerModuleKind {
self.key.kind
pub(super) fn module_type(&self) -> WorkerModuleType {
self.key.module_type
}
pub(super) fn credentials_mode(&self) -> RequestCredentialsMode {
@@ -1771,7 +1777,7 @@ impl WorkerModuleGraphFetchRequest {
}
pub(super) fn browser_request_metadata(&self) -> BrowserRequestMetadata {
worker_module_browser_request_metadata(self.key.kind, self.browser_request_metadata)
worker_module_browser_request_metadata(self.key.module_type, self.browser_request_metadata)
}
fn graph_browser_request_metadata(&self) -> BrowserRequestMetadata {
@@ -1780,12 +1786,12 @@ impl WorkerModuleGraphFetchRequest {
}
fn worker_module_browser_request_metadata(
kind: WorkerModuleKind,
module_type: WorkerModuleType,
graph_metadata: BrowserRequestMetadata,
) -> BrowserRequestMetadata {
match kind {
WorkerModuleKind::Json => BrowserRequestMetadata::JsonModule,
WorkerModuleKind::JavaScript | WorkerModuleKind::WebAssembly => graph_metadata,
match module_type {
WorkerModuleType::Json => BrowserRequestMetadata::JsonModule,
WorkerModuleType::JavaScriptOrWebAssembly => graph_metadata,
}
}
@@ -1812,45 +1818,64 @@ struct WorkerModuleRecord {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct WorkerModuleKey {
url: Url,
kind: WorkerModuleKind,
module_type: WorkerModuleType,
attributes: ModuleAttributesKey,
}
impl WorkerModuleKey {
fn java_script(url: Url) -> Self {
Self::java_script_with_attributes(url, ModuleAttributesKey::empty())
}
fn java_script_with_attributes(url: Url, attributes: ModuleAttributesKey) -> Self {
fn javascript_or_wasm(url: Url) -> Self {
Self {
url,
kind: WorkerModuleKind::JavaScript,
attributes,
module_type: WorkerModuleType::JavaScriptOrWebAssembly,
attributes: ModuleAttributesKey::empty(),
}
}
fn json(url: Url, attributes: ModuleAttributesKey) -> Self {
Self {
url,
kind: WorkerModuleKind::Json,
module_type: WorkerModuleType::Json,
attributes,
}
}
fn webassembly(url: Url) -> Self {
Self {
url,
kind: WorkerModuleKind::WebAssembly,
attributes: ModuleAttributesKey::empty(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) enum WorkerModuleKind {
JavaScript,
pub(super) enum WorkerModuleType {
// HTML keys the module map by the requested type. JavaScript and Wasm
// share this entry; the fetched MIME type selects the compiled record.
JavaScriptOrWebAssembly,
Json,
WebAssembly,
}
impl WorkerModuleType {
pub(super) fn response_kind(
self,
url: &Url,
headers: &[(String, String)],
bytes: &[u8],
) -> Result<WorkerScriptResourceKind, String> {
match self {
Self::Json => {
super::module_mime::ensure_worker_json_module_mime_from_headers(headers)?;
Ok(WorkerScriptResourceKind::JsonModule)
}
Self::JavaScriptOrWebAssembly => {
if super::worker_response_has_webassembly_mime(headers) {
return Ok(WorkerScriptResourceKind::WebAssemblyModule);
}
super::ensure_worker_script_mime_acceptable(url, headers, bytes).map_err(
|error| {
error.replace(
"unsupported script MIME type",
"unsupported module script MIME type",
)
},
)?;
Ok(WorkerScriptResourceKind::JavaScript)
}
}
}
}
#[derive(Clone)]
@@ -2051,16 +2076,15 @@ impl WorkerModuleGraph {
) -> Option<WorkerSyntheticModuleRecord> {
let entry = self.entry_for_module(module)?;
let record = &self.records[entry];
match record.key.kind {
WorkerModuleKind::Json => record
if let Some(wasm) = &record.wasm_module {
return Some(WorkerSyntheticModuleRecord::WebAssembly(wasm.clone()));
}
match record.key.module_type {
WorkerModuleType::Json => record
.source
.text_source()
.map(|source| WorkerSyntheticModuleRecord::Json(source.to_owned())),
WorkerModuleKind::WebAssembly => record
.wasm_module
.clone()
.map(WorkerSyntheticModuleRecord::WebAssembly),
WorkerModuleKind::JavaScript => None,
WorkerModuleType::JavaScriptOrWebAssembly => None,
}
}
@@ -2232,100 +2256,93 @@ fn resolve_worker_module_dependency(
request: &WorkerModuleRequest,
pending_keys: &mut HashSet<WorkerModuleKey>,
) -> WorkerModuleBootstrapResult<WorkerModuleGraphBuild> {
let dependency_url =
resolve_worker_module_specifier(&request.specifier, url).map_err(|message| {
let dependency_url = resolve_worker_module_specifier(&request.specifier, url).map_err(|error| {
Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&message,
WorkerParentErrorEventKind::Event,
))
})?;
if request.phase == ModuleImportPhase::Source {
let dependency_key = worker_module_key_for_attributes(&dependency_url, &request.attributes)
.map_err(|message| {
Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&format!("{message} for import `{}`", request.specifier),
WorkerParentErrorEventKind::Event,
))
})?;
if dependency_key.kind != WorkerModuleKind::WebAssembly {
return Err(Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&format!(
"source-phase import `{}` does not resolve to a WebAssembly module",
"Failed to resolve module worker dependency `{}`: {error}",
request.specifier
),
WorkerParentErrorEventKind::Event,
)));
}
}
let (dependency_key, dependency_source) =
match load_worker_static_module_dependency(dependency_url).map_err(|message| {
))
})?;
let dependency_key = worker_module_key_for_attributes(&dependency_url, &request.attributes)
.map_err(|message| {
Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&message,
&format!("{message} for import `{}`", request.specifier),
WorkerParentErrorEventKind::Event,
))
})? {
WorkerModuleDependencyLoad::Source { url, source } => {
let dependency_key = worker_module_key_for_attributes(&url, &request.attributes)
.map_err(|message| {
})?;
let (dependency_key, dependency_source) = match load_worker_static_module_dependency(
dependency_url,
dependency_key.module_type,
)
.map_err(|message| {
Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&message,
WorkerParentErrorEventKind::Event,
))
})? {
WorkerModuleDependencyLoad::Source { url, source } => {
let dependency_key = worker_module_key_for_attributes(&url, &request.attributes)
.map_err(|message| {
Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&format!("{message} for import `{}`", request.specifier),
WorkerParentErrorEventKind::Event,
))
})?;
(dependency_key, source)
}
WorkerModuleDependencyLoad::NeedFetch(dependency_url) => {
let dependency_key =
worker_module_key_for_attributes(&dependency_url, &request.attributes).map_err(
|message| {
Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&format!("{message} for import `{}`", request.specifier),
WorkerParentErrorEventKind::Event,
))
})?;
(dependency_key, source)
},
)?;
let existing_entry = graph.borrow().entry_for_key(&dependency_key);
if let Some(target_entry) = existing_entry {
graph.borrow_mut().add_dependency(
entry,
request.specifier.clone(),
request.attributes.clone(),
target_entry,
);
return Ok(WorkerModuleGraphBuild::Ready);
}
WorkerModuleDependencyLoad::NeedFetch(dependency_url) => {
let dependency_key =
worker_module_key_for_attributes(&dependency_url, &request.attributes)
.map_err(|message| {
Box::new(worker_bootstrap_error(
scope,
url.as_str(),
&format!("{message} for import `{}`", request.specifier),
WorkerParentErrorEventKind::Event,
))
})?;
let existing_entry = graph.borrow().entry_for_key(&dependency_key);
if let Some(target_entry) = existing_entry {
graph.borrow_mut().add_dependency(
entry,
request.specifier.clone(),
request.attributes.clone(),
target_entry,
);
return Ok(WorkerModuleGraphBuild::Ready);
}
if !pending_keys.insert(dependency_key.clone()) {
return Ok(WorkerModuleGraphBuild::Ready);
}
let fetch_id = reserve_worker_module_graph_fetch_id(scope);
let referrer_policy = graph.borrow().referrer_policy(entry).map(str::to_owned);
return Ok(WorkerModuleGraphBuild::NeedFetches(
WorkerModuleGraphFetchBatch::single(WorkerModuleGraphFetchRequest::new(
fetch_id,
dependency_key,
fetch_initiator_url.clone(),
csp_source,
Some(entry),
request.specifier.clone(),
request.attributes.clone(),
graph.borrow().credentials_mode(),
referrer_policy,
browser_request_metadata,
)),
));
if !pending_keys.insert(dependency_key.clone()) {
return Ok(WorkerModuleGraphBuild::Ready);
}
};
let fetch_id = reserve_worker_module_graph_fetch_id(scope);
let referrer_policy = graph.borrow().referrer_policy(entry).map(str::to_owned);
return Ok(WorkerModuleGraphBuild::NeedFetches(
WorkerModuleGraphFetchBatch::single(WorkerModuleGraphFetchRequest::new(
fetch_id,
dependency_key,
fetch_initiator_url.clone(),
csp_source,
Some(entry),
request.specifier.clone(),
request.attributes.clone(),
graph.borrow().credentials_mode(),
referrer_policy,
browser_request_metadata,
)),
));
}
};
let referrer_policy = graph.borrow().referrer_policy(entry).map(str::to_owned);
let target_entry = ensure_worker_module_entry(
scope,
@@ -2429,18 +2446,10 @@ fn compile_worker_module_record(
Vec<WorkerModuleRequest>,
Option<WasmModuleRecord>,
)> {
if matches!(key.kind, WorkerModuleKind::Json) {
if key.module_type == WorkerModuleType::Json {
return compile_worker_synthetic_module_record(scope, source_url);
}
if key.kind == WorkerModuleKind::WebAssembly {
let Some(bytes) = source.binary_source() else {
return Err(Box::new(worker_bootstrap_error(
scope,
source_url.as_str(),
"WebAssembly module worker source is not binary",
WorkerParentErrorEventKind::Event,
)));
};
if let Some(bytes) = source.binary_source() {
return compile_worker_wasm_module_record(scope, bytes, source_url);
}
let Some(source) = source.text_source() else {
@@ -2668,16 +2677,21 @@ fn resolve_worker_module_specifier(specifier: &str, base_url: &Url) -> Result<Ur
fn load_worker_static_module_dependency(
dependency_url: Url,
module_type: WorkerModuleType,
) -> Result<WorkerModuleDependencyLoad, String> {
match dependency_url.scheme() {
"data" => {
let source = super::decode_data_url_script_source(
let (bytes, mime) = moli_web_mime::data_url_body_and_mime_type(dependency_url.as_str())
.ok_or_else(|| format!("Failed to load module worker dependency: invalid data URL `{dependency_url}`"))?;
let source = WorkerModuleSource::from_response_parts(
module_type,
&dependency_url,
"Failed to load module worker dependency",
&[("content-type".to_owned(), mime)],
bytes,
)?;
Ok(WorkerModuleDependencyLoad::Source {
url: dependency_url,
source: WorkerModuleSource::text(source),
source,
})
}
"http" | "https" => Ok(WorkerModuleDependencyLoad::NeedFetch(dependency_url)),
@@ -2692,10 +2706,7 @@ fn worker_module_key_for_attributes(
attributes: &ModuleAttributesKey,
) -> Result<WorkerModuleKey, String> {
let Some(module_type) = attributes.module_type() else {
if url.path().to_ascii_lowercase().ends_with(".wasm") {
return Ok(WorkerModuleKey::webassembly(url.clone()));
}
return Ok(WorkerModuleKey::java_script(url.clone()));
return Ok(WorkerModuleKey::javascript_or_wasm(url.clone()));
};
match module_type {
"json" => Ok(WorkerModuleKey::json(url.clone(), attributes.clone())),
+14 -42
View File
@@ -98,8 +98,7 @@ use super::handle::{
WorkerBootstrapCompletion, WorkerBootstrapFailure, WorkerBootstrapSuccess,
WorkerDevToolsHandle, WorkerErrorPhase, WorkerErrorSource, WorkerFetchHandlerType,
WorkerHandle, WorkerMessage, WorkerNetworkPolicy, WorkerParentErrorEventKind,
WorkerRuntimeInspectorMessageBatch, WorkerScriptResource, WorkerScriptResourceKind,
WorkerToParentMessage,
WorkerRuntimeInspectorMessageBatch, WorkerScriptResource, WorkerToParentMessage,
};
use super::inspector_task_runner::{
WorkerInspectorTask, WorkerInspectorTaskMode, WorkerInspectorTaskRunner,
@@ -108,12 +107,11 @@ use super::module_runtime::{
WorkerBootstrapError, WorkerDynamicModuleImportAdvance, WorkerModuleBootstrapResume,
WorkerModuleBootstrapStart, WorkerModuleEvaluationCompletion, WorkerModuleFetchedSource,
WorkerModuleGraphFetchBatch, WorkerModuleGraphFetchCompletion, WorkerModuleGraphFetchCspSource,
WorkerModuleGraphFetchRequest, WorkerModuleKind, WorkerModulePendingBootstrap,
WorkerModuleSource, evaluate_module_worker_bootstrap_source,
install_classic_worker_dynamic_module_runtime, resume_worker_dynamic_module_evaluation,
resume_worker_dynamic_module_fetch, run_next_worker_dynamic_module_import,
worker_dynamic_module_import_waits_for_fetch, worker_has_pending_dynamic_module_imports,
worker_has_runnable_dynamic_module_imports,
WorkerModuleGraphFetchRequest, WorkerModulePendingBootstrap, WorkerModuleSource,
evaluate_module_worker_bootstrap_source, install_classic_worker_dynamic_module_runtime,
resume_worker_dynamic_module_evaluation, resume_worker_dynamic_module_fetch,
run_next_worker_dynamic_module_import, worker_dynamic_module_import_waits_for_fetch,
worker_has_pending_dynamic_module_imports, worker_has_runnable_dynamic_module_imports,
};
pub(super) type WorkerExceptionError = Box<(V8ExceptionReport, Option<v8::Global<v8::Value>>)>;
@@ -698,8 +696,7 @@ fn start_worker_module_graph_fetch(
.with_credentials_mode(request.credentials_mode());
let requested_url = request.url().clone();
let request_initiator_url = request.initiator_url().clone();
let requested_module_type = request.module_type().map(str::to_owned);
let requested_kind = request.kind();
let requested_module_type = request.module_type();
let request_credentials_mode = request.credentials_mode();
let response_policy = policy.clone();
let response_resource_kind = resource_kind;
@@ -753,24 +750,11 @@ fn start_worker_module_graph_fetch(
request_credentials_mode,
Default::default(),
)?;
if requested_kind == WorkerModuleKind::WebAssembly {
crate::worker::ensure_worker_wasm_module_mime(&response)?;
} else {
match requested_module_type.as_deref() {
Some("json") => crate::worker::ensure_worker_json_module_mime(&response)?,
_ => crate::worker::ensure_worker_script_mime_acceptable(
&response.final_url,
&response.headers,
response.body_bytes(),
)
.map_err(|error| {
error.replace(
"unsupported script MIME type",
"unsupported module script MIME type",
)
})?,
}
}
let response_kind = requested_module_type.response_kind(
&response.final_url,
&response.headers,
response.body_bytes(),
)?;
let response_time_ms = response_started_at
.elapsed()
.as_millis()
@@ -784,13 +768,9 @@ fn start_worker_module_graph_fetch(
&body_bytes,
response_time_ms,
)
.with_kind(worker_script_resource_kind_for_module(requested_kind));
.with_kind(response_kind);
let final_url = head.final_url.clone();
let source = if requested_kind == WorkerModuleKind::WebAssembly {
WorkerModuleSource::binary(body_bytes)
} else {
WorkerModuleSource::text(moli_encoding::decode_utf8(&body_bytes))
};
let source = WorkerModuleSource::from_bytes(response_kind, body_bytes);
Ok(WorkerModuleFetchedSource::new(final_url, source)
.with_resource(resource)
.with_response_referrer_policy(response_referrer_policy))
@@ -861,14 +841,6 @@ fn start_worker_module_graph_fetch_batch(
}
}
fn worker_script_resource_kind_for_module(kind: WorkerModuleKind) -> WorkerScriptResourceKind {
match kind {
WorkerModuleKind::JavaScript => WorkerScriptResourceKind::JavaScript,
WorkerModuleKind::Json => WorkerScriptResourceKind::JsonModule,
WorkerModuleKind::WebAssembly => WorkerScriptResourceKind::WebAssemblyModule,
}
}
fn drain_worker_file_reader_queue(
isolate: &mut v8::OwnedIsolate,
context: &v8::Global<v8::Context>,
@@ -5,6 +5,7 @@ use crate::worker::handle::WorkerParentErrorEventKind;
use crate::worker::{WorkerErrorPhase, WorkerScriptResourceKind};
use moli_crypto::sha256_hex;
mod response_mime;
mod source_phase;
const WORKER_WASM_IMPORT_PM: &[u8] = &[
@@ -0,0 +1,375 @@
use super::source_phase::ModuleSourceServer;
use super::*;
// Exports answer() -> 42 and a custom section "x" containing the byte 0xff.
const WASM_WITH_NON_UTF8_SECTION: &[u8] = &[
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03,
0x02, 0x01, 0x00, 0x07, 0x0a, 0x01, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x00, 0x00, 0x0a,
0x06, 0x01, 0x04, 0x00, 0x41, 0x2a, 0x0b, 0x00, 0x03, 0x01, 0x78, 0xff,
];
fn module_data_url(mime: &str, bytes: &[u8]) -> String {
let body = bytes
.iter()
.map(|byte| format!("%{byte:02X}"))
.collect::<String>();
format!("data:{mime},{body}")
}
#[tokio::test]
async fn worker_wasm_imports_use_response_mime_independently_of_url_suffix() {
ensure_v8();
for name in ["module.wasm", "module.js", "module"] {
for phase in ["static-source", "dynamic-source", "evaluation"] {
let mut server = ModuleSourceServer::start().await;
let url = serde_json::to_string(&format!("./{name}")).unwrap();
let source = match phase {
"static-source" => format!("import source wasm from {url};"),
"dynamic-source" => format!("const wasm = await import.source({url});"),
_ => format!(
"const namespace = await import({url}); postMessage(namespace.answer());"
),
};
let source = if phase == "evaluation" {
source
} else {
format!(
r#"{source}
postMessage([new WebAssembly.Instance(wasm).exports.answer(),
new Uint8Array(WebAssembly.Module.customSections(wasm, 'x')[0])[0]]);
"#
)
};
let mut handle = server.worker(source, WorkerScriptKind::Module);
server
.respond_bytes(
&format!("/worker/{name}"),
"200 OK",
"Application/Wasm; charset=UTF-8",
WASM_WITH_NON_UTF8_SECTION,
)
.await;
let expected = if phase == "evaluation" {
"42"
} else {
"[42,255]"
};
assert_eq!(
recv_post_json(&mut handle).await,
expected,
"{name}: {phase}"
);
handle.terminate_and_join();
server.assert_no_more_requests();
}
}
}
#[tokio::test]
async fn worker_javascript_at_wasm_url_is_not_poisoned_by_source_import() {
ensure_v8();
for source_first in [true, false] {
let mut server = ModuleSourceServer::start().await;
let imports = if source_first {
"[loadSource(), loadEvaluation()]"
} else {
"[loadEvaluation(), loadSource()]"
};
let mut handle = server.worker(format!(r#"
const loadSource = () => import.source('./module.wasm').then(() => 'unexpected', e => e.name);
const loadEvaluation = () => import('./module.wasm').then(m => m.answer);
const results = await Promise.all({imports});
postMessage([results, await loadSource(), self.runs]);
"#), WorkerScriptKind::Module);
server
.respond(
"/worker/module.wasm",
"200 OK",
"text/javascript",
"self.runs = (self.runs || 0) + 1; export const answer = 42;",
)
.await;
let expected = if source_first {
r#"[["SyntaxError",42],"SyntaxError",1]"#
} else {
r#"[[42,"SyntaxError"],"SyntaxError",1]"#
};
assert_eq!(recv_post_json(&mut handle).await, expected);
handle.terminate_and_join();
server.assert_no_more_requests();
}
}
#[tokio::test]
async fn worker_static_source_import_rejects_javascript_without_evaluating_it() {
ensure_v8();
let mut server = ModuleSourceServer::start().await;
let mut handle = server.worker(
"import source source from './module.wasm'; postMessage('unexpected root');".into(),
WorkerScriptKind::Module,
);
server
.respond(
"/worker/module.wasm",
"200 OK",
"text/javascript",
"postMessage('unexpected dependency');",
)
.await;
match timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap() {
WorkerToParentMessage::Error { message, .. } => {
assert!(message.contains("WebAssembly"), "{message}")
}
other => panic!("expected source-phase rejection before execution, got {other:?}"),
}
handle.terminate_and_join();
server.assert_no_more_requests();
}
#[tokio::test]
async fn worker_data_wasm_imports_preserve_bytes_and_share_source_with_evaluation() {
ensure_v8();
let data_url = module_data_url("application/wasm", WASM_WITH_NON_UTF8_SECTION);
let data_url = serde_json::to_string(&data_url).unwrap();
let mut handle = spawn_test_worker_with_options(
WorkerSpawnOptions::new(
format!(
r#"
import source wasm from {data_url};
const again = await import.source({data_url});
const namespace = await import({data_url});
postMessage([wasm === again, namespace.answer(),
new Uint8Array(WebAssembly.Module.customSections(wasm, 'x')[0])[0]]);
"#
),
"https://example.test/worker/main.js".into(),
)
.with_script_kind(WorkerScriptKind::Module),
);
assert_eq!(recv_post_json(&mut handle).await, "[true,42,255]");
handle.terminate_and_join();
}
#[tokio::test]
async fn worker_data_javascript_and_json_modules_use_utf8_decoding() {
ensure_v8();
let js = module_data_url(
"text/javascript;charset=windows-1252",
b"\xef\xbb\xbfexport const text = 'a\xffb';",
);
let json = module_data_url(
"application/json;charset=windows-1252",
b"\xef\xbb\xbf{\"text\":\"a\xffb\"}",
);
let mut handle = spawn_test_worker_with_options(
WorkerSpawnOptions::new(
format!(
r#"
const js = await import({});
const json = await import({}, {{with: {{type: 'json'}}}});
postMessage([js.text, json.default.text]);
"#,
serde_json::to_string(&js).unwrap(),
serde_json::to_string(&json).unwrap()
),
"https://example.test/worker/main.js".into(),
)
.with_script_kind(WorkerScriptKind::Module),
);
assert_eq!(recv_post_json(&mut handle).await, r#"["a�b","a�b"]"#);
handle.terminate_and_join();
}
#[tokio::test]
async fn worker_data_modules_enforce_mime_and_json_import_attributes() {
ensure_v8();
for (mime, json_attribute, body, expected) in [
("text/javascript", false, "export const answer = 42;", "42"),
(
"text/plain",
false,
"export const answer = 42;",
r#""TypeError""#,
),
("", false, "export const answer = 42;", r#""TypeError""#),
("application/json", true, r#"{"answer":42}"#, "42"),
("Text/JSON; charset=UTF-8", true, r#"{"answer":42}"#, "42"),
("application/manifest+json", true, r#"{"answer":42}"#, "42"),
("text/plain", true, r#"{"answer":42}"#, r#""TypeError""#),
(
"text/javascript",
true,
r#"{"answer":42}"#,
r#""TypeError""#,
),
(
"application/json",
false,
r#"{"answer":42}"#,
r#""TypeError""#,
),
] {
let url = serde_json::to_string(&module_data_url(mime, body.as_bytes())).unwrap();
let options = if json_attribute {
", {with: {type: 'json'}}"
} else {
""
};
let result = if json_attribute {
"m.default.answer"
} else {
"m.answer"
};
let mut handle = spawn_test_worker_with_options(
WorkerSpawnOptions::new(format!("import({url}{options}).then(m => postMessage({result}), e => postMessage(e.name));"),
"https://example.test/worker/main.js".into()).with_script_kind(WorkerScriptKind::Module),
);
assert_eq!(
recv_post_json(&mut handle).await,
expected,
"{mime}, JSON: {json_attribute}"
);
handle.terminate_and_join();
}
}
#[tokio::test]
async fn worker_json_imports_accept_text_json_response_mime() {
ensure_v8();
for source in [
"import data from './module.json' with {type: 'json'}; postMessage(data.answer);",
"const {default: data} = await import('./module.json', {with: {type: 'json'}}); postMessage(data.answer);",
] {
let mut server = ModuleSourceServer::start().await;
let mut handle = server.worker(source.into(), WorkerScriptKind::Module);
server
.respond(
"/worker/module.json",
"200 OK",
"Text/JSON; charset=UTF-8",
r#"{"answer":42}"#,
)
.await;
assert_eq!(recv_post_json(&mut handle).await, "42");
handle.terminate_and_join();
server.assert_no_more_requests();
}
}
#[tokio::test]
async fn worker_module_map_separates_json_from_javascript_or_wasm() {
ensure_v8();
for wasm in [true, false] {
let mut server = ModuleSourceServer::start().await;
let (wrong, right, mime, body) = if wasm {
(
"import('./module', {with: {type: 'json'}})",
"import.source('./module').then(m => new WebAssembly.Instance(m).exports.answer())",
"application/wasm",
WASM_WITH_NON_UTF8_SECTION,
)
} else {
(
"import('./module')",
"import('./module', {with: {type: 'json'}}).then(m => m.default.answer)",
"application/json",
br#"{"answer":42}"#.as_slice(),
)
};
let mut handle = server.worker(
format!(
r#"
const error = await {wrong}.then(() => 'unexpected', e => e.name);
const first = await {right};
const again = await {right};
postMessage([error, first, again]);
"#
),
WorkerScriptKind::Module,
);
for _ in 0..2 {
server
.respond_bytes("/worker/module", "200 OK", mime, body)
.await;
}
assert_eq!(recv_post_json(&mut handle).await, r#"["TypeError",42,42]"#);
handle.terminate_and_join();
server.assert_no_more_requests();
}
}
#[tokio::test]
async fn worker_javascript_root_at_wasm_url_reuses_the_module_map_entry() {
ensure_v8();
let mut server = ModuleSourceServer::start().await;
let mut handle = server.worker_at(r#"
export const answer = 42;
onmessage = () => import(import.meta.url).then(m => postMessage(m.answer), e => postMessage(e.name));
postMessage('ready');
"#.into(), WorkerScriptKind::Module, "main.wasm");
assert_eq!(recv_post_json(&mut handle).await, r#""ready""#);
handle.post_message(serialize_test_string("import root"));
assert_eq!(recv_post_json(&mut handle).await, "42");
handle.terminate_and_join();
server.assert_no_more_requests();
}
#[tokio::test]
async fn worker_wasm_root_without_wasm_suffix_can_be_imported_as_source() {
ensure_v8();
let (base_url, server) = spawn_path_response_http_server(vec![(
"/worker/worker-helper.js", "HTTP/1.1 200 OK", "text/javascript",
r#"export function pm(value) {
import.source('./main.bin').then(source => postMessage([value, source instanceof WebAssembly.Module]),
error => postMessage({error: error.name}));
}"#.into(), Duration::ZERO,
)]).await;
let mut handle = spawn_worker_with_source_and_kind_and_network_policy(
WorkerScriptSource::binary(WORKER_WASM_IMPORT_PM.to_vec()),
format!("{base_url}/worker/main.bin"),
worker_test_request_client(),
WorkerScriptKind::Module,
WorkerNetworkPolicy::default(),
);
assert_eq!(recv_post_json(&mut handle).await, "[42,true]");
handle.terminate_and_join();
server.await.unwrap();
}
#[tokio::test]
async fn service_worker_source_import_records_the_actual_wasm_resource_kind() {
ensure_v8();
let mut server = ModuleSourceServer::start().await;
let mut handle = spawn_test_worker_with_options(service_worker_module_options(
"import source wasm from './module.js'; if (!(wasm instanceof WebAssembly.Module)) throw new Error('not wasm'); skipWaiting();".into(),
format!("{}/worker/sw.js", server.url),
));
server
.respond_bytes(
"/worker/module.js",
"200 OK",
"application/wasm",
WASM_WITH_NON_UTF8_SECTION,
)
.await;
let mut resource = None;
let mut ready = false;
while resource.is_none() || !ready {
match timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap() {
WorkerToParentMessage::ServiceWorkerImportedScriptLoaded {
resource: loaded, ..
} => resource = Some(loaded),
WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } => ready = true,
WorkerToParentMessage::Error { message, .. } => {
panic!("unexpected bootstrap error: {message}")
}
_ => {}
}
}
let resource = resource.unwrap();
assert_eq!(resource.kind, WorkerScriptResourceKind::WebAssemblyModule);
assert_eq!(resource.body_len, WASM_WITH_NON_UTF8_SECTION.len());
assert_eq!(resource.body_sha256, sha256_hex(WASM_WITH_NON_UTF8_SECTION));
handle.terminate_and_join();
server.assert_no_more_requests();
}
@@ -3,14 +3,14 @@ use tokio::net::TcpStream;
// Keep responses under the test's control: a source import must settle even
// while the evaluation graph's dependency response is withheld.
struct ModuleSourceServer {
url: String,
pub(super) struct ModuleSourceServer {
pub(super) url: String,
requests: tokio::sync::mpsc::UnboundedReceiver<(String, TcpStream)>,
task: JoinHandle<()>,
}
impl ModuleSourceServer {
async fn start() -> Self {
pub(super) async fn start() -> Self {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let (sender, requests) = tokio::sync::mpsc::unbounded_channel();
@@ -37,16 +37,25 @@ impl ModuleSourceServer {
}
}
fn worker(&self, source: String, kind: WorkerScriptKind) -> WorkerTestHandle {
pub(super) fn worker(&self, source: String, kind: WorkerScriptKind) -> WorkerTestHandle {
self.worker_at(source, kind, "main.js")
}
pub(super) fn worker_at(
&self,
source: String,
kind: WorkerScriptKind,
name: &str,
) -> WorkerTestHandle {
spawn_worker_with_request_client_and_kind(
source,
format!("{}/worker/main.js", self.url),
format!("{}/worker/{name}", self.url),
worker_test_request_client(),
kind,
)
}
async fn request(&mut self, path: &str) -> TcpStream {
pub(super) async fn request(&mut self, path: &str) -> TcpStream {
let (actual, stream) = timeout(TIMEOUT, self.requests.recv())
.await
.unwrap()
@@ -55,16 +64,31 @@ impl ModuleSourceServer {
stream
}
async fn respond(&mut self, path: &str, status: &str, mime: &str, body: &str) {
pub(super) async fn respond(&mut self, path: &str, status: &str, mime: &str, body: &str) {
Self::write(self.request(path).await, status, mime, body).await;
}
async fn write(mut stream: TcpStream, status: &str, mime: &str, body: &str) {
let response = format!(
"HTTP/1.1 {status}\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
pub(super) async fn respond_bytes(
&mut self,
path: &str,
status: &str,
mime: &str,
body: &[u8],
) {
Self::write_bytes(self.request(path).await, status, mime, body).await;
}
async fn write(stream: TcpStream, status: &str, mime: &str, body: &str) {
Self::write_bytes(stream, status, mime, body.as_bytes()).await;
}
async fn write_bytes(mut stream: TcpStream, status: &str, mime: &str, body: &[u8]) {
let head = format!(
"HTTP/1.1 {status}\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(head.as_bytes()).await.unwrap();
stream.write_all(body).await.unwrap();
}
async fn wasm(&mut self) {
@@ -77,7 +101,7 @@ impl ModuleSourceServer {
.await;
}
fn assert_no_more_requests(&mut self) {
pub(super) fn assert_no_more_requests(&mut self) {
assert!(self.requests.try_recv().is_err(), "unexpected module fetch");
}
}
+3 -1
View File
@@ -132,7 +132,9 @@ pub fn multipart_form_data_boundary(input: &str) -> Option<String> {
}
pub fn is_json_module_mime(input: &str) -> bool {
mime_essence(input).is_some_and(|mime| mime == "application/json" || mime.ends_with("+json"))
mime_essence(input).is_some_and(|mime| {
matches!(mime.as_str(), "application/json" | "text/json") || mime.ends_with("+json")
})
}
pub fn is_webassembly_mime(input: &str) -> bool {
+4 -1
View File
@@ -267,7 +267,10 @@ fn matches_script_and_form_content_types() {
assert!(is_json_module_mime("Application/JSON; charset=utf-8"));
assert!(is_json_module_mime("application/manifest+json"));
assert!(!is_json_module_mime("text/json"));
assert!(is_json_module_mime("text/json"));
assert!(is_json_module_mime("Text/JSON; charset=utf-8"));
assert!(!is_json_module_mime("text/json+blah"));
assert!(!is_json_module_mime("image/json"));
assert!(is_media_source_type_supported(
"video/mp4; codecs=\"avc1.42E01E\""
@@ -76,7 +76,7 @@ promise_test(async function () {
"import value from " + JSON.stringify(jsUrl) + " with { type: 'json' };",
"postMessage(value);",
].join("\n"),
["syntax"],
["non-JSON module response", "JSON import attribute", "text/javascript"],
"static non-JSON import with JSON type",
);