Skip to content

Instantly share code, notes, and snippets.

@LBBO
Last active July 31, 2022 08:44
Show Gist options
  • Save LBBO/ea6f6b8dc053225ed171faf050f71331 to your computer and use it in GitHub Desktop.
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.
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)
})
})
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