import assert from "assert"; import { getCRCHex, partition, split } from "./utils"; const bytewords = 'ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlimplionlistlogoloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolposepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzinczonezoom'; let bytewordsLookUpTable: number[] = []; const BYTEWORDS_NUM = 256; const BYTEWORD_LENGTH = 4; const MINIMAL_BYTEWORD_LENGTH = 2; enum STYLES { STANDARD = 'standard', URI = 'uri', MINIMAL = 'minimal' } const getWord = (index: number): string => { return bytewords.slice(index * BYTEWORD_LENGTH, (index * BYTEWORD_LENGTH) + BYTEWORD_LENGTH) } const getMinimalWord = (index: number): string => { const byteword = getWord(index); return `${byteword[0]}${byteword[BYTEWORD_LENGTH - 1]}` } const addCRC = (string: string): string => { const crc = getCRCHex(Buffer.from(string, 'hex')); return `${string}${crc}`; } const encodeWithSeparator = (word: string, separator: string): string => { const crcAppendedWord = addCRC(word); const crcWordBuff = Buffer.from(crcAppendedWord, 'hex'); const result = crcWordBuff.reduce((result: string[], w) => ([...result, getWord(w)]), []); return result.join(separator); } const encodeMinimal = (word: string): string => { const crcAppendedWord = addCRC(word); const crcWordBuff = Buffer.from(crcAppendedWord, 'hex'); const result = crcWordBuff.reduce((result, w) => result + getMinimalWord(w), ''); return result; } const decodeWord = (word: string, wordLength: number): string => { assert(word.length === wordLength, 'Invalid Bytewords: word.length does not match wordLength provided'); const dim = 26; // Since the first and last letters of each Byteword are unique, // we can use them as indexes into a two-dimensional lookup table. // This table is generated lazily. if (bytewordsLookUpTable.length === 0) { const array_len = dim * dim; bytewordsLookUpTable = [...new Array(array_len)].map(() => -1) for (let i = 0; i < BYTEWORDS_NUM; i++) { const byteword = getWord(i); let x = byteword[0].charCodeAt(0) - 'a'.charCodeAt(0); let y = byteword[3].charCodeAt(0) - 'a'.charCodeAt(0); let offset = y * dim + x; bytewordsLookUpTable[offset] = i; } } // If the coordinates generated by the first and last letters are out of bounds, // or the lookup table contains -1 at the coordinates, then the word is not valid. let x = (word[0]).toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0); let y = (word[wordLength == 4 ? 3 : 1]).toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0); assert(0 <= x && x < dim && 0 <= y && y < dim, 'Invalid Bytewords: invalid word'); let offset = y * dim + x; let value = bytewordsLookUpTable[offset]; assert(value !== -1, 'Invalid Bytewords: value not in lookup table'); // If we're decoding a full four-letter word, verify that the two middle letters are correct. if (wordLength == BYTEWORD_LENGTH) { const byteword = getWord(value) let c1 = word[1].toLowerCase(); let c2 = word[2].toLowerCase(); assert(c1 === byteword[1] && c2 === byteword[2], 'Invalid Bytewords: invalid middle letters of word'); } // Successful decode. return Buffer.from([value]).toString('hex') } const _decode = (string: string, separator: string, wordLength: number): string => { const words = wordLength == BYTEWORD_LENGTH ? string.split(separator) : partition(string, 2) const decodedString = words.map((word: string) => decodeWord(word, wordLength)).join(''); assert(decodedString.length >= 5, 'Invalid Bytewords: invalid decoded string length'); const [body, bodyChecksum] = split(Buffer.from(decodedString, 'hex'), 4) const checksum = getCRCHex(body)// convert to hex assert(checksum === bodyChecksum.toString('hex'), 'Invalid Checksum'); return body.toString('hex'); } const decode = (string: string, style: STYLES = STYLES.MINIMAL): string => { switch (style) { case STYLES.STANDARD: return _decode(string, ' ', BYTEWORD_LENGTH); case STYLES.URI: return _decode(string, '-', BYTEWORD_LENGTH); case STYLES.MINIMAL: return _decode(string, '', MINIMAL_BYTEWORD_LENGTH); default: throw new Error(`Invalid style ${style}`) } } const encode = (string: string, style: STYLES = STYLES.MINIMAL): string => { switch (style) { case STYLES.STANDARD: return encodeWithSeparator(string, ' '); case STYLES.URI: return encodeWithSeparator(string, '-'); case STYLES.MINIMAL: return encodeMinimal(string); default: throw new Error(`Invalid style ${style}`) } } export default { decode, encode, STYLES }