From b4cd346b6244443023e4b2c5216149025517e5e3 Mon Sep 17 00:00:00 2001 From: Andras Toth Date: Thu, 22 Sep 2022 10:54:05 +0200 Subject: [PATCH] [FIX] cross language compatible meta subject BREAKING CHANGE --- nats-base-client/base64.ts | 41 +++++++++++++++++++++++++++++++++ nats-base-client/objectstore.ts | 5 ++-- tests/objectstore_test.ts | 10 ++++++-- 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 nats-base-client/base64.ts diff --git a/nats-base-client/base64.ts b/nats-base-client/base64.ts new file mode 100644 index 00000000..13d628ce --- /dev/null +++ b/nats-base-client/base64.ts @@ -0,0 +1,41 @@ +export class Base64Codec { + static encode(bytes: string | Uint8Array): string { + if (typeof bytes === "string") { + return btoa(bytes); + } + const a = Array.from(bytes); + return btoa(String.fromCharCode(...a)); + } + + static decode(s: string, binary = false): Uint8Array | string { + const bin = atob(s); + if (!binary) { + return bin; + } + return Uint8Array.from(bin, (c) => c.charCodeAt(0)); + } +} + +export class Base64UrlCodec { + static encode(bytes: string | Uint8Array): string { + return Base64UrlCodec.toB64URLEncoding(Base64Codec.encode(bytes)); + } + + static decode(s: string, binary = false): Uint8Array | string { + return Base64Codec.decode(Base64UrlCodec.fromB64URLEncoding(s), binary); + } + + static toB64URLEncoding(b64str: string): string { + return b64str + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); + } + + static fromB64URLEncoding(b64str: string): string { + // pads are % 4, but not necessary on decoding + return b64str + .replace(/_/g, "/") + .replace(/-/g, "+"); + } +} diff --git a/nats-base-client/objectstore.ts b/nats-base-client/objectstore.ts index ce9bec56..d25a7483 100644 --- a/nats-base-client/objectstore.ts +++ b/nats-base-client/objectstore.ts @@ -35,6 +35,7 @@ import { StreamInfoRequestOptions, } from "./types.ts"; import { validateBucket, validateKey } from "./kv.ts"; +import { Base64UrlCodec } from "./base64.ts"; import { JSONCodec } from "./codec.ts"; import { nuid } from "./nuid.ts"; import { deferred } from "./util.ts"; @@ -247,7 +248,7 @@ export class ObjectStoreImpl implements ObjectStore { return Promise.reject(error); } - const meta = `$O.${this.name}.M.${obj}`; + const meta = this._metaSubject(obj); try { const m = await this.jsm.streams.getMessage(this.stream, { last_by_subj: meta, @@ -673,7 +674,7 @@ export class ObjectStoreImpl implements ObjectStore { } _metaSubject(n: string): string { - return `$O.${this.name}.M.${n}`; + return `$O.${this.name}.M.${Base64UrlCodec.encode(n)}`; } _metaSubjectAll(): string { diff --git a/tests/objectstore_test.ts b/tests/objectstore_test.ts index b4b8c8bf..e2c7f0ce 100644 --- a/tests/objectstore_test.ts +++ b/tests/objectstore_test.ts @@ -33,6 +33,7 @@ import { assertRejects } from "https://deno.land/std@0.152.0/testing/asserts.ts" import { equals } from "https://deno.land/std@0.152.0/bytes/mod.ts"; import { ObjectInfo, ObjectStoreMeta } from "../nats-base-client/types.ts"; import { SHA256 } from "../nats-base-client/sha256.js"; +import { Base64UrlCodec } from "../nats-base-client/base64.ts"; function readableStreamFrom(data: Uint8Array): ReadableStream { return new ReadableStream({ @@ -676,9 +677,14 @@ Deno.test("objectstore - sanitize", async () => { const info = await os.status({ subjects_filter: ">", }); - assertEquals(info.streamInfo.state?.subjects!["$O.test.M.has_dots_here"], 1); assertEquals( - info.streamInfo.state.subjects!["$O.test.M.the_spaces_are_here"], + info.streamInfo.state + ?.subjects![`$O.test.M.${Base64UrlCodec.encode("has_dots_here")}`], + 1, + ); + assertEquals( + info.streamInfo.state + .subjects![`$O.test.M.${Base64UrlCodec.encode("the_spaces_are_here")}`], 1, );