diff options
Diffstat (limited to 'extensions')
-rw-r--r-- | extensions/webstorage/01_webstorage.js | 190 | ||||
-rw-r--r-- | extensions/webstorage/Cargo.toml | 19 | ||||
-rw-r--r-- | extensions/webstorage/README.md | 5 | ||||
-rw-r--r-- | extensions/webstorage/lib.deno_webstorage.d.ts | 42 | ||||
-rw-r--r-- | extensions/webstorage/lib.rs | 316 |
5 files changed, 572 insertions, 0 deletions
diff --git a/extensions/webstorage/01_webstorage.js b/extensions/webstorage/01_webstorage.js new file mode 100644 index 000000000..a11d44068 --- /dev/null +++ b/extensions/webstorage/01_webstorage.js @@ -0,0 +1,190 @@ +((window) => { + const core = window.Deno.core; + const webidl = window.__bootstrap.webidl; + + const _rid = Symbol("[[rid]]"); + + class Storage { + [_rid]; + + constructor() { + webidl.illegalConstructor(); + } + + get length() { + webidl.assertBranded(this, Storage); + return core.opSync("op_webstorage_length", this[_rid]); + } + + key(index) { + webidl.assertBranded(this, Storage); + const prefix = "Failed to execute 'key' on 'Storage'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + index = webidl.converters["unsigned long"](index, { + prefix, + context: "Argument 1", + }); + + return core.opSync("op_webstorage_key", { + rid: this[_rid], + index, + }); + } + + setItem(key, value) { + webidl.assertBranded(this, Storage); + const prefix = "Failed to execute 'setItem' on 'Storage'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + key = webidl.converters.DOMString(key, { + prefix, + context: "Argument 1", + }); + value = webidl.converters.DOMString(value, { + prefix, + context: "Argument 2", + }); + + core.opSync("op_webstorage_set", { + rid: this[_rid], + keyName: key, + keyValue: value, + }); + } + + getItem(key) { + webidl.assertBranded(this, Storage); + const prefix = "Failed to execute 'getItem' on 'Storage'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + key = webidl.converters.DOMString(key, { + prefix, + context: "Argument 1", + }); + + return core.opSync("op_webstorage_get", { + rid: this[_rid], + keyName: key, + }); + } + + removeItem(key) { + webidl.assertBranded(this, Storage); + const prefix = "Failed to execute 'removeItem' on 'Storage'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + key = webidl.converters.DOMString(key, { + prefix, + context: "Argument 1", + }); + + core.opSync("op_webstorage_remove", { + rid: this[_rid], + keyName: key, + }); + } + + clear() { + webidl.assertBranded(this, Storage); + core.opSync("op_webstorage_clear", this[_rid]); + } + } + + function createStorage(persistent) { + if (persistent) window.location; + + const rid = core.opSync("op_webstorage_open", persistent); + + const storage = webidl.createBranded(Storage); + storage[_rid] = rid; + + const proxy = new Proxy(storage, { + deleteProperty(target, key) { + if (typeof key == "symbol") { + delete target[key]; + } else { + target.removeItem(key); + } + return true; + }, + defineProperty(target, key, descriptor) { + if (typeof key == "symbol") { + Object.defineProperty(target, key, descriptor); + } else { + target.setItem(key, descriptor.value); + } + return true; + }, + get(target, key) { + if (typeof key == "symbol") return target[key]; + if (key in target) { + return Reflect.get(...arguments); + } else { + return target.getItem(key) ?? undefined; + } + }, + set(target, key, value) { + if (typeof key == "symbol") { + Object.defineProperty(target, key, { + value, + configurable: true, + }); + } else { + target.setItem(key, value); + } + return true; + }, + has(target, p) { + return (typeof target.getItem(p)) === "string"; + }, + ownKeys() { + return core.opSync("op_webstorage_iterate_keys", rid); + }, + getOwnPropertyDescriptor(target, key) { + if (arguments.length === 1) { + return undefined; + } + if (key in target) { + return undefined; + } + const value = target.getItem(key); + if (value === null) { + return undefined; + } + return { + value, + enumerable: true, + configurable: true, + writable: true, + }; + }, + }); + + proxy[Symbol.for("Deno.customInspect")] = function (inspect) { + return `${this.constructor.name} ${ + inspect({ + length: this.length, + ...Object.fromEntries(Object.entries(proxy)), + }) + }`; + }; + + return proxy; + } + + let localStorage; + let sessionStorage; + + window.__bootstrap.webStorage = { + localStorage() { + if (!localStorage) { + localStorage = createStorage(true); + } + return localStorage; + }, + sessionStorage() { + if (!sessionStorage) { + sessionStorage = createStorage(false); + } + return sessionStorage; + }, + Storage, + }; +})(this); diff --git a/extensions/webstorage/Cargo.toml b/extensions/webstorage/Cargo.toml new file mode 100644 index 000000000..acfaf6a16 --- /dev/null +++ b/extensions/webstorage/Cargo.toml @@ -0,0 +1,19 @@ +# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_webstorage" +version = "0.1.0" +edition = "2018" +description = "Implementation of WebStorage API for Deno" +authors = ["the Deno authors"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.86.0", path = "../../core" } +rusqlite = { version = "0.25.0", features = ["unlock_notify", "bundled"] } +serde = { version = "1.0.125", features = ["derive"] } diff --git a/extensions/webstorage/README.md b/extensions/webstorage/README.md new file mode 100644 index 000000000..a0f8a0613 --- /dev/null +++ b/extensions/webstorage/README.md @@ -0,0 +1,5 @@ +# deno_webstorage + +This op crate implements the WebStorage spec in Deno. + +Spec: https://html.spec.whatwg.org/multipage/webstorage.html diff --git a/extensions/webstorage/lib.deno_webstorage.d.ts b/extensions/webstorage/lib.deno_webstorage.d.ts new file mode 100644 index 000000000..bf438e005 --- /dev/null +++ b/extensions/webstorage/lib.deno_webstorage.d.ts @@ -0,0 +1,42 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// deno-lint-ignore-file no-explicit-any + +/// <reference no-default-lib="true" /> +/// <reference lib="esnext" /> + +/** This Web Storage API interface provides access to a particular domain's session or local storage. It allows, for example, the addition, modification, or deletion of stored data items. */ +interface Storage { + /** + * Returns the number of key/value pairs currently present in the list associated with the object. + */ + readonly length: number; + /** + * Empties the list associated with the object of all key/value pairs, if there are any. + */ + clear(): void; + /** + * Returns the current value associated with the given key, or null if the given key does not exist in the list associated with the object. + */ + getItem(key: string): string | null; + /** + * Returns the name of the nth key in the list, or null if n is greater than or equal to the number of key/value pairs in the object. + */ + key(index: number): string | null; + /** + * Removes the key/value pair with the given key from the list associated with the object, if a key/value pair with the given key exists. + */ + removeItem(key: string): void; + /** + * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. + * + * Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) + */ + setItem(key: string, value: string): void; + [name: string]: any; +} + +declare var Storage: { + prototype: Storage; + new (): Storage; +}; diff --git a/extensions/webstorage/lib.rs b/extensions/webstorage/lib.rs new file mode 100644 index 000000000..90ae0598a --- /dev/null +++ b/extensions/webstorage/lib.rs @@ -0,0 +1,316 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::bad_resource_id; +use deno_core::error::AnyError; +use deno_core::include_js_files; +use deno_core::op_sync; +use deno_core::Extension; +use deno_core::OpState; +use deno_core::Resource; +use deno_core::ZeroCopyBuf; +use rusqlite::params; +use rusqlite::Connection; +use rusqlite::OptionalExtension; +use serde::Deserialize; +use std::borrow::Cow; +use std::fmt; +use std::path::PathBuf; + +#[derive(Clone)] +struct LocationDataDir(PathBuf); + +pub fn init(location_data_dir: Option<PathBuf>) -> Extension { + Extension::builder() + .js(include_js_files!( + prefix "deno:extensions/webstorage", + "01_webstorage.js", + )) + .ops(vec![ + ("op_webstorage_open", op_sync(op_webstorage_open)), + ("op_webstorage_length", op_sync(op_webstorage_length)), + ("op_webstorage_key", op_sync(op_webstorage_key)), + ("op_webstorage_set", op_sync(op_webstorage_set)), + ("op_webstorage_get", op_sync(op_webstorage_get)), + ("op_webstorage_remove", op_sync(op_webstorage_remove)), + ("op_webstorage_clear", op_sync(op_webstorage_clear)), + ( + "op_webstorage_iterate_keys", + op_sync(op_webstorage_iterate_keys), + ), + ]) + .state(move |state| { + if let Some(location_data_dir) = location_data_dir.clone() { + state.put(LocationDataDir(location_data_dir)); + } + Ok(()) + }) + .build() +} + +pub fn get_declaration() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_webstorage.d.ts") +} + +struct WebStorageConnectionResource(Connection); + +impl Resource for WebStorageConnectionResource { + fn name(&self) -> Cow<str> { + "webStorage".into() + } +} + +pub fn op_webstorage_open( + state: &mut OpState, + persistent: bool, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<u32, AnyError> { + let connection = if persistent { + let path = state.try_borrow::<LocationDataDir>().ok_or_else(|| { + DomExceptionNotSupportedError::new( + "LocalStorage is not supported in this context.", + ) + })?; + std::fs::create_dir_all(&path.0)?; + Connection::open(path.0.join("local_storage"))? + } else { + Connection::open_in_memory()? + }; + + connection.execute( + "CREATE TABLE IF NOT EXISTS data (key VARCHAR UNIQUE, value VARCHAR)", + params![], + )?; + + let rid = state + .resource_table + .add(WebStorageConnectionResource(connection)); + Ok(rid) +} + +pub fn op_webstorage_length( + state: &mut OpState, + rid: u32, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<u32, AnyError> { + let resource = state + .resource_table + .get::<WebStorageConnectionResource>(rid) + .ok_or_else(bad_resource_id)?; + + let mut stmt = resource.0.prepare("SELECT COUNT(*) FROM data")?; + + let length: u32 = stmt.query_row(params![], |row| row.get(0))?; + + Ok(length) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct KeyArgs { + rid: u32, + index: u32, +} + +pub fn op_webstorage_key( + state: &mut OpState, + args: KeyArgs, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<Option<String>, AnyError> { + let resource = state + .resource_table + .get::<WebStorageConnectionResource>(args.rid) + .ok_or_else(bad_resource_id)?; + + let mut stmt = resource + .0 + .prepare("SELECT key FROM data LIMIT 1 OFFSET ?")?; + + let key: Option<String> = stmt + .query_row(params![args.index], |row| row.get(0)) + .optional()?; + + Ok(key) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SetArgs { + rid: u32, + key_name: String, + key_value: String, +} + +pub fn op_webstorage_set( + state: &mut OpState, + args: SetArgs, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<(), AnyError> { + let resource = state + .resource_table + .get::<WebStorageConnectionResource>(args.rid) + .ok_or_else(bad_resource_id)?; + + let mut stmt = resource + .0 + .prepare("SELECT SUM(pgsize) FROM dbstat WHERE name = 'data'")?; + let size: u32 = stmt.query_row(params![], |row| row.get(0))?; + + if size >= 5000000 { + return Err( + DomExceptionQuotaExceededError::new("Exceeded maximum storage size") + .into(), + ); + } + + resource.0.execute( + "INSERT OR REPLACE INTO data (key, value) VALUES (?, ?)", + params![args.key_name, args.key_value], + )?; + + Ok(()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetArgs { + rid: u32, + key_name: String, +} + +pub fn op_webstorage_get( + state: &mut OpState, + args: GetArgs, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<Option<String>, AnyError> { + let resource = state + .resource_table + .get::<WebStorageConnectionResource>(args.rid) + .ok_or_else(bad_resource_id)?; + + let mut stmt = resource.0.prepare("SELECT value FROM data WHERE key = ?")?; + + let val = stmt + .query_row(params![args.key_name], |row| row.get(0)) + .optional()?; + + Ok(val) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveArgs { + rid: u32, + key_name: String, +} + +pub fn op_webstorage_remove( + state: &mut OpState, + args: RemoveArgs, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<(), AnyError> { + let resource = state + .resource_table + .get::<WebStorageConnectionResource>(args.rid) + .ok_or_else(bad_resource_id)?; + + resource + .0 + .execute("DELETE FROM data WHERE key = ?", params![args.key_name])?; + + Ok(()) +} + +pub fn op_webstorage_clear( + state: &mut OpState, + rid: u32, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<(), AnyError> { + let resource = state + .resource_table + .get::<WebStorageConnectionResource>(rid) + .ok_or_else(bad_resource_id)?; + + resource.0.execute("DROP TABLE data", params![])?; + resource.0.execute( + "CREATE TABLE data (key VARCHAR UNIQUE, value VARCHAR)", + params![], + )?; + + Ok(()) +} + +pub fn op_webstorage_iterate_keys( + state: &mut OpState, + rid: u32, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<Vec<String>, AnyError> { + let resource = state + .resource_table + .get::<WebStorageConnectionResource>(rid) + .ok_or_else(bad_resource_id)?; + + let mut stmt = resource.0.prepare("SELECT key FROM data")?; + + let keys = stmt + .query_map(params![], |row| row.get::<_, String>(0))? + .map(|r| r.unwrap()) + .collect(); + + Ok(keys) +} + +#[derive(Debug)] +pub struct DomExceptionQuotaExceededError { + pub msg: String, +} + +impl DomExceptionQuotaExceededError { + pub fn new(msg: &str) -> Self { + DomExceptionQuotaExceededError { + msg: msg.to_string(), + } + } +} + +impl fmt::Display for DomExceptionQuotaExceededError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } +} + +impl std::error::Error for DomExceptionQuotaExceededError {} + +pub fn get_quota_exceeded_error_class_name( + e: &AnyError, +) -> Option<&'static str> { + e.downcast_ref::<DomExceptionQuotaExceededError>() + .map(|_| "DOMExceptionQuotaExceededError") +} + +#[derive(Debug)] +pub struct DomExceptionNotSupportedError { + pub msg: String, +} + +impl DomExceptionNotSupportedError { + pub fn new(msg: &str) -> Self { + DomExceptionNotSupportedError { + msg: msg.to_string(), + } + } +} + +impl fmt::Display for DomExceptionNotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } +} + +impl std::error::Error for DomExceptionNotSupportedError {} + +pub fn get_not_supported_error_class_name( + e: &AnyError, +) -> Option<&'static str> { + e.downcast_ref::<DomExceptionNotSupportedError>() + .map(|_| "DOMExceptionNotSupportedError") +} |