diff --git a/std/ws/sha1.ts b/std/util/sha1.ts similarity index 99% rename from std/ws/sha1.ts rename to std/util/sha1.ts index fc86881f87..bb9103d276 100644 --- a/std/ws/sha1.ts +++ b/std/util/sha1.ts @@ -47,9 +47,9 @@ export class Sha1 { this._finalized = this._hashed = false; } - update(data: string | ArrayBuffer | ArrayBufferView): void { + update(data: string | ArrayBuffer | ArrayBufferView): Sha1 { if (this._finalized) { - return; + return this; } let notString = true; let message; @@ -119,6 +119,7 @@ export class Sha1 { this._hBytes += (this._bytes / 4294967296) >>> 0; this._bytes = this._bytes >>> 0; } + return this; } finalize(): void { diff --git a/std/ws/sha1_test.ts b/std/util/sha1_test.ts similarity index 85% rename from std/ws/sha1_test.ts rename to std/util/sha1_test.ts index 8830173a4d..159bdd94de 100644 --- a/std/ws/sha1_test.ts +++ b/std/util/sha1_test.ts @@ -3,20 +3,20 @@ const { test } = Deno; import { assertEquals } from "../testing/asserts.ts"; import { Sha1 } from "./sha1.ts"; -test("[ws/sha] test1", () => { +test("[util/sha] test1", () => { const sha1 = new Sha1(); sha1.update("abcde"); assertEquals(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334"); }); -test("[ws/sha] testWithArray", () => { +test("[util/sha] testWithArray", () => { const data = Uint8Array.of(0x61, 0x62, 0x63, 0x64, 0x65); const sha1 = new Sha1(); sha1.update(data); assertEquals(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334"); }); -test("[ws/sha] testSha1WithBuffer", () => { +test("[util/sha] testSha1WithBuffer", () => { const data = Uint8Array.of(0x61, 0x62, 0x63, 0x64, 0x65); const sha1 = new Sha1(); sha1.update(data.buffer); diff --git a/std/uuid/_common.ts b/std/uuid/_common.ts index b0cad25840..8f13ac7e32 100644 --- a/std/uuid/_common.ts +++ b/std/uuid/_common.ts @@ -13,6 +13,35 @@ export function bytesToUuid(bytes: number[] | Uint8Array): string { "-", ...bits.slice(8, 10), "-", - ...bits.slice(10), + ...bits.slice(10, 16), ].join(""); } + +export function uuidToBytes(uuid: string): number[] { + const bytes: number[] = []; + + uuid.replace(/[a-fA-F0-9]{2}/g, (hex: string): string => { + bytes.push(parseInt(hex, 16)); + return ""; + }); + + return bytes; +} + +export function stringToBytes(str: string): number[] { + str = unescape(encodeURIComponent(str)); + const bytes = new Array(str.length); + for (let i = 0; i < str.length; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes; +} + +export function createBuffer(content: number[]): ArrayBuffer { + const arrayBuffer = new ArrayBuffer(content.length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let i = 0; i < content.length; i++) { + uint8Array[i] = content[i]; + } + return arrayBuffer; +} diff --git a/std/uuid/mod.ts b/std/uuid/mod.ts index 23d0c6410e..7ffbb1349e 100644 --- a/std/uuid/mod.ts +++ b/std/uuid/mod.ts @@ -2,6 +2,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import * as v1 from "./v1.ts"; import * as v4 from "./v4.ts"; +import * as v5 from "./v5.ts"; export const NIL_UUID = "00000000-0000-0000-0000-000000000000"; @@ -21,7 +22,4 @@ const NOT_IMPLEMENTED = { // TODO Implement export const v3 = NOT_IMPLEMENTED; -export { v1, v4 }; - -// TODO Implement -export const v5 = NOT_IMPLEMENTED; +export { v1, v4, v5 }; diff --git a/std/uuid/test.ts b/std/uuid/test.ts index 74d3c093d6..58eae2fff8 100755 --- a/std/uuid/test.ts +++ b/std/uuid/test.ts @@ -11,3 +11,7 @@ import "./tests/v1/generate.ts"; // V4 Tests import "./tests/v4/validate.ts"; import "./tests/v4/generate.ts"; + +// V5 Tests +import "./tests/v5/validate.ts"; +import "./tests/v5/generate.ts"; diff --git a/std/uuid/tests/v5/generate.ts b/std/uuid/tests/v5/generate.ts new file mode 100644 index 0000000000..c869ef505c --- /dev/null +++ b/std/uuid/tests/v5/generate.ts @@ -0,0 +1,66 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { assert, assertEquals } from "../../../testing/asserts.ts"; +const { test } = Deno; +import { generate, validate } from "../../v5.ts"; +const NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341"; +test({ + name: "[UUID] test_uuid_v5", + fn(): void { + const u = generate({ value: "", namespace: NAMESPACE }); + assertEquals(typeof u, "string", "returns a string"); + assert(u !== "", "return string is not empty"); + }, +}); + +test({ + name: "[UUID] test_uuid_v5_format", + fn(): void { + for (let i = 0; i < 10000; i++) { + const u = generate({ value: String(i), namespace: NAMESPACE }) as string; + assert(validate(u), `${u} is not a valid uuid v5`); + } + }, +}); + +test({ + name: "[UUID] test_uuid_v5_option", + fn(): void { + const v5Options = { + value: "Hello, World", + namespace: NAMESPACE, + }; + const u = generate(v5Options); + assertEquals(u, "4b4f2adc-5b27-57b5-8e3a-c4c4bcf94f05"); + }, +}); + +test({ + name: "[UUID] test_uuid_v5_buf_offset", + fn(): void { + const buf = [ + 75, + 79, + 42, + 220, + 91, + 39, + 87, + 181, + 142, + 58, + 196, + 196, + 188, + 249, + 79, + 5, + ]; + const origin = JSON.parse(JSON.stringify(buf)); + generate({ value: "Hello, World", namespace: NAMESPACE }, buf); + assertEquals(origin, buf); + + generate({ value: "Hello, World", namespace: NAMESPACE }, buf, 3); + assertEquals(origin.slice(0, 3), buf.slice(0, 3)); + assertEquals(origin, buf.slice(3)); + }, +}); diff --git a/std/uuid/tests/v5/validate.ts b/std/uuid/tests/v5/validate.ts new file mode 100644 index 0000000000..888acb1c59 --- /dev/null +++ b/std/uuid/tests/v5/validate.ts @@ -0,0 +1,19 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { assert } from "../../../testing/asserts.ts"; +import { generate, validate } from "../../v5.ts"; + +Deno.test({ + name: "[UUID] is_valid_uuid_v5", + fn(): void { + const u = generate({ + value: "Hello, World", + namespace: "1b671a64-40d5-491e-99b0-da01ff1f3341", + }) as string; + const t = "4b4f2adc-5b27-57b5-8e3a-c4c4bcf94f05"; + const n = "4b4f2adc-5b27-17b5-8e3a-c4c4bcf94f05"; + + assert(validate(u), `generated ${u} should be valid`); + assert(validate(t), `${t} should be valid`); + assert(!validate(n), `${n} should not be valid`); + }, +}); diff --git a/std/uuid/v5.ts b/std/uuid/v5.ts new file mode 100644 index 0000000000..1216762dde --- /dev/null +++ b/std/uuid/v5.ts @@ -0,0 +1,52 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { + bytesToUuid, + createBuffer, + stringToBytes, + uuidToBytes, +} from "./_common.ts"; +import { Sha1 } from "../util/sha1.ts"; +import { isString } from "../node/util.ts"; +import { assert } from "../testing/asserts.ts"; + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +export function validate(id: string): boolean { + return UUID_RE.test(id); +} + +interface V5Options { + value: string | number[]; + namespace: string | number[]; +} + +export function generate( + options: V5Options, + buf?: number[], + offset?: number +): string | number[] { + const i = (buf && offset) || 0; + + let { value, namespace } = options; + if (isString(value)) value = stringToBytes(value as string); + if (isString(namespace)) namespace = uuidToBytes(namespace as string); + assert( + namespace.length === 16, + "namespace must be uuid string or an Array of 16 byte values" + ); + + const content = (namespace as number[]).concat(value as number[]); + const bytes = new Sha1().update(createBuffer(content)).digest(); + + bytes[6] = (bytes[6] & 0x0f) | 0x50; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + if (buf) { + for (let idx = 0; idx < 16; ++idx) { + buf[i + idx] = bytes[idx]; + } + } + + return buf || bytesToUuid(bytes); +} diff --git a/std/ws/mod.ts b/std/ws/mod.ts index 87084312fd..9632fbbfb0 100644 --- a/std/ws/mod.ts +++ b/std/ws/mod.ts @@ -4,7 +4,7 @@ import { decode, encode } from "../encoding/utf8.ts"; import { hasOwnProperty } from "../util/has_own_property.ts"; import { BufReader, BufWriter } from "../io/bufio.ts"; import { readLong, readShort, sliceLongToBytes } from "../io/ioutil.ts"; -import { Sha1 } from "./sha1.ts"; +import { Sha1 } from "../util/sha1.ts"; import { writeResponse } from "../http/io.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { Deferred, deferred } from "../util/async.ts";