Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING std/uuid: rework v4 and v5 module #971

Merged
merged 2 commits into from
Jul 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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