From fbc58f95d415e56c53bed1bb71322611c5d0758a Mon Sep 17 00:00:00 2001 From: Hexagon Date: Sat, 11 May 2024 02:15:35 +0200 Subject: [PATCH] getMany -> list. Add data integrity check. --- README.md | 6 +++--- src/kv.test.ts | 14 +++++++------- src/kv.ts | 6 +++--- src/ledger.ts | 9 ++++++++- src/transaction.ts | 27 +++++++++++++++++++-------- src/utils/hash.ts | 12 ++++++++++++ 6 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 src/utils/hash.ts diff --git a/README.md b/README.md index 4254184..037d054 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,12 @@ await kvStore.set(["users", "by_id", 5], { // Use the index to select users between 2 and 4 console.log( "Users 2-4:", - await kvStore.getMany(["users", "by_id", { from: 2, to: 4 }]), + await kvStore.list(["users", "by_id", { from: 2, to: 4 }]), ); // ... will output the objects of Alice, Ben and Lisa // Use a plain JavaScript filter (less performant) to find a user named ben -const ben = (await kvStore.getMany(["users"])).filter((user) => +const ben = (await kvStore.list(["users"])).filter((user) => user.name === "Ben" ); console.log("Ben: ", ben); // Outputs the object of Ben @@ -113,7 +113,7 @@ await kvStore.close(); - `open(filepath)` - `set(key, value)` - `get(key)` - - `getMany(key)` + - `list(key)` - `delete(key)` - `beginTransaction()` - `endTransaction()` diff --git a/src/kv.test.ts b/src/kv.test.ts index e9462f2..16a3d66 100644 --- a/src/kv.test.ts +++ b/src/kv.test.ts @@ -138,7 +138,7 @@ test("KV: supports numeric key ranges", async () => { } // Test if the 'get' function returns the expected values - const rangeResults = await kvStore.getMany(["data", { from: 7, to: 9 }]); + const rangeResults = await kvStore.list(["data", { from: 7, to: 9 }]); assertEquals(rangeResults.length, 3); assertEquals(rangeResults[0].data, "Value 7"); assertEquals(rangeResults[1].data, "Value 8"); @@ -159,7 +159,7 @@ test("KV: supports additional levels after numeric key ranges", async () => { } // Test if the 'get' function returns the expected values - const rangeResults = await kvStore.getMany([ + const rangeResults = await kvStore.list([ "data", { from: 7, to: 9 }, "doc1", @@ -184,13 +184,13 @@ test("KV: supports empty numeric key ranges to get all", async () => { } // Test if the 'get' function returns the expected values - const rangeResults = await kvStore.getMany(["data", {}, "doc1"]); + const rangeResults = await kvStore.list(["data", {}, "doc1"]); assertEquals(rangeResults.length, 6); - const rangeResults2 = await kvStore.getMany(["data", {}, "doc2"]); + const rangeResults2 = await kvStore.list(["data", {}, "doc2"]); assertEquals(rangeResults2.length, 6); - const rangeResults3 = await kvStore.getMany(["data", {}]); + const rangeResults3 = await kvStore.list(["data", {}]); assertEquals(rangeResults3.length, 12); - const rangeResults4 = await kvStore.getMany(["data"]); + const rangeResults4 = await kvStore.list(["data"]); assertEquals(rangeResults4.length, 12); await kvStore.close(); @@ -207,7 +207,7 @@ test("KV: supports string key ranges", async () => { await kvStore.set(["files", "image_1"], "Image 1"); // Get all values within the "doc_" range - const rangeResults = await kvStore.getMany(["files", { + const rangeResults = await kvStore.list(["files", { from: "doc_", to: "doc_z", }]); diff --git a/src/kv.ts b/src/kv.ts index a4456a4..ce31942 100644 --- a/src/kv.ts +++ b/src/kv.ts @@ -141,7 +141,7 @@ export class KV { * @returns The retrieved value, or null if not found. */ public async get(key: KVKeyRepresentation): Promise { - const result = await this.getMany(key, 1); + const result = await this.list(key, 1); if (result.length) { return result[0]; } else { @@ -156,7 +156,7 @@ export class KV { * @param limit - Optional maximum number of values to retrieve. * @returns An array of retrieved values. */ - async getMany( + async list( key: KVKeyRepresentation, limit?: number, ): Promise { @@ -173,7 +173,7 @@ export class KV { if (result?.transaction) { results.push({ ts: result?.transaction.timestamp, - data: result?.transaction.value, + data: result?.transaction.data, }); count++; } diff --git a/src/ledger.ts b/src/ledger.ts index 45277b0..e462143 100644 --- a/src/ledger.ts +++ b/src/ledger.ts @@ -10,6 +10,7 @@ import { SUPPORTED_LEDGER_VERSIONS } from "./constants.ts"; import { KVOperation, KVTransaction } from "./transaction.ts"; import type { KVKey } from "./key.ts"; import { rename, unlink } from "@cross/fs"; +import { compareHash, sha1 } from "./utils/hash.ts"; export interface KVTransactionMeta { key: KVKey; @@ -235,12 +236,18 @@ export class KVLedger { // Read transaction data (optional) if (decodeData) { + const originalHash: Uint8Array = transaction.hash!; const transactionHeaderData = await readAtPosition( fd, dataLength, offset + 8 + headerLength, ); - transaction.dataFromUint8Array(transactionHeaderData); + await transaction.dataFromUint8Array(transactionHeaderData); + + // Validate data + if (!compareHash(originalHash, transaction.hash!)) { + throw new Error("Invalid data"); + } } return { offset: offset, diff --git a/src/transaction.ts b/src/transaction.ts index 16a0318..50477c5 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -1,3 +1,4 @@ +import { sha1 } from "./utils/hash.ts"; import { extDecoder, extEncoder } from "./cbor.ts"; import { KVKey, type KVKeyRepresentation } from "./key.ts"; @@ -24,6 +25,11 @@ export interface KVTransactionHeader { * Operation timestamp */ t: number; + + /** + * Hash + */ + h: Uint8Array; } export type KVTransactionData = Uint8Array; @@ -33,12 +39,13 @@ export class KVTransaction { public key?: KVKey; public operation?: KVOperation; public timestamp?: number; - public value?: unknown; + public data?: Uint8Array; + public hash?: Uint8Array; constructor() { } - public create( + public async create( key: KVKey | KVKeyRepresentation, operation: KVOperation, timestamp: number, @@ -51,7 +58,8 @@ export class KVTransaction { } this.operation = operation; this.timestamp = timestamp; - this.value = value; + this.data = value ? extEncoder.encode(value) : undefined; + this.hash = this.data ? await sha1(this.data) : undefined; } public headerFromUint8Array(data: Uint8Array) { @@ -61,10 +69,14 @@ export class KVTransaction { this.key = decoded.k; this.operation = decoded.o; this.timestamp = decoded.t; + this.hash = decoded.h; } - public dataFromUint8Array(data: Uint8Array) { - this.value = extDecoder.decode(data); + public async dataFromUint8Array(data: Uint8Array) { + this.data = extDecoder.decode(data); + if (data) { + this.hash = await sha1(data); + } } /** @@ -76,6 +88,7 @@ export class KVTransaction { k: this.key!, o: this.operation!, t: this.timestamp!, + h: this.hash!, }; // Encode header @@ -84,9 +97,7 @@ export class KVTransaction { ); // Encode data - const pendingTransactionData = this.value - ? extEncoder.encode(this.value) - : undefined; + const pendingTransactionData = this.data; const pendingTransactionDataLength = pendingTransactionData ? pendingTransactionData.length : 0; diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100644 index 0000000..cebce0c --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,12 @@ +export async function sha1(data: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-1", data)); +} + +export function compareHash(arr1: Uint8Array, arr2: Uint8Array): boolean { + if (arr1.length !== arr2.length) return false; // Length mismatch + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) return false; // Value mismatch + } + return true; +}