Skip to content

Commit

Permalink
Improve project name generator
Browse files Browse the repository at this point in the history
ref DEV-1612
  • Loading branch information
louischan-oursky committed Aug 26, 2024
2 parents 4ca04a8 + 873adf5 commit 5c08f40
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 55 deletions.
13 changes: 2 additions & 11 deletions portal/src/graphql/portal/CreateProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { ErrorParseRule, makeReasonErrorParseRule } from "../../error/parse";
import { useSimpleForm } from "../../hook/useSimpleForm";
import {
randomProjectName,
getRandom32BitsNumber,
maskNumber,
projectNameWithCompanyName,
} from "../../util/projectname";
import PrimaryButton from "../../PrimaryButton";

Expand Down Expand Up @@ -75,14 +74,6 @@ function processCompanyName(companyName: string): string {
.toLowerCase();
}

function addRandomNumberSuffix(intermediateProjectName: string): string {
return (
intermediateProjectName +
"-" +
maskNumber(getRandom32BitsNumber(), 0, 10).toString()
);
}

function CreateProjectScreenContent(props: CreateProjectScreenContentProps) {
const { numberOfApps } = props;
const navigate = useNavigate();
Expand All @@ -104,7 +95,7 @@ function CreateProjectScreenContent(props: CreateProjectScreenContentProps) {
if (typedState) {
const intermediateName = processCompanyName(typedState.company_name);
if (intermediateName !== "")
defaultState.appID = addRandomNumberSuffix(intermediateName);
defaultState.appID = projectNameWithCompanyName(intermediateName);
}
return defaultState;
}, [state]);
Expand Down
84 changes: 53 additions & 31 deletions portal/src/util/projectname.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,69 @@
import { describe, it, expect } from "@jest/globals";
import { maskNumber, deterministicProjectName } from "./projectname";
import {
deterministicProjectName,
extractBits,
projectNameWithCompanyName,
} from "./projectname";

