Skip to content

Commit

Permalink
feat: Allow (but don't require) overwriting putBlock in JS (#409)
Browse files Browse the repository at this point in the history
* feat: Allow (but don't require) overwriting `putBlock` in JS

* chore: Write a test for overwriting the `putBlock` method
  • Loading branch information
matheus23 authored Mar 8, 2024
1 parent 534c312 commit b4bc5e2
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 8 deletions.
87 changes: 79 additions & 8 deletions wnfs-wasm/src/fs/blockstore.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -30,6 +30,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = "putBlockKeyed")]
pub(crate) fn put_block_keyed(store: &BlockStore, cid: Vec<u8>, bytes: Vec<u8>) -> Promise;

#[wasm_bindgen(method, js_name = "putBlock")]
pub(crate) fn put_block(store: &BlockStore, bytes: Vec<u8>, codec: u32) -> Promise;

#[wasm_bindgen(method, js_name = "getBlock")]
pub(crate) fn get_block(store: &BlockStore, cid: Vec<u8>) -> Promise;

Expand Down Expand Up @@ -59,15 +62,15 @@ 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(())
}

async fn get_block(&self, cid: &Cid) -> Result<Bytes, BlockStoreError> {
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));
Expand All @@ -79,10 +82,78 @@ impl WnfsBlockStore for ForeignBlockStore {
}

async fn has_block(&self, cid: &Cid) -> Result<bool, BlockStoreError> {
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<Bytes>, codec: u64) -> Result<Cid, BlockStoreError> {
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<BlockStoreError, BlockStoreError> {
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:?}"
))
}
32 changes: 32 additions & 0 deletions wnfs-wasm/tests/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,42 @@ const createRecipientExchangeRoot = async (
return [key, rootDir];
};

class Sha256BlockStore {
private store: Map<string, Uint8Array>;

constructor() {
this.store = new Map();
}

async getBlock(cid: Uint8Array): Promise<Uint8Array | undefined> {
const decodedCid = CID.decode(cid);
return this.store.get(decodedCid.toString());
}

async putBlockKeyed(cid: Uint8Array, bytes: Uint8Array): Promise<void> {
const decodedCid = CID.decode(cid);
this.store.set(decodedCid.toString(), bytes);
}

async hasBlock(cid: Uint8Array): Promise<boolean> {
const decodedCid = CID.decode(cid);
return this.store.has(decodedCid.toString());
}

// We overwrite the putBlock method
async putBlock(bytes: Uint8Array, codec: number): Promise<Uint8Array> {
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,
Expand Down
29 changes: 29 additions & 0 deletions wnfs-wasm/tests/public.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
///<reference path="server/index.d.ts"/>

import { expect, test } from "@playwright/test";
import { CID } from "multiformats";
import { sha256 } from "multiformats/hashes/sha2";

const url = "http://localhost:8085";

Expand Down Expand Up @@ -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);
})
})
1 change: 1 addition & 0 deletions wnfs-wasm/tests/server/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions wnfs-wasm/tests/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
sampleCID,
CID,
MemoryBlockStore,
Sha256BlockStore,
Rng,
createSharerDir,
createRecipientExchangeRoot,
Expand Down Expand Up @@ -34,6 +35,7 @@ const setup = async () => {
sampleCID,
CID,
MemoryBlockStore,
Sha256BlockStore,
Rng,
createSharerDir,
createRecipientExchangeRoot,
Expand Down

0 comments on commit b4bc5e2

Please sign in to comment.