チーム向けのチャットボットAIを提供しているGlama.aiのエンジニアであるフランク・フィーゲル氏が、容量削減のために変数名を短縮し、改行や空白を消す「Minify」が行われたJavaScriptのコードをChatGPTを使って読みやすい形式に戻すことに成功したと報告しています。

Using ChatGPT to reverse engineer minified JavaScript - ChatGPT for teams | Glama

https://glama.ai/blog/2024-08-29-reverse-engineering-minified-code-using-openai



フィーゲル氏が最初に注目したのは、以下のサイトで背景に表示されているアニメーションでした。このアニメーションは文字で構成されたアスキーアートが波打って目まぐるしく変化していくもので、フィーゲル氏はどのようにしてこのアニメーションをサイトに実装しているのかが気になったとのこと。

Reactive Network

https://reactive.network/hackathon



そして、調査の結果、以下のコードがアニメーションを構成していると判明。

const { floor: ra, abs: KE, min: QE } = Math,
O5 = ["reactive.network REACTIVE.NETWORK", "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,^`'. .:â-‘â-’â-“â-ˆ"],
G7 = Date.now() % 3 ? O5[1] : O5[0],
V5 = G7.length,
JE = { fps: 60 };
function eT(e, t, n, r) {
const i = t.time * 8e-5,
s = QE(t.cols, t.rows),
o = t.metrics.aspect * 0.2,
l = { x: ((4 * (e.x - t.cols / 6.25)) / s) * o, y: (5 * (e.y - t.rows / 4)) / s },
u = ra(KE(YE(l) - i) * V5 + (ra(e.x / 1) % 2) * 2) % V5;
return G7[u];
}
const tT = () => {
const e = j.useRef(null),
[t, n] = j.useState({ height: null, width: null });
return (
j.useEffect(() => {
function r() {
n({ height: window.innerHeight, width: window.innerWidth });
}
if (typeof window < "u") return n({ height: window.innerHeight, width: window.innerWidth }), window.addEventListener("resize", r), () => window.removeEventListener("resize", r);
}, []),
j.useEffect(() => {
const r = e.current;
if (!r) return;
const i = 12,
s = ra(t.width / i) * 1.6,
o = ra(t.height / i),
l = { aspect: s / o },
u = setInterval(() => {
let c = "";
for (let d = 0; d < o; d++) {
for (let f = 0; f < s; f++) c += eT({ x: f, y: d }, { cols: s, rows: o, metrics: l, time: Date.now() });
c += `
`;
}
r.textContent = c;
}, 1e3 / JE.fps);
return () => clearInterval(u);
}, [t]),
a.jsx("div", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }, children: a.jsx("div", { ref: e, style: { width: "100%", height: "100%", whiteSpace: "pre", overflow: "hidden" } }) })
);
};
function nT(e) {
return Math.cos(e.x * e.x - e.y * e.y);
}
const { floor: ia, abs: rT, min: iT } = Math,
D5 = ["reactive.network REACTIVE.NETWORK", "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,^`'. .:â-‘â-’â-“â-ˆ"],
X7 = Date.now() % 3 ? D5[1] : D5[0],
F5 = X7.length,
sT = { fps: 60 };
function oT(e, t, n, r) {
const i = t.time * 8e-5,
s = iT(t.cols, t.rows),
o = t.metrics.aspect * 0.2,
l = { x: ((4 * (e.x - t.cols / 6.25)) / s) * o, y: (5 * (e.y - t.rows / 4)) / s },
u = ia(rT(nT(l) - i) * F5 + (ia(e.x / 1) % 2) * 2) % F5;
return X7[u];
}
const aT = () => {
const e = j.useRef(null),
[t, n] = j.useState({ height: null, width: null });
return (
j.useEffect(() => {
function r() {
n({ height: window.innerHeight, width: window.innerWidth });
}
if (typeof window < "u") return n({ height: window.innerHeight, width: window.innerWidth }), window.addEventListener("resize", r), () => window.removeEventListener("resize", r);
}, []),
j.useEffect(() => {
const r = e.current;
if (!r) return;
const i = 12,
s = ia(t.width / i) * 1.6,
o = ia(t.height / i),
l = { aspect: s / o },
u = setInterval(() => {
let c = "";
for (let d = 0; d < o; d++) {
for (let f = 0; f < s; f++) c += oT({ x: f, y: d }, { cols: s, rows: o, metrics: l, time: Date.now() });
c += `
`;
}
r.textContent = c;
}, 1e3 / sT.fps);
return () => clearInterval(u);
}, [t]),
a.jsx("div", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }, children: a.jsx("div", { ref: e, style: { width: "100%", height: "100%", whiteSpace: "pre", overflow: "hidden" } }) })
);
},

しかし、このコードはMinifyされていて可読性が低かったため、フィーゲル氏はChatGPTにコードの説明を依頼しました。

ChatGPTは「提供されているコードは非常に複雑ですが、より理解しやすい形式に分解して、各部分とその機能について説明します。このコードはReactアプリケーションのもので、JavaScriptを使っていくつかの関数とコンポーネントを定義し、動的なテキストコンテンツ(おそらくアスキーアート、あるいは定義済みセットの文字を使った何らかの視覚的効果)の生成を行います。さらにイベント処理、パフォーマンスタイミング、HTMLコンテンツ変換用のユーティリティ関数も含まれています」と回答。そして、コードの主要な構成要素として、以下の3つに分解して解説しています。