describe("maskNumber", () => {
it("should mask the number starting from 0 bit and get 11 bits after it", () => {
expect(maskNumber(12345678, 0, 11)).toEqual(334);
describe("extractBits", () => {
it("should handle bytes that look like signed bits", () => {
expect(
extractBits(
new Uint8Array([
0b10000000, 0b10000001, 0b10000010, 0b10000011, 0b10000100,
0b10000101,
])
)
).toEqual([0b10000000100, 0b0000110000010100000111000010010]);
});

it("should mask the number starting from 20 bit and get 5 bits after it", () => {
expect(maskNumber(12345678, 20, 5)).toEqual(11);
it("should handle bytes", () => {
expect(
extractBits(
new Uint8Array([
0b00000000, 0b00000001, 0b00000010, 0b00000011, 0b00000100,
0b00000101,
])
)
).toEqual([0b00000000000, 0b0000100000010000000110000010000]);
});
});

describe("deterministicProjectName", () => {
it("deterministicProjectName(0) is 'abandon-abandon-0'", () => {
// 0 is 0b00000000000_00000000000_0000000000
// 0b00000000000 is abandon
// 0b0000000000 is 0
// So the name is abandon-abandon-0
expect(deterministicProjectName(0)).toEqual("abandon-abandon-0");
it("deterministicProjectName([0, 0, 0, 0, 0, 0]) is 'abandon-000000'", () => {
// [0, 0, 0, 0, 0, 0] is 0b00000000000_0000000000000000000000000000000_000000
// 0b00000000000 is 'abandon'
// 0000000000000000000000000000000 is '000000'
// last 6 bits 000000 is not used
// So the name is 'abandon-000000'
const fortyEightBits = new Uint8Array([0, 0, 0, 0, 0, 0]);
expect(deterministicProjectName(fortyEightBits)).toEqual("abandon-000000");
});

it("deterministicProjectName(1) is ''", () => {
// 1 is 0b00000000000_00000000000_0000000001
// 0b00000000000 is abandon
// 0b0000000001 is 1
// So the name is abandon-abandon-1
expect(deterministicProjectName(1)).toEqual("abandon-abandon-1");
it("deterministicProjectName([0, 0, 0, 0, 1, 0]) is 'abandon-000004'", () => {
// [0, 0, 0, 0, 1, 0] is 0b00000000000_0000000000000000000000000000100_000000
// 0b00000000000 is 'abandon'
// 0000000000000000000000000000100 is '000004'
// last 6 bits 000000 is not used
// So the name is 'abandon-000000'
const fortyEightBits = new Uint8Array([0, 0, 0, 0, 1, 0]);
expect(deterministicProjectName(fortyEightBits)).toEqual("abandon-000004");
});

it("deterministicProjectName(87878787) is ''", () => {
// 87878787 is 0b00000101001_11100111011_0010000011
// 0b00000101001 is ahead
// 0b11100111011 is trash
// 0b0010000011 is 131
// So the name is ahead-trash-131
expect(deterministicProjectName(87878787)).toEqual("ahead-trash-131");
it("deterministicProjectName([87, 87, 87, 87, 87, 87]) is 'firm-pwldul' ", () => {
// [87, 87, 87, 87, 87, 87] is 0b01010111010_1011101010111010101110101011101_010111
// 0b0b01010111010 is 'firm'
// 1011101010111010101110101011101 is 'pwldul'
// last 6 bits 010111 is not used
// So the name is 'firm-000000'
const fortyEightBits = new Uint8Array([87, 87, 87, 87, 87, 87]);
expect(deterministicProjectName(fortyEightBits)).toEqual("firm-pwldul");
});
});

it("deterministicProjectName(4294967295) is ''", () => {
// 4294967295 (2^32 - 1) is 0b11111111111_11111111111_1111111111
// 0b11111111111 is zoo
// 0b1111111111 is 1023
// So the name is zoo-zoo-1023
expect(deterministicProjectName(4294967295)).toEqual("zoo-zoo-1023");
describe("projectNameWithCompanyName", () => {
it("projectNameWithCompanyName('authgear') starts with 'authgear-` and ends with 6 lowercase-alphanumeric characters", () => {
const authgearProjectName = projectNameWithCompanyName("authgear");
expect(authgearProjectName).toMatch(/^authgear-[a-z0-9]{6}$/);
});
});
85 changes: 72 additions & 13 deletions portal/src/util/projectname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,86 @@ function determineWord(index: number): string {
return wordlist[index];
}

export function getRandom32BitsNumber(): number {
const randomBuffer = new Uint32Array(1);
export function getRandom48Bits(): Uint8Array {
const randomBuffer = new Uint8Array(6);
window.crypto.getRandomValues(randomBuffer);
return randomBuffer[0];
return randomBuffer;
}

export function maskNumber(num: number, startAt: number, bits: number): number {
return (num >> startAt) & ((1 << bits) - 1);
/**
* This function take 48 bits, and split into 2 numbers, bit_0_10 and bit_11_41, dropping the last 6 bits
*
* @export
* @param {Uint8Array} fortyEightBits
* @returns {[number, number]}
*/
export function extractBits(fortyEightBits: Uint8Array): [number, number] {
const bit_0_7 = fortyEightBits[0];
const bit_8_15 = fortyEightBits[1];
const bit_16_23 = fortyEightBits[2];
const bit_24_31 = fortyEightBits[3];
const bit_32_39 = fortyEightBits[4];
const bit_40_47 = fortyEightBits[5];

// 11 bits are needed, there are 2^11 = 2048 words in the wordlist
const bit_0_10 = (bit_0_7 << 3) | (bit_8_15 >>> 5);

const bit_11_15 = bit_8_15 & 0b00011111;
const bit_40_41 = bit_40_47 >>> 6;

// 31 bits are needed. We need 6 lowercase-alphanumeric characters.
// There are 36 lowercase-alphanumeric characters in total.
// log36(2^31) = 5.996...
const bit_11_41 =
(bit_11_15 << 26) |
(bit_16_23 << 18) |
(bit_24_31 << 10) |
(bit_32_39 << 2) |
bit_40_41;

return [bit_0_10, bit_11_41];
}

export function deterministicAlphanumericString(bits: number): string {
const string = bits.toString(36);

if (string.length > 6) {
throw new Error("number of bits must be less than 31");
}

const zeroPaddedString = string.padStart(6, "0");
return zeroPaddedString;
}

export function deterministicProjectName(num: number): string {
const randomNumber = maskNumber(num, 0, 10);
const secondRandomStringIndex = maskNumber(num, 10, 11);
const firstRandomStringIndex = maskNumber(num, 21, 11);
/**
* This function take 48 bits
* The 1st - 11th bits bit_0_10 are used to determine the word
* The 12th - 42nd bits bit_11_41 are used to determine the alphanumeric string
* The 43rd - 48th bits bit_42_47 are not used
*
* @export
* @param {Uint8Array} fortyEightBits Uint8Array of length 6
* @returns {string}
*/
export function deterministicProjectName(fortyEightBits: Uint8Array): string {
if (fortyEightBits.length !== 6) {
throw new Error("fortyEightBits must be 6 bytes");
}
const [bit_0_10, bit_11_41] = extractBits(fortyEightBits);
const firstRandomString = determineWord(bit_0_10);

const firstRandomString = determineWord(firstRandomStringIndex);
const secondRandomString = determineWord(secondRandomStringIndex);
const alphaNumericString = deterministicAlphanumericString(bit_11_41);

return `${firstRandomString}-${secondRandomString}-${randomNumber}`;
return `${firstRandomString}-${alphaNumericString}`;
}

export function randomProjectName(): string {
return deterministicProjectName(getRandom32BitsNumber());
return deterministicProjectName(getRandom48Bits());
}

export function projectNameWithCompanyName(companyName: string): string {
const randomBits = getRandom48Bits();
const [_, thirtyOneBits] = extractBits(randomBits);
const alphaNumericString = deterministicAlphanumericString(thirtyOneBits);
return `${companyName}-${alphaNumericString}`;
}

0 comments on commit 5c08f40

Please sign in to comment.