From 965d45766484bb483b8d362cf422ecf11a204a4e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 27 Nov 2024 08:36:34 +0100 Subject: [PATCH] :bug: Fix incorrect encoding of u32 parts of uuid --- common/src/app/common/uuid.cljc | 57 ++++++++-- common/src/app/common/uuid_impl.js | 145 ++++++++++++++++-------- common/test/common_tests/uuid_test.cljc | 49 ++++++++ 3 files changed, 196 insertions(+), 55 deletions(-) diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index 3c5bf5d07..9e2f7f6ee 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -44,15 +44,15 @@ [v] (= zero v)) -#?(:clj - (defn get-word-high - [id] - (.getMostSignificantBits ^UUID id))) +(defn get-word-high + [id] + #?(:clj (.getMostSignificantBits ^UUID id) + :cljs (impl/getHi (.-uuid ^UUID id)))) -#?(:clj - (defn get-word-low - [id] - (.getLeastSignificantBits ^UUID id))) +(defn get-word-low + [id] + #?(:clj (.getLeastSignificantBits ^UUID id) + :cljs (impl/getLo (.-uuid ^UUID id)))) (defn get-bytes [^UUID o] @@ -80,12 +80,21 @@ [id] (impl/shortV8 (dm/str id)))) +#?(:cljs + (defn get-unsigned-parts + "Get a Uint32 array of length 4 that represents the UUID, needed + for interact with wasm" + [this] + (impl/getUnsignedParts (.-uuid ^UUID this)))) + + #?(:cljs (defn get-u32 + "A cached variant of get-unsigned-parts" [this] (let [buffer (unchecked-get this "__u32_buffer")] (if (nil? buffer) - (let [buffer (impl/getUnsignedInt32Array (.-uuid ^UUID this))] + (let [buffer (get-unsigned-parts (.-uuid ^UUID this))] (unchecked-set this "__u32_buffer" buffer) buffer) buffer)))) @@ -97,3 +106,33 @@ b (.getLeastSignificantBits ^UUID id)] (+ (clojure.lang.Murmur3/hashLong a) (clojure.lang.Murmur3/hashLong b))))) + +;; Commented code used for debug +;; #?(:cljs +;; (defn ^:export test-uuid +;; [] +;; (let [expected #uuid "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"] +;; +;; (js/console.log "===> to-from-bytes-roundtrip") +;; (js/console.log (uuid.impl/getBytes (str expected))) +;; (js/console.log (uuid.impl/fromBytes (uuid.impl/getBytes (str expected)))) +;; +;; (js/console.log "===> HI LO roundtrip") +;; (let [hi (uuid.impl/getHi (str expected)) +;; lo (uuid.impl/getLo (str expected)) +;; res (uuid.impl/custom hi lo)] +;; +;; (js/console.log "HI:" hi) +;; (js/console.log "LO:" lo) +;; (js/console.log "RS:" res)) +;; +;; (js/console.log "===> OTHER") +;; (let [parts (uuid.impl/getUnsignedParts (str expected)) +;; res (uuid.impl/fromUnsignedParts (aget parts 0) +;; (aget parts 1) +;; (aget parts 2) +;; (aget parts 3))] +;; (js/console.log "PARTS:" parts) +;; (js/console.log "RES: " res)) +;; +;; ))) diff --git a/common/src/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js index 3267ba3f6..fec186bb5 100644 --- a/common/src/app/common/uuid_impl.js +++ b/common/src/app/common/uuid_impl.js @@ -192,6 +192,76 @@ goog.scope(function() { } }; + const fillBytes = (uuid) => { + let rest; + int8[0] = (rest = parseInt(uuid.slice(0, 8), 16)) >>> 24; + int8[1] = (rest >>> 16) & 0xff; + int8[2] = (rest >>> 8) & 0xff; + int8[3] = rest & 0xff; + + // Parse ........-####-....-....-............ + int8[4] = (rest = parseInt(uuid.slice(9, 13), 16)) >>> 8; + int8[5] = rest & 0xff; + + // Parse ........-....-####-....-............ + int8[6] = (rest = parseInt(uuid.slice(14, 18), 16)) >>> 8; + int8[7] = rest & 0xff; + + // Parse ........-....-....-####-............ + int8[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8; + int8[9] = rest & 0xff, + + // Parse ........-....-....-....-############ + // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes) + int8[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff; + int8[11] = (rest / 0x100000000) & 0xff; + int8[12] = (rest >>> 24) & 0xff; + int8[13] = (rest >>> 16) & 0xff; + int8[14] = (rest >>> 8) & 0xff; + int8[15] = rest & 0xff; + } + + const fromPair = (hi, lo) => { + view.setBigInt64(0, hi); + view.setBigInt64(8, lo); + return encoding.bufferToHex(int8, true); + } + + const getHi = (uuid) => { + fillBytes(uuid); + return view.getBigInt64(0); + } + + const getLo = (uuid) => { + fillBytes(uuid); + return view.getBigInt64(8); + } + + const getBytes = (uuid) => { + fillBytes(uuid); + return Int8Array.from(int8); + } + + const getUnsignedParts = (uuid) => { + fillBytes(uuid); + const result = new Uint32Array(4); + + result[0] = view.getUint32(0) + result[1] = view.getUint32(4); + result[2] = view.getUint32(8); + result[3] = view.getUint32(12); + + return result; + } + + const fromUnsignedParts = (a, b, c, d) => { + view.setUint32(0, a) + view.setUint32(4, b) + view.setUint32(8, c) + view.setUint32(12, d) + return encoding.bufferToHex(int8, true); + } + const fromArray = (u8data) => { int8.set(u8data); return encoding.bufferToHex(int8, true); @@ -209,8 +279,14 @@ goog.scope(function() { }; factory.create = create; - factory.setTag = setTag; factory.fromArray = fromArray; + factory.fromPair = fromPair; + factory.fromUnsignedParts = fromUnsignedParts; + factory.getBytes = getBytes; + factory.getHi = getHi; + factory.getLo = getLo; + factory.getUnsignedParts = getUnsignedParts; + factory.setTag = setTag; return factory; })(); @@ -220,67 +296,44 @@ goog.scope(function() { return encoding.bufferToBase62(short); }; - self.custom = function formatAsUUID(mostSigBits, leastSigBits) { - const most = mostSigBits.toString("16").padStart(16, "0"); - const least = leastSigBits.toString("16").padStart(16, "0"); - return `${most.substring(0, 8)}-${most.substring(8, 12)}-${most.substring(12)}-${least.substring(0, 4)}-${least.substring(4)}`; + self.custom = function formatAsUUID(hi, lo) { + if (!(hi instanceof BigInt)) { + hi = BigInt(hi); + } + if (!(hi instanceof BigInt)) { + lo = BigInt(lo); + } + + return self.v8.fromPair(hi, lo); }; self.fromBytes = function(data) { if (data instanceof Uint8Array) { return self.v8.fromArray(data); } else if (data instanceof Int8Array) { - data = Uint8Array.from(data); return self.v8.fromArray(data); } else { - let buffer = data?.buffer; - if (buffer instanceof ArrayBuffer) { - data = new Uint8Array(buffer); - return self.v8.fromArray(data); - } else { - throw new Error("invalid array type received"); - } + throw new Error("invalid array type received"); } }; - // Code based from uuidjs/parse.ts self.getBytes = function parse(uuid) { - const buffer = new ArrayBuffer(16); - const view = new Int8Array(buffer); - let rest; + return self.v8.getBytes(uuid); + }; - // Parse ########-....-....-....-............ - view[0] = (rest = parseInt(uuid.slice(0, 8), 16)) >>> 24; - view[1] = (rest >>> 16) & 0xff; - view[2] = (rest >>> 8) & 0xff; - view[3] = rest & 0xff; + self.getUnsignedParts = function (uuid) { + return self.v8.getUnsignedParts(uuid); + }; - // Parse ........-####-....-....-............ - view[4] = (rest = parseInt(uuid.slice(9, 13), 16)) >>> 8; - view[5] = rest & 0xff; + self.fromUnsignedParts = function(a,b,c,d) { + return self.v8.fromUnsignedParts(a,b,c,d); + }; - // Parse ........-....-####-....-............ - view[6] = (rest = parseInt(uuid.slice(14, 18), 16)) >>> 8; - view[7] = rest & 0xff; - - // Parse ........-....-....-####-............ - view[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8; - view[9] = rest & 0xff, - - // Parse ........-....-....-....-############ - // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes) - view[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff; - view[11] = (rest / 0x100000000) & 0xff; - view[12] = (rest >>> 24) & 0xff; - view[13] = (rest >>> 16) & 0xff; - view[14] = (rest >>> 8) & 0xff; - view[15] = rest & 0xff; - - return view; + self.getHi = function (uuid) { + return self.v8.getHi(uuid); } - self.getUnsignedInt32Array = function (uuid) { - const bytes = self.getBytes(uuid); - return new Uint32Array(bytes.buffer); + self.getLo = function (uuid) { + return self.v8.getLo(uuid); } }); diff --git a/common/test/common_tests/uuid_test.cljc b/common/test/common_tests/uuid_test.cljc index fac59c529..e0031e1c3 100644 --- a/common/test/common_tests/uuid_test.cljc +++ b/common/test/common_tests/uuid_test.cljc @@ -43,5 +43,54 @@ (t/is (= result uuid)))))) +(t/deftest bytes-roundtrip-2 + (let [uuid (uuid/uuid "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8") + result-bytes (uuid/get-bytes uuid) + expected-hi #?(:clj -6799692559624781374 + :cljs (js/BigInt "-6799692559624781374")) + expected-lo #?(:clj -3327364263599220776 + :cljs (js/BigInt "-3327364263599220776")) + expected-bytes [-95, -94, -93, -92, -79, -78, -63, -62, -47, -46, -45, -44, -43, -42, -41, -40]] + (t/testing "get-bytes" + (let [data (uuid/get-bytes uuid)] + (t/is (= (nth expected-bytes 0) (aget data 0))) + (t/is (= (nth expected-bytes 1) (aget data 1))) + (t/is (= (nth expected-bytes 2) (aget data 2))) + (t/is (= (nth expected-bytes 3) (aget data 3))) + (t/is (= (nth expected-bytes 4) (aget data 4))) + (t/is (= (nth expected-bytes 5) (aget data 5))) + (t/is (= (nth expected-bytes 6) (aget data 6))) + (t/is (= (nth expected-bytes 7) (aget data 7))) + (t/is (= (nth expected-bytes 8) (aget data 8))) + (t/is (= (nth expected-bytes 9) (aget data 9))) + (t/is (= (nth expected-bytes 10) (aget data 10))) + (t/is (= (nth expected-bytes 11) (aget data 11))) + (t/is (= (nth expected-bytes 12) (aget data 12))) + (t/is (= (nth expected-bytes 13) (aget data 13))) + (t/is (= (nth expected-bytes 14) (aget data 14))) + (t/is (= (nth expected-bytes 15) (aget data 15))))) + + (t/testing "from-bytes" + (let [data (create-array expected-bytes) + result (uuid/from-bytes data)] + (t/is (= result uuid)))) + + (t/testing "hi-low" + (let [hi (uuid/get-word-high uuid) + lo (uuid/get-word-low uuid)] + + (t/is (= hi expected-hi)) + (t/is (= lo expected-lo)))) + + #?(:cljs + (t/testing "unsigned-parts" + (let [parts (uuid/get-unsigned-parts uuid) + expected [2711790500, 2981282242, 3520254932, 3587626968]] + + (t/is (instance? js/Uint32Array parts)) + (t/is (= (nth expected 0) (aget parts 0))) + (t/is (= (nth expected 1) (aget parts 1))) + (t/is (= (nth expected 2) (aget parts 2))) + (t/is (= (nth expected 3) (aget parts 3))))))))