1:文字セットの選択

const { floor: ra, abs: KE, min: QE } = Math;
const O5 = ["reactive.network REACTIVE.NETWORK", "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,^`'. .:â-‘â-’â-“â-ˆ"];
const G7 = Date.now() % 3 ? O5[1] : O5[0];
const V5 = G7.length;

この部分は後続のコードで使用される文字セットを定義しています。これにより、生成される視覚効果のバリエーションを増やしたり、異なる外観を作り出したりすることが可能になります。また、時間に基づいて文字セットを選択することで実行ごとに異なる結果を得るようにして、動的な要素を加えています。

2:動的に文字を選択する関数(eT関数)について

function eT(e, t, n, r) {
const i = t.time * 8e-5;
const s = QE(t.cols, t.rows);
const o = t.metrics.aspect * 0.2;
const l = { x: ((4 * (e.x - t.cols / 6.25)) / s) * o, y: (5 * (e.y - t.rows / 4)) / s };
const u = ra(KE(YE(l) - i) * V5 + (ra(e.x / 1) % 2) * 2) % V5;
return G7[u];
}

この関数は、入力された位置(x、y座標)と現在の時間に基づいて、文字セットから特定の文字を選択します。位置や時間に応じて異なる文字を選択することで、動的で変化するパターンを生成します。

3:動的コンテンツを表示するためのReactコンポーネント(tT)の構造と機能

const tT = () => {
const e = j.useRef(null);
const [t, n] = j.useState({ height: null, width: null });
j.useEffect(() => {
function r() {
n({ height: window.innerHeight, width: window.innerWidth });
}
if (typeof window < "u") {
n({ height: window.innerHeight, width: window.innerWidth });
window.addEventListener("resize", r);
return () => window.removeEventListener("resize", r);
}
}, []);

j.useEffect(() => {
const r = e.current;
if (!r) return;
const i = 12;
const s = ra(t.width / i) * 1.6;
const o = ra(t.height / i);
const l = { aspect: s / o };
const u = setInterval(() => {
let c = "";
for (let d = 0; d < o; d++) {
for (let f = 0; f < s; f++) c += eT({ x: f, y: d }, { cols: s, rows: o, metrics: l, time: Date.now() });
c += `
`;
}
r.textContent = c;
}, 1e3 / JE.fps);
return () => clearInterval(u);
}, [t]);

return a.jsx("div", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }, children: a.jsx("div", { ref: e, style: { width: "100%", height: "100%", whiteSpace: "pre", overflow: "hidden" } }) });
};

このコンポーネントは、ウィンドウサイズの変更に対応しつつ、高速で更新される動的なテキストベースのビジュアル効果を生成・表示する役割を果たしています。eT関数と組み合わせることで、時間とともに変化する複雑なパターンを作り出しています。

そして、フィーゲル氏が「TypeScriptで同等のコードを実装し、人間が読めるようにしてください」と頼んだところ、ChatGPTは以下のコードを出力しました。

import React, { useEffect, useRef, useState } from 'react';

const selectedCharacterSet =
"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,^`'. .:â-‘â-’â-“â-ˆ";
const characterSetLength = selectedCharacterSet.length;

const calculateCharacter = (
x: number,
y: number,
cols: number,
rows: number,
aspect: number,
time: number,
): string => {
const timeFactor = time * 0.000_08;
const size = Math.min(cols, rows);
const aspectRatio = aspect * 0.2;
const position = {
x: ((4 * (x - cols / 6.25)) / size) * aspectRatio,
y: (5 * (y - rows / 4)) / size,
};

const index =
Math.floor(
Math.abs(
Math.cos(position.x * position.x - position.y * position.y) -
timeFactor,
) *
characterSetLength +
(Math.floor(x) % 2) * 2,
) % characterSetLength;
return selectedCharacterSet[index];
};

type Size = {
height: null | number;
width: null | number;
};

const AsciiArtGenerator: React.FC = () => {
const textRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState<Size>({ height: null, width: null });

useEffect(() => {
const handleResize = () => {
setSize({ height: window.innerHeight, width: window.innerWidth });
};

// Initial size setting
handleResize();

window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

useEffect(() => {
const element = textRef.current;
if (!element || !size.width || !size.height) return undefined;

const cols = Math.floor(size.width / 12) * 1.6;
const rows = Math.floor(size.height / 12);
const aspectRatio = cols / rows;

const intervalId = setInterval(() => {
let content = '';

for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
content += calculateCharacter(
x,
y,
cols,
rows,
aspectRatio,
Date.now(),
);
}

content += '
';
}

element.textContent = content;
}, 1_000 / 60); // 60 fps

return () => {
clearInterval(intervalId);
};
}, [size]);

return (
<div
ref={textRef}
style={{
height: '100%',
overflow: 'hidden',
whiteSpace: 'pre',
width: '100%',
}}
/>
);
};

export default () => {
return <AsciiArtGenerator />;
};

フィーゲル氏が実際にこのコードを試してみたところ、可読性が担保されながら、出力結果はオリジナルのアニメーションとほぼ同じものだったとのこと。



実際にはいくつかの違いがあったものの、その後にソーシャルニュースサイトのHacker Newsで「文字コードの関係でコードのコピー&ペーストにミスがある」と指摘され、修正したことでオリジナルと同一のアニメーションが出力されたそうです。