Last active
July 31, 2022 08:44
-
-
Save LBBO/ea6f6b8dc053225ed171faf050f71331 to your computer and use it in GitHub Desktop.
Converts any JavaScript input into valid, executable JavaScript that only consists of '(', ')', '[', ']',, '!', and '+'. This is an update of some code I wrote in 2019(ish) that broke over time due to relying on certain `window` or `global` properties. Now, it relies only on standard JS features and is even covered by jest unit tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
allowedChars, | |
Alphabet, | |
brainfuckify, | |
constants, | |
encodeNumber, | |
getFinalAlphabet, | |
interpret, | |
joinArrays, | |
encodeString, | |
stringOf, | |
updateAvailableCharacters, | |
wrapInParens, | |
} from './brainfuckify' | |
describe('constants', () => { | |
it.each([ | |
['emptyArray', []], | |
['zero', 0], | |
['one', 1], | |
['true', true], | |
['false', false], | |
['trueStr', 'true'], | |
['falseStr', 'false'], | |
['undefined', undefined], | |
['undefinedStr', 'undefined'], | |
['emptyString', ''], | |
] as const)( | |
'should correctly evaluate %p to %p', | |
(key: keyof typeof constants, val) => { | |
expect(interpret(constants[key])).toEqual(val) | |
}, | |
) | |
}) | |
describe('encodeNumber', () => { | |
it.each([0, 1, 2, 3, 50])('should convert %p', (n) => { | |
expect(interpret(encodeNumber(n))).toBe(n) | |
}) | |
}) | |
describe('updateAvailableCharacters', () => { | |
it('should throw an error when code doesnt evaluate to string', () => { | |
expect(() => updateAvailableCharacters({}, constants.false)).toThrowError() | |
}) | |
it('should set characters of true', () => { | |
const newAlphabet = updateAvailableCharacters({}, constants.trueStr) | |
expect(newAlphabet['t']).toBeDefined() | |
expect(newAlphabet['r']).toBeDefined() | |
expect(newAlphabet['u']).toBeDefined() | |
expect(newAlphabet['e']).toBeDefined() | |
expect(interpret(newAlphabet['t'])).toBe('t') | |
expect(interpret(newAlphabet['r'])).toBe('r') | |
expect(interpret(newAlphabet['u'])).toBe('u') | |
expect(interpret(newAlphabet['e'])).toBe('e') | |
}) | |
it('should not replace longer definition', () => { | |
const fakeOne = Array(5).fill(1).reduce(stringOf, constants.one) | |
expect(interpret(fakeOne)).toBe('1') | |
const newAlphabet = updateAvailableCharacters( | |
{ '1': stringOf(constants.one) }, | |
fakeOne, | |
) | |
expect(newAlphabet[1]).toEqual(stringOf(constants.one)) | |
}) | |
it('should handle multiple character inputs', () => { | |
const input = joinArrays( | |
[stringOf(constants.one), wrapInParens(stringOf(constants.zero))], | |
'+', | |
) | |
const expected = '10' | |
expect(interpret(input)).toBe(expected) | |
const alphabet = updateAvailableCharacters({}, input) | |
expect(Object.keys(alphabet)).toContain('0') | |
expect(Object.keys(alphabet)).toContain('1') | |
expect(interpret(alphabet['0'])).toBe('0') | |
expect(interpret(alphabet['1'])).toBe('1') | |
}) | |
}) | |
describe('encodeString', () => { | |
it('should return correct code', () => { | |
const alphabet = updateAvailableCharacters({}, stringOf(constants.one)) | |
const expectedResult = '1111' | |
const result = encodeString(alphabet, expectedResult) | |
expect(Array.isArray(result)).toBe(true) | |
expect(result.find((c) => !allowedChars.includes(c))).toBeUndefined() | |
}) | |
it('should convert a series of strings that are known', () => { | |
const alphabet = updateAvailableCharacters({}, stringOf(constants.one)) | |
const expectedResult = '1111' | |
const result = encodeString(alphabet, expectedResult) | |
expect(interpret(result)).toBe(expectedResult) | |
}) | |
it('should convert a series of different known chars', () => { | |
const alphabet = [stringOf(constants.zero), stringOf(constants.one)].reduce( | |
updateAvailableCharacters, | |
{}, | |
) | |
const expectedResult = '01001011' | |
const result = encodeString(alphabet, expectedResult) | |
expect(interpret(result)).toBe(expectedResult) | |
}) | |
it('should throw an error when an unknown char is used', () => { | |
const alphabet: Alphabet = {} | |
const expectedResult = 'a' | |
expect(() => encodeString(alphabet, expectedResult)).toThrow() | |
}) | |
it('should handle digits without them being in the alphabet', () => { | |
const target = '1234567890' | |
expect(interpret(encodeString({}, target))).toBe(target) | |
}) | |
}) | |
describe('finalAlphabet', () => { | |
it('should compute all values as desired', () => { | |
const finalAlphabet = getFinalAlphabet() | |
Object.keys(finalAlphabet).forEach((char) => { | |
expect(interpret(finalAlphabet[char])).toBe(char) | |
}) | |
}) | |
it('should contain a certain set of chars', () => { | |
const foundChars = Object.keys(getFinalAlphabet()).join('') | |
const expectedChars = [ | |
'true', | |
'false', | |
'undefined', | |
'function entries() { [native code] }', | |
'function Number() { [native code] }', | |
'function String() { [native code] }', | |
'function Object() { [native code] }', | |
"TypeError: Cannot read properties of undefined (reading '1')", | |
"SyntaxError: Unexpected token ')'", | |
] | |
.join('') | |
.split('') | |
expectedChars.forEach((char) => expect(foundChars).toContain(char)) | |
}) | |
it('should produce the characters to get fromCharCode', () => { | |
const alphabet = getFinalAlphabet() | |
expect(() => encodeString(alphabet, 'fromCharCode')).not.toThrow() | |
}) | |
}) | |
describe('brainfuckify', () => { | |
it('should be able to handle any weird string', () => { | |
const target = '😎 🎉' | |
expect(interpret(brainfuckify(`"${target}"`))).toBe(target) | |
}) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const allowedChars = ['(', ')', '[', ']', '!', '+'] as const | |
export type AllowedChar = typeof allowedChars[number] | |
export type EncodedCode = AllowedChar[] | |
export type Alphabet = Record<string, EncodedCode> | |
export const interpret = (code: EncodedCode): unknown => { | |
const executable = code.join('') | |
try { | |
return eval(executable) | |
} catch (e: unknown) { | |
console.error(e) | |
throw new Error(`Code execution failed due to above error. Was trying to run: | |
${executable} | |
`) | |
} | |
} | |
export const joinArrays = <T>(arr: T[][], separator: T): T[] => | |
arr.reduce( | |
(res: T[], digitArr: T[], index): T[] => [ | |
...res, | |
...(index > 0 ? ([separator] as T[]) : []), | |
...digitArr, | |
], | |
[], | |
) | |
export const wrapInParens = (str: EncodedCode): EncodedCode => [ | |
'(', | |
...str, | |
')', | |
] | |
export const indexInto = ( | |
obj: EncodedCode, | |
index: EncodedCode, | |
): EncodedCode => [...obj, '[', ...index, ']'] | |
export const stringOf = (str: EncodedCode): EncodedCode => [ | |
...str, | |
'+', | |
'[', | |
']', | |
] | |
export const call = (func: EncodedCode, arg?: EncodedCode): EncodedCode => [ | |
...func, | |
'(', | |
...(arg ?? []), | |
')', | |
] | |
const unwrappedEmptyArray: EncodedCode = ['[', ']'] | |
const unwrappedFalse: EncodedCode = ['!', '[', ']'] | |
const unwrappedTrue: EncodedCode = ['!', ...unwrappedFalse] | |
const unwrappedZero: EncodedCode = ['+', ...unwrappedEmptyArray] | |
const unwrappedOne: EncodedCode = ['+', ...unwrappedTrue] | |
const trueStr = stringOf(unwrappedTrue) | |
const falseStr = stringOf(unwrappedFalse) | |
const unwrappedUndefined = indexInto(unwrappedEmptyArray, unwrappedZero) | |
const undefinedStr = stringOf(unwrappedUndefined) | |
const emptyString = stringOf(unwrappedEmptyArray) | |
export const constants = { | |
emptyArray: unwrappedEmptyArray, | |
zero: unwrappedZero, | |
one: unwrappedOne, | |
true: unwrappedTrue, | |
false: unwrappedFalse, | |
trueStr: trueStr, | |
falseStr: falseStr, | |
undefined: unwrappedUndefined, | |
undefinedStr: undefinedStr, | |
emptyString, | |
} | |
export const encodeNumber = (num: number): EncodedCode => { | |
if (num % 1 === 0) { | |
if (num === 0) { | |
return unwrappedZero | |
} else if (num === 1) { | |
return unwrappedOne | |
} else if (0 < num) { | |
return joinArrays(Array(num).fill(unwrappedTrue), '+') | |
} | |
} | |
throw new Error('Please only provide positive integers!') | |
} | |
export const encodeString = (alphabet: Alphabet, str: string): EncodedCode => | |
joinArrays( | |
str.split('').map((char) => { | |
if (alphabet[char]) { | |
return alphabet[char] | |
} else if (char.match(/\d/i)) { | |
return wrapInParens(stringOf(encodeNumber(parseInt(char)))) | |
} else { | |
throw new Error(`Character ${char} not found in alphabet!`) | |
} | |
}), | |
'+', | |
) | |
export const updateAvailableCharacters = ( | |
alphabet: Alphabet, | |
interpretableString: EncodedCode, | |
) => { | |
const newValue = interpret(interpretableString) | |
if (typeof newValue !== 'string') { | |
throw new Error(`The provided brainfucked code doesn't result in a string!`) | |
} | |
return newValue.split('').reduce((alphabet, char, index): Alphabet => { | |
const newCommand = wrapInParens( | |
indexInto(wrapInParens(interpretableString), encodeNumber(index)), | |
) | |
const oldCommand = alphabet[char] | |
return { | |
...alphabet, | |
[char]: | |
oldCommand && oldCommand.length < newCommand.length | |
? oldCommand | |
: newCommand, | |
} | |
}, alphabet) | |
} | |
const getConstructor = (alphabet: Alphabet, obj: EncodedCode) => | |
indexInto(wrapInParens(obj), encodeString(alphabet, 'constructor')) | |
const getConstructorString = (alphabet: Alphabet, obj: EncodedCode) => | |
stringOf(getConstructor(alphabet, obj)) | |
const createFunctionFromEncodedBody = ( | |
alphabet: Alphabet, | |
code: EncodedCode, | |
): EncodedCode => { | |
const functionConstructor = getConstructor( | |
alphabet, | |
getConstructor(alphabet, unwrappedEmptyArray), | |
) | |
// calling `Function('<code>')` will return a function with that code as its body | |
const desiredFunction = call(functionConstructor, code) | |
return desiredFunction | |
} | |
const createFunctionFromKnownChars = ( | |
alphabet: Alphabet, | |
code: string, | |
): EncodedCode => | |
createFunctionFromEncodedBody(alphabet, encodeString(alphabet, code)) | |
const encodeAnyValueFromCodeWithKnownChars = ( | |
alphabet: Alphabet, | |
code: string, | |
): EncodedCode => call(createFunctionFromKnownChars(alphabet, `return ${code}`)) | |
const getCharFromBase36Number = ( | |
alphabet: Alphabet, | |
desiredChar: string, | |
): EncodedCode => | |
call( | |
indexInto( | |
wrapInParens( | |
encodeNumber(desiredChar.charCodeAt(0) - 'a'.charCodeAt(0) + 10), | |
), | |
encodeString(alphabet, 'toString'), | |
), | |
encodeNumber(36), | |
) | |
const getErrorMessageFrom = (alphabet: Alphabet, throwingCode: string) => { | |
const err = call( | |
createFunctionFromKnownChars( | |
alphabet, | |
`try{${throwingCode}}catch(e){return e}`, | |
), | |
) | |
return stringOf(err) | |
} | |
export const getFinalAlphabet = () => | |
[ | |
() => falseStr, | |
() => trueStr, | |
() => undefinedStr, | |
// "function entries() { [native code] }" | |
(alphabet: Alphabet): EncodedCode => | |
stringOf( | |
indexInto(unwrappedEmptyArray, encodeString(alphabet, 'entries')), | |
), | |
// "function Number() { [native code] }" | |
(alphabet: Alphabet): EncodedCode => | |
getConstructorString(alphabet, unwrappedOne), | |
// "function String() { [native code] }" | |
(alphabet: Alphabet): EncodedCode => | |
getConstructorString(alphabet, emptyString), | |
// 'function Object() { [native code] }' | |
(alphabet: Alphabet): EncodedCode => | |
stringOf(encodeAnyValueFromCodeWithKnownChars(alphabet, '{}')), | |
// 'h' | |
(alphabet: Alphabet): EncodedCode => getCharFromBase36Number(alphabet, 'h'), | |
// 'y' | |
(alphabet: Alphabet): EncodedCode => getCharFromBase36Number(alphabet, 'y'), | |
// "TypeError: Cannot read properties of undefined (reading '1')" | |
(alphabet: Alphabet): EncodedCode => | |
getErrorMessageFrom(alphabet, '1[1][1]'), | |
// "SyntaxError: Unexpected token ')'" | |
(alphabet: Alphabet): EncodedCode => | |
getErrorMessageFrom(alphabet, "eval(')')"), | |
].reduce( | |
( | |
alphabet: Alphabet, | |
computeNextInterpretableString: (a: Alphabet) => EncodedCode, | |
): Alphabet => | |
updateAvailableCharacters( | |
alphabet, | |
computeNextInterpretableString(alphabet), | |
), | |
{}, | |
) | |
export const brainfuckify = (code: string): EncodedCode => { | |
const alphabet = getFinalAlphabet() | |
const fromCharCode = indexInto( | |
getConstructor(alphabet, emptyString), | |
encodeString(alphabet, 'fromCharCode'), | |
) | |
const fasterGetNumber = (num: number): EncodedCode => | |
encodeAnyValueFromCodeWithKnownChars(alphabet, num.toString()) | |
const encodedCode: EncodedCode = joinArrays( | |
`return ${code}`.split('').map((char): EncodedCode => { | |
try { | |
return encodeString(alphabet, char) | |
} catch { | |
return call(fromCharCode, fasterGetNumber(char.charCodeAt(0))) | |
} | |
}), | |
'+', | |
) | |
const func = createFunctionFromEncodedBody(alphabet, encodedCode) | |
return call(func) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment