mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 15:49:44 -05:00
1563088f06
In the spec, a URL record has an associated "blob URL entry", which for `blob:` URLs is populated during parsing to contain a reference to the `Blob` object that backs that object URL. It is this blob URL entry that the `fetch` API uses to resolve an object URL. Therefore, since the `Request` constructor parses URL inputs, it will have an associated blob URL entry which will be used when fetching, even if the object URL has been revoked since the construction of the `Request` object. (The `Request` constructor takes the URL as a string and parses it, so the object URL must be live at the time it is called.) This PR adds a new `blobFromObjectUrl` JS function (backed by a new `op_blob_from_object_url` op) that, if the URL is a valid object URL, returns a new `Blob` object whose parts are references to the same Rust `BlobPart`s used by the original `Blob` object. It uses this function to add a new `blobUrlEntry` field to inner requests, which will be `null` or such a `Blob`, and then uses `Blob.prototype.stream()` as the response's body. As a result of this, the `blob:` URL resolution from `op_fetch` is now useless, and has been removed.
388 lines
10 KiB
Rust
388 lines
10 KiB
Rust
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
|
|
|
mod blob;
|
|
mod message_port;
|
|
|
|
use deno_core::error::range_error;
|
|
use deno_core::error::type_error;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::include_js_files;
|
|
use deno_core::op_async;
|
|
use deno_core::op_sync;
|
|
use deno_core::url::Url;
|
|
use deno_core::Extension;
|
|
use deno_core::OpState;
|
|
use deno_core::Resource;
|
|
use deno_core::ResourceId;
|
|
use deno_core::ZeroCopyBuf;
|
|
use encoding_rs::CoderResult;
|
|
use encoding_rs::Decoder;
|
|
use encoding_rs::DecoderResult;
|
|
use encoding_rs::Encoding;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::borrow::Cow;
|
|
use std::cell::RefCell;
|
|
use std::fmt;
|
|
use std::path::PathBuf;
|
|
use std::usize;
|
|
|
|
use crate::blob::op_blob_create_object_url;
|
|
use crate::blob::op_blob_create_part;
|
|
use crate::blob::op_blob_from_object_url;
|
|
use crate::blob::op_blob_read_part;
|
|
use crate::blob::op_blob_remove_part;
|
|
use crate::blob::op_blob_revoke_object_url;
|
|
use crate::blob::op_blob_slice_part;
|
|
pub use crate::blob::Blob;
|
|
pub use crate::blob::BlobPart;
|
|
pub use crate::blob::BlobStore;
|
|
pub use crate::blob::InMemoryBlobPart;
|
|
|
|
pub use crate::message_port::create_entangled_message_port;
|
|
use crate::message_port::op_message_port_create_entangled;
|
|
use crate::message_port::op_message_port_post_message;
|
|
use crate::message_port::op_message_port_recv_message;
|
|
pub use crate::message_port::JsMessageData;
|
|
pub use crate::message_port::MessagePort;
|
|
|
|
/// Load and execute the javascript code.
|
|
pub fn init(blob_store: BlobStore, maybe_location: Option<Url>) -> Extension {
|
|
Extension::builder()
|
|
.js(include_js_files!(
|
|
prefix "deno:ext/web",
|
|
"00_infra.js",
|
|
"01_dom_exception.js",
|
|
"01_mimesniff.js",
|
|
"02_event.js",
|
|
"02_structured_clone.js",
|
|
"03_abort_signal.js",
|
|
"04_global_interfaces.js",
|
|
"05_base64.js",
|
|
"06_streams.js",
|
|
"08_text_encoding.js",
|
|
"09_file.js",
|
|
"10_filereader.js",
|
|
"11_blob_url.js",
|
|
"12_location.js",
|
|
"13_message_port.js",
|
|
))
|
|
.ops(vec![
|
|
("op_base64_decode", op_sync(op_base64_decode)),
|
|
("op_base64_encode", op_sync(op_base64_encode)),
|
|
(
|
|
"op_encoding_normalize_label",
|
|
op_sync(op_encoding_normalize_label),
|
|
),
|
|
("op_encoding_new_decoder", op_sync(op_encoding_new_decoder)),
|
|
("op_encoding_decode", op_sync(op_encoding_decode)),
|
|
("op_encoding_encode_into", op_sync(op_encoding_encode_into)),
|
|
("op_blob_create_part", op_sync(op_blob_create_part)),
|
|
("op_blob_slice_part", op_sync(op_blob_slice_part)),
|
|
("op_blob_read_part", op_async(op_blob_read_part)),
|
|
("op_blob_remove_part", op_sync(op_blob_remove_part)),
|
|
(
|
|
"op_blob_create_object_url",
|
|
op_sync(op_blob_create_object_url),
|
|
),
|
|
(
|
|
"op_blob_revoke_object_url",
|
|
op_sync(op_blob_revoke_object_url),
|
|
),
|
|
("op_blob_from_object_url", op_sync(op_blob_from_object_url)),
|
|
(
|
|
"op_message_port_create_entangled",
|
|
op_sync(op_message_port_create_entangled),
|
|
),
|
|
(
|
|
"op_message_port_post_message",
|
|
op_sync(op_message_port_post_message),
|
|
),
|
|
(
|
|
"op_message_port_recv_message",
|
|
op_async(op_message_port_recv_message),
|
|
),
|
|
])
|
|
.state(move |state| {
|
|
state.put(blob_store.clone());
|
|
if let Some(location) = maybe_location.clone() {
|
|
state.put(Location(location));
|
|
}
|
|
Ok(())
|
|
})
|
|
.build()
|
|
}
|
|
|
|
fn op_base64_decode(
|
|
_state: &mut OpState,
|
|
input: String,
|
|
_: (),
|
|
) -> Result<ZeroCopyBuf, AnyError> {
|
|
let mut input: &str = &input.replace(|c| char::is_ascii_whitespace(&c), "");
|
|
// "If the length of input divides by 4 leaving no remainder, then:
|
|
// if input ends with one or two U+003D EQUALS SIGN (=) characters,
|
|
// remove them from input."
|
|
if input.len() % 4 == 0 {
|
|
if input.ends_with("==") {
|
|
input = &input[..input.len() - 2]
|
|
} else if input.ends_with('=') {
|
|
input = &input[..input.len() - 1]
|
|
}
|
|
}
|
|
|
|
// "If the length of input divides by 4 leaving a remainder of 1,
|
|
// throw an InvalidCharacterError exception and abort these steps."
|
|
if input.len() % 4 == 1 {
|
|
return Err(
|
|
DomExceptionInvalidCharacterError::new("Failed to decode base64.").into(),
|
|
);
|
|
}
|
|
|
|
if input
|
|
.chars()
|
|
.any(|c| c != '+' && c != '/' && !c.is_alphanumeric())
|
|
{
|
|
return Err(
|
|
DomExceptionInvalidCharacterError::new(
|
|
"Failed to decode base64: invalid character",
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
let cfg = base64::Config::new(base64::CharacterSet::Standard, true)
|
|
.decode_allow_trailing_bits(true);
|
|
let out = base64::decode_config(&input, cfg).map_err(|err| {
|
|
DomExceptionInvalidCharacterError::new(&format!(
|
|
"Failed to decode base64: {:?}",
|
|
err
|
|
))
|
|
})?;
|
|
Ok(ZeroCopyBuf::from(out))
|
|
}
|
|
|
|
fn op_base64_encode(
|
|
_state: &mut OpState,
|
|
s: ZeroCopyBuf,
|
|
_: (),
|
|
) -> Result<String, AnyError> {
|
|
let cfg = base64::Config::new(base64::CharacterSet::Standard, true)
|
|
.decode_allow_trailing_bits(true);
|
|
let out = base64::encode_config(&s, cfg);
|
|
Ok(out)
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DecoderOptions {
|
|
label: String,
|
|
ignore_bom: bool,
|
|
fatal: bool,
|
|
}
|
|
|
|
fn op_encoding_normalize_label(
|
|
_state: &mut OpState,
|
|
label: String,
|
|
_: (),
|
|
) -> Result<String, AnyError> {
|
|
let encoding = Encoding::for_label_no_replacement(label.as_bytes())
|
|
.ok_or_else(|| {
|
|
range_error(format!(
|
|
"The encoding label provided ('{}') is invalid.",
|
|
label
|
|
))
|
|
})?;
|
|
Ok(encoding.name().to_lowercase())
|
|
}
|
|
|
|
fn op_encoding_new_decoder(
|
|
state: &mut OpState,
|
|
options: DecoderOptions,
|
|
_: (),
|
|
) -> Result<ResourceId, AnyError> {
|
|
let DecoderOptions {
|
|
label,
|
|
fatal,
|
|
ignore_bom,
|
|
} = options;
|
|
|
|
let encoding = Encoding::for_label(label.as_bytes()).ok_or_else(|| {
|
|
range_error(format!(
|
|
"The encoding label provided ('{}') is invalid.",
|
|
label
|
|
))
|
|
})?;
|
|
|
|
let decoder = if ignore_bom {
|
|
encoding.new_decoder_without_bom_handling()
|
|
} else {
|
|
encoding.new_decoder_with_bom_removal()
|
|
};
|
|
|
|
let rid = state.resource_table.add(TextDecoderResource {
|
|
decoder: RefCell::new(decoder),
|
|
fatal,
|
|
});
|
|
|
|
Ok(rid)
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DecodeOptions {
|
|
rid: ResourceId,
|
|
stream: bool,
|
|
}
|
|
|
|
fn op_encoding_decode(
|
|
state: &mut OpState,
|
|
data: ZeroCopyBuf,
|
|
options: DecodeOptions,
|
|
) -> Result<String, AnyError> {
|
|
let DecodeOptions { rid, stream } = options;
|
|
|
|
let resource = state.resource_table.get::<TextDecoderResource>(rid)?;
|
|
|
|
let mut decoder = resource.decoder.borrow_mut();
|
|
let fatal = resource.fatal;
|
|
|
|
let max_buffer_length = if fatal {
|
|
decoder
|
|
.max_utf8_buffer_length_without_replacement(data.len())
|
|
.ok_or_else(|| range_error("Value too large to decode."))?
|
|
} else {
|
|
decoder
|
|
.max_utf8_buffer_length(data.len())
|
|
.ok_or_else(|| range_error("Value too large to decode."))?
|
|
};
|
|
|
|
let mut output = String::with_capacity(max_buffer_length);
|
|
|
|
if fatal {
|
|
let (result, _) =
|
|
decoder.decode_to_string_without_replacement(&data, &mut output, !stream);
|
|
match result {
|
|
DecoderResult::InputEmpty => Ok(output),
|
|
DecoderResult::OutputFull => {
|
|
Err(range_error("Provided buffer too small."))
|
|
}
|
|
DecoderResult::Malformed(_, _) => {
|
|
Err(type_error("The encoded data is not valid."))
|
|
}
|
|
}
|
|
} else {
|
|
let (result, _, _) = decoder.decode_to_string(&data, &mut output, !stream);
|
|
match result {
|
|
CoderResult::InputEmpty => Ok(output),
|
|
CoderResult::OutputFull => Err(range_error("Provided buffer too small.")),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TextDecoderResource {
|
|
decoder: RefCell<Decoder>,
|
|
fatal: bool,
|
|
}
|
|
|
|
impl Resource for TextDecoderResource {
|
|
fn name(&self) -> Cow<str> {
|
|
"textDecoder".into()
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct EncodeIntoResult {
|
|
read: usize,
|
|
written: usize,
|
|
}
|
|
|
|
fn op_encoding_encode_into(
|
|
_state: &mut OpState,
|
|
input: String,
|
|
mut buffer: ZeroCopyBuf,
|
|
) -> Result<EncodeIntoResult, AnyError> {
|
|
// Since `input` is already UTF-8, we can simply find the last UTF-8 code
|
|
// point boundary from input that fits in `buffer`, and copy the bytes up to
|
|
// that point.
|
|
let boundary = if buffer.len() >= input.len() {
|
|
input.len()
|
|
} else {
|
|
let mut boundary = buffer.len();
|
|
|
|
// The maximum length of a UTF-8 code point is 4 bytes.
|
|
for _ in 0..4 {
|
|
if input.is_char_boundary(boundary) {
|
|
break;
|
|
}
|
|
debug_assert!(boundary > 0);
|
|
boundary -= 1;
|
|
}
|
|
|
|
debug_assert!(input.is_char_boundary(boundary));
|
|
boundary
|
|
};
|
|
|
|
buffer[..boundary].copy_from_slice(input[..boundary].as_bytes());
|
|
|
|
Ok(EncodeIntoResult {
|
|
// The `read` output parameter is measured in UTF-16 code units.
|
|
read: input[..boundary].encode_utf16().count(),
|
|
written: boundary,
|
|
})
|
|
}
|
|
|
|
pub fn get_declaration() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_web.d.ts")
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct DomExceptionQuotaExceededError {
|
|
pub msg: String,
|
|
}
|
|
|
|
impl DomExceptionQuotaExceededError {
|
|
pub fn new(msg: &str) -> Self {
|
|
DomExceptionQuotaExceededError {
|
|
msg: msg.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct DomExceptionInvalidCharacterError {
|
|
pub msg: String,
|
|
}
|
|
|
|
impl DomExceptionInvalidCharacterError {
|
|
pub fn new(msg: &str) -> Self {
|
|
DomExceptionInvalidCharacterError {
|
|
msg: msg.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DomExceptionQuotaExceededError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.pad(&self.msg)
|
|
}
|
|
}
|
|
impl fmt::Display for DomExceptionInvalidCharacterError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.pad(&self.msg)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DomExceptionQuotaExceededError {}
|
|
|
|
impl std::error::Error for DomExceptionInvalidCharacterError {}
|
|
|
|
pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> {
|
|
e.downcast_ref::<DomExceptionQuotaExceededError>()
|
|
.map(|_| "DOMExceptionQuotaExceededError")
|
|
.or_else(|| {
|
|
e.downcast_ref::<DomExceptionInvalidCharacterError>()
|
|
.map(|_| "DOMExceptionInvalidCharacterError")
|
|
})
|
|
}
|
|
pub struct Location(pub Url);
|