Simbol yang tampak sama belum tentu sama: laboratorium Unicode untuk kunci data pasar
Dua label dapat terlihat sama di layar, tetapi terdiri dari code point yang berbeda. Sebaliknya, dua string yang berbeda dapat menjadi sama setelah normalisasi tertentu. Dalam sistem data pasar, perbedaan kecil ini bukan sekadar masalah tipografi: ia dapat memecah satu aliran data menjadi dua, menggabungkan dua entitas secara keliru, atau menyembunyikan karakter format yang tidak terlihat.
Artikel ini membangun model pemeriksaan yang kecil dan dapat direproduksi dengan JavaScript. Seluruh nama penyedia, label, ID, dan rekaman bersifat sintetis. Tidak ada harga nyata, nama bursa, alamat dompet, akun pelanggan, transaksi, atau rekomendasi aset.
Tujuannya juga terbatas. Kita tidak akan membuat “pembersih Unicode” universal. Kita akan memisahkan tiga hal yang sering tercampur:
- raw_label, yaitu string yang diterima dan disimpan sebagai bukti;
- display_label, yaitu bentuk yang telah ditinjau untuk ditampilkan kepada manusia; dan
- canonical_asset_id, yaitu identitas internal dari registri atau pemetaan yang dikelola.
Prinsip utamanya sederhana: label dapat membantu pencarian dan diagnosis, tetapi label tidak boleh menjadi satu-satunya sumber identitas.
Kasus sintetis yang sulit terlihat
Perhatikan dua string berikut:
- LAB-AUR
- LАB-AUR
Pada baris kedua, huruf yang tampak seperti A adalah Cyrillic A, U+0410. Huruf A biasa pada baris pertama adalah Latin A, U+0041. Glyph-nya sangat mirip pada banyak font, tetapi JavaScript memperlakukan kedua string sebagai nilai berbeda.
Contoh lain menyisipkan ZERO WIDTH SPACE, U+200B:
- NUSA-LAB
- NUSA-LAB
Baris kedua tampak hampir sama karena karakter tambahan itu tidak memiliki lebar visual. Namun panjang string, susunan code point, nilai hash, dan kunci Map dapat berbeda.
Contoh-contoh ini bukan tuduhan bahwa sebuah input berbahaya. Nama manusia dan merek yang sah dapat mencampur script atau memakai tanda gabung. Hasil pemeriksaan adalah sinyal untuk ditinjau, bukan vonis.
Mulai dari code point, bukan penampilan
JavaScript mengiterasi string berdasarkan code point ketika kita memakai for...of atau Array.from. Fungsi kecil berikut mengubah setiap karakter menjadi label U+ yang dapat diaudit:
function toCodePoints(value) {
return Array.from(String(value), (character, index) => {
const codePoint = character.codePointAt(0);
return {
index,
character,
codePoint: "U+" + codePoint
.toString(16)
.toUpperCase()
.padStart(4, "0")
};
});
}
console.table(toCodePoints("LАB-AUR"));
Output memperlihatkan U+004C untuk L, U+0410 untuk Cyrillic A, lalu U+0042 untuk B. Pemeriksaan ini lebih kuat daripada meminta operator “melihat lebih teliti”, karena perbedaan justru dapat dirancang agar sulit terlihat.
Untuk antarmuka, karakter tak terlihat sebaiknya diberi label eksplisit seperti ⟨ZERO WIDTH SPACE⟩. Combining mark dapat ditampilkan di atas dotted circle. Dengan begitu, tabel tetap dapat dibaca tanpa bergantung pada warna atau font tertentu.
Empat bentuk normalisasi mempunyai tujuan berbeda
JavaScript menyediakan String.prototype.normalize dengan NFC, NFD, NFKC, dan NFKD.
- NFC membentuk representasi terkomposisi bila tersedia.
- NFD memecah karakter ke bentuk dekomposisi kanonik.
- NFKC juga menerapkan pemetaan kompatibilitas sebelum komposisi.
- NFKD menerapkan pemetaan kompatibilitas dalam bentuk terdekomposisi.
Kita dapat merekam semuanya tanpa menimpa input:
function normalizationReport(value) {
const raw = String(value);
return {
raw,
nfc: raw.normalize("NFC"),
nfd: raw.normalize("NFD"),
nfkc: raw.normalize("NFKC"),
nfkd: raw.normalize("NFKD")
};
}
Pasangan CAFÉ-LAB dan CAFÉ-LAB adalah contoh kanonik. Yang pertama memakai É terkomposisi, sedangkan yang kedua memakai E diikuti COMBINING ACUTE ACCENT. String mentah berbeda, tetapi bentuk NFC-nya sama.
Pasangan RAYA-LAB dan RAYA-LAB menunjukkan efek kompatibilitas. Empat huruf pertama pada string pertama memakai bentuk full-width. NFKC melipatnya menjadi bentuk ASCII sehingga hasil NFKC dapat sama.
Namun NFKC bukan solusi untuk semua karakter mirip. NFKC tidak mengubah Cyrillic А menjadi Latin A. Kedua huruf itu berasal dari script berbeda dan merupakan karakter berbeda, walaupun bentuk visualnya serupa.
Ada konsekuensi desain penting: normalisasi dapat membantu membandingkan, tetapi bentuk normal bukan pengganti identitas bisnis. NFKC juga dapat menghapus perbedaan kompatibilitas yang mungkin bermakna dalam konteks lain. Sistem harus menyimpan nilai mentah dan mendokumentasikan bentuk mana yang dipakai untuk pencarian, tampilan, atau validasi.
Deteksi mixed-script yang konservatif
Unicode property escapes membuat pemeriksaan script dasar dapat dilakukan tanpa dependensi:
function scriptForCharacter(character) {
if (/\p{Script=Latin}/u.test(character)) return "Latin";
if (/\p{Script=Cyrillic}/u.test(character)) return "Cyrillic";
if (/\p{Script=Greek}/u.test(character)) return "Greek";
if (/\p{Script=Inherited}/u.test(character)) return "Inherited";
if (/\p{Script=Common}/u.test(character)) return "Common";
if (/\p{Letter}/u.test(character)) return "Other letter";
return "Common";
}
Kemudian kita mengumpulkan script huruf dan mengabaikan Common serta Inherited saat menentukan mixed-script. Tanda hubung, angka, dan combining mark tidak otomatis menciptakan campuran dua alfabet.
Aturan ini tetap heuristik. Nama yang sah dapat mencampur Latin dengan script lain. Karena itu respons yang aman adalah menandai rekaman untuk peninjauan, bukan menghapus, mengganti, atau menggabungkannya secara otomatis.
Karakter tak terlihat harus terlihat di log
Fixture laboratorium memeriksa lima code point format yang sering relevan untuk diagnosis:
- U+200B ZERO WIDTH SPACE;
- U+200C ZERO WIDTH NON-JOINER;
- U+200D ZERO WIDTH JOINER;
- U+2060 WORD JOINER; dan
- U+FEFF ZERO WIDTH NO-BREAK SPACE.
Daftar ini sengaja kecil. Unicode mempunyai karakter format lain dan beberapa di antaranya sah dalam bahasa tertentu. Sistem produksi memerlukan kebijakan yang disesuaikan dengan medan input, bahasa, dan sumber data.
Yang penting adalah perilaku log. Jangan menulis pesan samar seperti “symbol mismatch”. Catat nilai mentah dalam bentuk aman, code point yang ditemukan, sumber rekaman, waktu penerimaan, aturan yang aktif, dan keputusan yang diambil. Jangan mencatat data pribadi atau rahasia hanya demi diagnosis.
Skeleton demo bukan identitas keamanan
Untuk memperlihatkan konsep confusable, laboratorium memakai skeleton kecil yang hanya memetakan delapan karakter Latin-mirip dari Cyrillic dan Greek. Ia juga memakai NFKC dan mengabaikan lima karakter tak terlihat yang disebutkan tadi.
const demoMap = new Map([
["А", "A"],
["а", "a"],
["О", "O"],
["о", "o"],
["Α", "A"],
["α", "a"],
["Ο", "O"],
["ο", "o"]
]);
function demoSkeleton(value) {
let output = "";
for (const character of String(value).normalize("NFKC")) {
output += demoMap.get(character) ?? character;
}
return output.toUpperCase();
}
Jika dua string mentah berbeda tetapi skeleton demo sama, antarmuka menampilkan “kemungkinan tampak serupa”. Itu hanya demonstrasi. Fungsi ini tidak mengimplementasikan seluruh data confusables Unicode, tidak memahami konteks bahasa, dan tidak layak menjadi kunci database.
Ada dua kegagalan yang perlu dihindari:
- false negative: karakter mirip yang tidak ada dalam delapan pemetaan tidak terdeteksi;
- false positive: dua label sah dianggap mencurigakan hanya karena heuristik kecil menyamakan bentuknya.
Untuk pertahanan yang lebih lengkap, gunakan data dan rekomendasi Unicode yang sesuai, versioning yang jelas, serta peninjauan manusia pada keputusan berisiko. Tetap jangan menjadikan skeleton sebagai canonical_asset_id.
Model data yang mempertahankan jejak
Sebuah rekaman ingest minimal dapat memiliki struktur seperti ini:
{
"provider": "penyedia-lab-beta",
"raw_label": "LАB-AUR",
"display_label": "LАB-AUR",
"canonical_asset_id": "asset:synthetic:002",
"received_at": "2032-04-05T08:15:30.000Z",
"review_state": "mixed-script-review",
"normalization": {
"nfc": "LАB-AUR",
"nfkc": "LАB-AUR"
}
}
canonical_asset_id berasal dari pemetaan penyedia ke registri internal. Bila sebuah penyedia mengganti label tampilan, ID internal tidak ikut berubah tanpa proses migrasi yang eksplisit. Bila dua penyedia memakai label sama untuk entitas berbeda, ID internal tetap berbeda.
Alur ingest yang lebih aman dapat memakai urutan berikut:
- validasi tipe, panjang, dan encoding input;
- simpan raw_label secara immutable atau append-only;
- hitung code point dan bentuk normal untuk observabilitas;
- tandai karakter tak terlihat atau mixed-script menurut kebijakan;
- cari pemetaan berdasarkan namespace penyedia dan ID sumber;
- tahan rekaman ambigu untuk peninjauan;
- tulis canonical_asset_id hanya dari pemetaan yang disetujui;
- simpan alasan keputusan dan versi aturan.
Urutan ini menghindari kesalahan penting: memutuskan identitas hanya karena dua string menjadi sama setelah suatu transformasi.
Pengujian yang membuktikan batas, bukan sekadar jalur bahagia
Paket laboratorium menyertakan sepuluh rekaman sintetis dalam JSON dan modul JavaScript. Dua representasi fixture harus identik. Pengujian Node memeriksa 29 invariant, antara lain:
- Cyrillic A tetap berbeda dari Latin A setelah NFKC;
- bentuk full-width bertemu bentuk ASCII setelah NFKC;
- bentuk aksen terkomposisi dan terdekomposisi bertemu setelah NFC;
- ZERO WIDTH SPACE ditemukan dan diberi label;
- mixed-script menghasilkan status review;
- sepuluh ID rekaman dan sepuluh canonical_asset_id tetap unik;
- fungsi analisis menolak fixture yang tidak lengkap;
- hasil analisis tidak memutasi nilai sumber; dan
- seluruh uji yang juga berjalan di peramban menghasilkan hasil yang sama.
Satu uji penting memastikan dua label mirip tetap mempunyai canonical_asset_id berbeda. Invariant ini menegaskan arsitektur: detektor memberi sinyal, sedangkan registri menentukan identitas.
Jalankan uji lokal dengan Node:
node test/unicode-model.test.mjs
Hasil yang diharapkan:
RESULT: 29 tests passed, 0 failed.
Jika versi runtime tidak mendukung Unicode property escapes, pengujian harus gagal secara terbuka. Jangan mengganti kegagalan itu dengan klasifikasi diam-diam yang tampak berhasil.
Checklist operasional saat alarm muncul
Ketika sistem menemukan label yang berbeda atau karakter tak terlihat, operator dapat mengikuti daftar pendek ini:
- tampilkan code point dan bentuk normal di lingkungan yang aman;
- periksa namespace penyedia serta ID sumber, bukan hanya simbol;
- bandingkan dengan pemetaan registri yang telah disetujui;
- pastikan tidak ada perubahan aturan atau versi Unicode yang baru diterapkan;
- tahan merge otomatis bila identitas belum jelas;
- dokumentasikan keputusan dan contoh sintetis untuk regression test; dan
- hindari menyalin payload sensitif ke tiket atau kanal publik.
Sistem juga harus membedakan “input perlu ditinjau” dari “input berbahaya”. Label yang sah dapat terlihat tidak biasa. Bahasa antarmuka dan log sebaiknya menyatakan fakta yang diamati, misalnya “mengandung Latin dan Cyrillic”, bukan menyimpulkan niat.
Batas laboratorium
Laboratorium ini tidak membuktikan keaslian sumber, kebenaran harga, kesegaran data, kepemilikan aset, atau niat seseorang. Ia tidak mencakup seluruh Unicode confusables, bidirectional controls, segmentasi grapheme, aturan setiap bahasa, atau spoofing berbasis font.
Data uji sepenuhnya fiktif. Hasilnya tidak menilai platform, token, perusahaan, bursa, atau proyek nyata. Pendekatan ini juga bukan nasihat investasi atau kepatuhan.
Untuk rujukan normatif, lihat Unicode Standard Annex #15: Unicode Normalization Forms dan Unicode Technical Report #36: Unicode Security Considerations. Versi standar dan data yang digunakan sistem produksi harus dicatat karena perilaku dapat berubah saat runtime diperbarui.
Ringkasan
Teks yang terlihat sederhana dapat membawa struktur yang berbeda. Pemeriksaan code point, NFC, NFKC, mixed-script, dan karakter tak terlihat membuat perbedaan itu dapat diamati. Namun lapisan tersebut tetap merupakan alat diagnosis.
Desain yang lebih tahan kesalahan menyimpan raw_label, meninjau display_label, dan menetapkan canonical_asset_id melalui pemetaan terkontrol. Dengan pemisahan itu, normalisasi membantu operator tanpa diam-diam menulis ulang bukti atau menggabungkan entitas.
Catatan transparansi: Artikel dan contoh kode ini disusun dengan bantuan AI. Pemeriksaan teknis paket menjalankan 29 uji deterministik; naskah bahasa Indonesia belum ditinjau secara independen oleh penutur asli.
Catatan penulis: Mushegh Manukyan adalah Founder & CEO ARMCP, berbasis di Yerevan, Armenia.