diff --git a/wnfs-wasm/src/fs/blockstore.rs b/wnfs-wasm/src/fs/blockstore.rs index e68d2de1..aeaf74b8 100644 --- a/wnfs-wasm/src/fs/blockstore.rs +++ b/wnfs-wasm/src/fs/blockstore.rs @@ -1,11 +1,11 @@ //! The bindgen API for WNFS block store. -use super::utils::anyhow_error; use anyhow::Result; use bytes::Bytes; -use js_sys::{Promise, Uint8Array}; +use js_sys::{Promise, Reflect, Uint8Array}; use libipld_core::cid::Cid; -use wasm_bindgen::prelude::wasm_bindgen; +use std::str::FromStr; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; use wasm_bindgen_futures::JsFuture; use wnfs::common::{BlockStore as WnfsBlockStore, BlockStoreError}; @@ -30,6 +30,9 @@ extern "C" { #[wasm_bindgen(method, js_name = "putBlockKeyed")] pub(crate) fn put_block_keyed(store: &BlockStore, cid: Vec, bytes: Vec) -> Promise; + #[wasm_bindgen(method, js_name = "putBlock")] + pub(crate) fn put_block(store: &BlockStore, bytes: Vec, codec: u32) -> Promise; + #[wasm_bindgen(method, js_name = "getBlock")] pub(crate) fn get_block(store: &BlockStore, cid: Vec) -> Promise; @@ -59,7 +62,7 @@ impl WnfsBlockStore for ForeignBlockStore { JsFuture::from(self.0.put_block_keyed(cid.to_bytes(), bytes.into())) .await - .map_err(anyhow_error("Cannot put block: {:?}"))?; + .map_err(handle_blockstore_err)?; Ok(()) } @@ -67,7 +70,7 @@ impl WnfsBlockStore for ForeignBlockStore { async fn get_block(&self, cid: &Cid) -> Result { let value = JsFuture::from(self.0.get_block(cid.to_bytes())) .await - .map_err(anyhow_error("Cannot get block: {:?}"))?; + .map_err(handle_blockstore_err)?; if value.is_undefined() { return Err(BlockStoreError::CIDNotFound(*cid)); @@ -79,10 +82,78 @@ impl WnfsBlockStore for ForeignBlockStore { } async fn has_block(&self, cid: &Cid) -> Result { - let value = JsFuture::from(self.0.has_block(cid.to_bytes())) + let has_block = JsFuture::from(self.0.has_block(cid.to_bytes())) .await - .map_err(anyhow_error("Cannot run has_block: {:?}"))?; + .map_err(handle_blockstore_err)?; + + Ok(js_sys::Boolean::from(has_block).value_of()) + } + + async fn put_block(&self, bytes: impl Into, codec: u64) -> Result { + let bytes: Bytes = bytes.into(); + + if Reflect::has(&self.0, &"putBlock".into()).map_err(reflection_err)? { + let codec = codec.try_into().map_err(|e| { + anyhow::anyhow!("Can't convert 64-bit codec to 32-bit codec for javascript: {e:?}") + })?; + let cid = JsFuture::from(self.0.put_block(bytes.into(), codec)) + .await + .map_err(handle_blockstore_err)?; + + // Convert the value to a vector of bytes. + let bytes = Uint8Array::new(&cid).to_vec(); + + // Construct CID from the bytes. + Ok(Cid::try_from(&bytes[..])?) + } else { + let cid = self.create_cid(&bytes, codec)?; + self.put_block_keyed(cid, bytes).await?; + Ok(cid) + } + } +} - Ok(js_sys::Boolean::from(value).value_of()) +fn handle_blockstore_err(js_err: JsValue) -> BlockStoreError { + match into_blockstore_err(js_err) { + Ok(err) => err, + Err(err) => err, } } + +fn into_blockstore_err(js_err: JsValue) -> Result { + let code = Reflect::get(&js_err, &"code".into()).map_err(reflection_err)?; + + if let Some(code) = code.as_string() { + Ok(match code.as_ref() { + "MAXIMUM_BLOCK_SIZE_EXCEEDED" => BlockStoreError::MaximumBlockSizeExceeded( + Reflect::get(&js_err, &"size".into()) + .map_err(reflection_err)? + .as_f64() + .ok_or_else(|| reflection_err("'size' field on error not a number"))? + as usize, + ), + "CID_NOT_FOUND" => BlockStoreError::CIDNotFound(Cid::from_str( + &Reflect::get(&js_err, &"cid".into()) + .map_err(reflection_err)? + .as_string() + .ok_or_else(|| reflection_err("'cid' field on error not a string"))?, + )?), + "CID_ERROR" => BlockStoreError::CIDError(libipld_core::cid::Error::ParsingError), + _ => { + // It may just be another error type + BlockStoreError::Custom(anyhow::anyhow!("Blockstore operation failed: {js_err:?}")) + } + }) + } else { + // 'code' may not be a string, e.g. undefined or integer, due to other errors on the js side. + Ok(BlockStoreError::Custom(anyhow::anyhow!( + "Blockstore operation failed: {js_err:?}" + ))) + } +} + +fn reflection_err(err: impl core::fmt::Debug) -> BlockStoreError { + BlockStoreError::Custom(anyhow::anyhow!( + "Fatal error while collecting JS error in blockstore operation: {err:?}" + )) +} diff --git a/wnfs-wasm/tests/mock.ts b/wnfs-wasm/tests/mock.ts index 695a79fd..4b0c33e4 100644 --- a/wnfs-wasm/tests/mock.ts +++ b/wnfs-wasm/tests/mock.ts @@ -186,10 +186,42 @@ const createRecipientExchangeRoot = async ( return [key, rootDir]; }; +class Sha256BlockStore { + private store: Map; + + constructor() { + this.store = new Map(); + } + + async getBlock(cid: Uint8Array): Promise { + const decodedCid = CID.decode(cid); + return this.store.get(decodedCid.toString()); + } + + async putBlockKeyed(cid: Uint8Array, bytes: Uint8Array): Promise { + const decodedCid = CID.decode(cid); + this.store.set(decodedCid.toString(), bytes); + } + + async hasBlock(cid: Uint8Array): Promise { + const decodedCid = CID.decode(cid); + return this.store.has(decodedCid.toString()); + } + + // We overwrite the putBlock method + async putBlock(bytes: Uint8Array, codec: number): Promise { + const hash = await sha256.digest(bytes); + const cid = CID.create(1, codec, hash); + this.store.set(cid.toString(), bytes); + return cid.bytes; + } +} + export { sampleCID, CID, MemoryBlockStore, + Sha256BlockStore, Rng, createSharerDir, createRecipientExchangeRoot, diff --git a/wnfs-wasm/tests/public.spec.ts b/wnfs-wasm/tests/public.spec.ts index 77941019..c2a02412 100644 --- a/wnfs-wasm/tests/public.spec.ts +++ b/wnfs-wasm/tests/public.spec.ts @@ -1,6 +1,8 @@ /// import { expect, test } from "@playwright/test"; +import { CID } from "multiformats"; +import { sha256 } from "multiformats/hashes/sha2"; const url = "http://localhost:8085"; @@ -366,3 +368,30 @@ test.describe("PublicDirectory", () => { expect(result).toEqual(5 * 1024 * 1024); }); }); + +test.describe("BlockStore", () => { + test("a BlockStore implementation can overwrite the putBlock method", async ({ page }) => { + const result = await page.evaluate(async () => { + const { + wnfs: { PublicFile }, + mock: { CID, Sha256BlockStore }, + } = await window.setup(); + + const store = new Sha256BlockStore(); + const time = new Date(); + const file = new PublicFile(time); + + const longString = "x".repeat(5 * 1024 * 1024); + const content = new TextEncoder().encode(longString); + const file2 = await file.setContent(time, content, store); + + const cid = await file2.store(store); + + return CID.decode(cid).toString(); + }); + + const cid = CID.parse(result); + + expect(cid.multihash.code).toEqual(sha256.code); + }) +}) diff --git a/wnfs-wasm/tests/server/index.d.ts b/wnfs-wasm/tests/server/index.d.ts index 44086f46..4bbad564 100644 --- a/wnfs-wasm/tests/server/index.d.ts +++ b/wnfs-wasm/tests/server/index.d.ts @@ -7,6 +7,7 @@ declare global { sampleCID: typeof import("../mock").sampleCID; CID: typeof import("../mock").CID, MemoryBlockStore: typeof import("../mock").MemoryBlockStore; + Sha256BlockStore: typeof import("../mock").Sha256BlockStore; Rng: typeof import("../mock").Rng; ExchangeKey: typeof import("../mock").ExchangeKey; PrivateKey: typeof import("../mock").PrivateKey; diff --git a/wnfs-wasm/tests/server/index.ts b/wnfs-wasm/tests/server/index.ts index e341face..5af08214 100644 --- a/wnfs-wasm/tests/server/index.ts +++ b/wnfs-wasm/tests/server/index.ts @@ -4,6 +4,7 @@ import { sampleCID, CID, MemoryBlockStore, + Sha256BlockStore, Rng, createSharerDir, createRecipientExchangeRoot, @@ -34,6 +35,7 @@ const setup = async () => { sampleCID, CID, MemoryBlockStore, + Sha256BlockStore, Rng, createSharerDir, createRecipientExchangeRoot,