Skip to content

Commit

Permalink
BREAKING std/uuid: rework v4 and v5 module (#971)
Browse files Browse the repository at this point in the history
Co-authored-by: William Perron <[email protected]>
  • Loading branch information
lucacasonato and wperron authored Jul 9, 2021
1 parent 52e42d6 commit 0192a85
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 111 deletions.
29 changes: 23 additions & 6 deletions uuid/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
# UUID
# `std/uuid`

Support for version 1, 4, and 5 UUIDs.
Generate and validate v1, v4, and v5 UUIDs.

## Usage
## Examples

### Generate and validate a v4 (random) UUID

```ts
import { v4 } from "https://deno.land/std@$STD_VERSION/uuid/mod.ts";

// Generate a v4 uuid.
const myUUID = v4.generate();
// Generate a v4 UUID. For this we use the browser standard `crypto.randomUUID`
// function.
const myUUID = crypto.randomUUID();

// Validate a v4 uuid.
// Validate the v4 UUID.
const isValid = v4.validate(myUUID);
```

### Generate and validate a v5 (SHA-1 digest) UUID

```ts
import { v5 } from "https://deno.land/std@$STD_VERSION/uuid/mod.ts";

const data = new TextEncoder().encode("Hello World!");

// Generate a v5 UUID using a namespace and some data.
const myUUID = await v5.generate("6ba7b810-9dad-11d1-80b4-00c04fd430c8", data);

// Validate the v5 UUID.
const isValid = v5.validate(myUUID);
```
18 changes: 13 additions & 5 deletions uuid/mod.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

// Based on https://github.com/kelektiv/node-uuid -> https://www.ietf.org/rfc/rfc4122.txt
// Supporting Support for RFC4122 version 1, 4, and 5 UUIDs
// Copyright 2018-2021 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";

/**
* Checks if UUID is nil
* @param val UUID value
* Check if the passed UUID is the nil UUID.
*
* ```js
* import { isNil } from "./mod.ts";
*
* isNil("00000000-0000-0000-0000-000000000000") // true
* isNil(crypto.randomUUID()) // false
* ```
*/
export function isNil(val: string): boolean {
return val === NIL_UUID;
export function isNil(id: string): boolean {
return id === NIL_UUID;
}

export { v1, v4, v5 };
28 changes: 18 additions & 10 deletions uuid/v4.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { bytesToUuid } from "./_common.ts";

const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

/**
* Validates the UUID v4.
* @param id UUID value.
* Validate that the passed UUID is an RFC4122 v4 UUID.
*
* ```ts
* import { validate } from "./v4.ts";
* import { generate as generateV1 } from "./v1.ts";
*
* validate(crypto.randomUUID()); // true
* validate(generateV1() as string); // false
* validate("this-is-not-a-uuid"); // false
* ```
*/
export function validate(id: string): boolean {
return UUID_RE.test(id);
}

/** Generates a RFC4122 v4 UUID (pseudo-randomly-based) */
/**
* @deprecated v4 UUID generation is deprecated and will be removed in a future
* std/uuid release. Use the web standard `globalThis.crypto.randomUUID()`
* function instead.
*
* Generate a RFC4122 v4 UUID (pseudo-random).
*/
export function generate(): string {
const rnds = crypto.getRandomValues(new Uint8Array(16));

rnds[6] = (rnds[6] & 0x0f) | 0x40; // Version 4
rnds[8] = (rnds[8] & 0x3f) | 0x80; // Variant 10

return bytesToUuid(rnds);
return crypto.randomUUID();
}
82 changes: 35 additions & 47 deletions uuid/v5.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,56 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import {
bytesToUuid,
createBuffer,
stringToBytes,
uuidToBytes,
} from "./_common.ts";
import { Sha1 } from "../hash/sha1.ts";
import { bytesToUuid, uuidToBytes } from "./_common.ts";
import { concat } from "../bytes/mod.ts";
import { assert } from "../_util/assert.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;

/**
* Validates the UUID v5.
* @param id UUID value.
* Validate that the passed UUID is an RFC4122 v5 UUID.
*
* ```ts
* import { generate as generateV5, validate } from "./v5.ts";
*
* validate(await generateV5("6ba7b810-9dad-11d1-80b4-00c04fd430c8", new Uint8Array())); // true
* validate(crypto.randomUUID()); // false
* validate("this-is-not-a-uuid"); // false
* ```
*/
export function validate(id: string): boolean {
return UUID_RE.test(id);
}

/** The options used for generating a v5 uuid. */
export interface V5Options {
value: string | number[];
namespace: string | number[];
}

/**
* Generates a RFC4122 v5 UUID (SHA-1 namespace-based).
* @param options Can use a namespace and value to create SHA-1 hash.
* @param buf Can allow the UUID to be written in byte-form starting at the offset.
* @param offset Index to start writing on the UUID bytes in buffer.
* Generate a RFC4122 v5 UUID (SHA-1 namespace).
*
* ```js
* import { generate } from "./v5.ts";
*
* const NAMESPACE_URL = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
*
* const uuid = await generate(NAMESPACE_URL, new TextEncoder().encode("python.org"));
* uuid === "886313e1-3b8a-5372-9b90-0c9aee199e5d" // true
* ```
*
* @param namespace The namespace to use, encoded as a UUID.
* @param data The data to hash to calculate the SHA-1 digest for the UUID.
*/
export function generate(
options: V5Options,
buf?: number[],
offset?: number,
): string | number[] {
const i = (buf && offset) || 0;

let { value, namespace } = options;
if (typeof value === "string") {
value = stringToBytes(value);
}
export async function generate(
namespace: string,
data: Uint8Array,
): Promise<string> {
// TODO(lucacasonato): validate that `namespace` is a valid UUID.

if (typeof namespace === "string") {
namespace = uuidToBytes(namespace);
}
const space = uuidToBytes(namespace);
assert(space.length === 16, "namespace must be a valid UUID");

assert(
namespace.length === 16,
"namespace must be uuid string or an Array of 16 byte values",
);

const content = namespace.concat(value);
const bytes = new Sha1().update(createBuffer(content)).digest();
const toHash = concat(new Uint8Array(space), data);
const buffer = await crypto.subtle.digest("sha-1", toHash);
const bytes = new Uint8Array(buffer);

bytes[6] = (bytes[6] & 0x0f) | 0x50;
bytes[8] = (bytes[8] & 0x3f) | 0x80;

if (buf !== undefined) {
for (let idx = 0; idx < 16; ++idx) {
buf[i + idx] = bytes[idx];
}
}

return buf ?? bytesToUuid(bytes);
return bytesToUuid(bytes);
}
57 changes: 14 additions & 43 deletions uuid/v5_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,32 @@ import { generate, validate } from "./v5.ts";

const NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";

Deno.test("[UUID] test_uuid_v5", () => {
const u = generate({ value: "", namespace: NAMESPACE });
Deno.test("[UUID] test_uuid_v5", async () => {
const u = await generate(NAMESPACE, new Uint8Array());
assertEquals(typeof u, "string", "returns a string");
assert(u !== "", "return string is not empty");
});

Deno.test("[UUID] test_uuid_v5_format", () => {
Deno.test("[UUID] test_uuid_v5_format", async () => {
for (let i = 0; i < 10000; i++) {
const u = generate({ value: i.toString(), namespace: NAMESPACE }) as string;
const u = await generate(
NAMESPACE,
new TextEncoder().encode(i.toString()),
) as string;
assert(validate(u), `${u} is not a valid uuid v5`);
}
});

Deno.test("[UUID] test_uuid_v5_option", () => {
const v5Options = {
value: "Hello, World",
namespace: NAMESPACE,
};
const u = generate(v5Options);
Deno.test("[UUID] test_uuid_v5_option", async () => {
const u = await generate(NAMESPACE, new TextEncoder().encode("Hello, World"));
assertEquals(u, "4b4f2adc-5b27-57b5-8e3a-c4c4bcf94f05");
});

Deno.test("[UUID] test_uuid_v5_buf_offset", () => {
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));
});

Deno.test("[UUID] is_valid_uuid_v5", () => {
const u = generate({
value: "Hello, World",
namespace: "1b671a64-40d5-491e-99b0-da01ff1f3341",
}) as string;
Deno.test("[UUID] is_valid_uuid_v5", async () => {
const u = await generate(
"1b671a64-40d5-491e-99b0-da01ff1f3341",
new TextEncoder().encode("Hello, World"),
);
const t = "4b4f2adc-5b27-57b5-8e3a-c4c4bcf94f05";
const n = "4b4f2adc-5b27-17b5-8e3a-c4c4bcf94f05";

Expand Down

0 comments on commit 0192a85

Please sign in to comment.