בוחן תווי יוניקוד
מפרק טקסט לנקודות הקוד שלו עם שמות, קטגוריות, בלוקים וקידודים, ומסמן תווים בלתי נראים, פקדות bidi ותווים דומים.
מה הכלי הזה עושה
הדביקו טקסט כלשהו והוא יחזור מפורק לתווים הבודדים שמהם הוא באמת בנוי. כל אחד מהם מקבל את שמו ביוניקוד, את מספר ה-U+ שלו, איזה סוג תו הוא, לאיזו מערכת כתב ולאיזה בלוק הוא שייך, כמה בייטים הוא תופס ב-UTF-8 וב-UTF-16, ואיזו צורת הבריחה הוא דורש בשישה פורמטים שונים.
רוב האנשים מגיעים לכאן כי משהו לא מסתדר. מחרוזת ארוכה ממה שהיא נראית, שני ערכים שנראים זהים מסרבים להיות שווים, שם משתמש נדחה בגלל תווים שנראים כמו אותיות רגילות, או ששורת קוד נקראת אחרת ממה שהיא מתנהגת. לארבעתם יש אותה צורה: תו שעושה משהו שהמסך לא מראה.
תווים, נקודות קוד וגרפימות
למילה «תו» יש לפחות שלוש משמעויות שונות, ובלבול ביניהן הוא המקור לרוב הבלבול סביב יוניקוד. נקודת קוד היא ערך אחד בתקן יוניקוד, שנכתב U+0041 או U+1F600. גרפימה היא מה שקורא סופר כתו אחד. לעיתים קרובות אלה לא אותם מספרים.
- «a» רגילה היא גרפימה אחת, נקודת קוד אחת ויחידת UTF-16 אחת — הכול מסכים.
- U+00E9, ה-é המורכבת מראש, היא גרפימה אחת, נקודת קוד אחת ויחידת UTF-16 אחת.
- U+0065 U+0301, אותה אות בדיוק שנכתבה כ-e ועוד סימן צירוף, היא גרפימה אחת אבל שתי נקודות קוד ושתי יחידות UTF-16.
- U+1F600, הפרצוף המחייך, הוא גרפימה אחת ונקודת קוד אחת אבל שתי יחידות UTF-16 — הוא חי מעל ה-BMP, ולכן JavaScript מאחסן אותו כזוג סורוגייטים.
- אמוג׳י המשפחה הוא גרפימה אחת, חמש נקודות קוד ושמונה יחידות UTF-16: שלושה אנשים המחוברים בשני מחברים בלתי נראים.
אמוג׳י המשפחה הוא המקרה שמפיל אנשים. הוא נראה כמו דבר אחד, JavaScript מדווח על אורך 8, ועמודה במסד נתונים שמחזיקה 5 תווים לא תכיל אותו. הכלי הזה מקבץ נקודות קוד תחת הגרפימה שהן בונות, כך ששתי הקריאות נראות בבת אחת.
התווים שאי אפשר לראות
מספר מפתיע של תווי יוניקוד לא מציירים דבר. הם קיימים כדי לשלוט באופן שבו הטקסט סביבם מתנהג, ומכיוון שהם לא משאירים סימן הם שורדים העתקה, הדבקה וסקירת קוד בלי שאיש ישים לב. אלה אלה שהכלי מצביע עליהם ולא רק מונה אותם:
- רווח באפס רוחב, מחבר ולא-מחבר (U+200B, U+200D, U+200C) — בלי רוחב, בלי סימן, והם שוברים כל השוואת מחרוזות שהם נוגעים בה.
- מקף רך (U+00AD) — הצעה היכן מותר לשבור מילה, בלתי נראה עד שזה קורה.
- רווח לא-שובר (U+00A0) — נראה בדיוק כמו רווח והוא תו אחר, ולכן הוא שורד כל כך הרבה זמן בקובצי הגדרות.
- סימן סדר בייטים (U+FEFF) — נשאר לעיתים קרובות בתחילת קובץ, שם הוא הופך לתו ראשון בלתי נראה של הערך הראשון.
- פקדות והיפוכי bidi (מ-U+202A עד U+202E, ומ-U+2066 עד U+2069) — הן מסדרות מחדש את הטקסט סביבן.
פקדות ה-bidi ראויות להזכרה נפרדת. הן נוספו כדי שאפשר יהיה לערבב עברית וערבית עם טקסט לטיני כראוי, והן עושות את העבודה הזאת היטב. אבל היפוך שהונח בתוך הערה או בתוך מחרוזת יכול לגרום לשורת קוד להיראות בסדר שונה לגמרי מהסדר שהמהדר קורא — כלומר סוקר יכול לאשר קוד שעושה משהו אחר לגמרי. סוג התרגיל הזה מוכר בשם Trojan Source, וזו הסיבה שהכלי מסמן כל פקדת bidi בנפרד משאר התווים הבלתי נראים.
תווים דומים וערבוב כתבים
ביוניקוד יש תווים רבים הזהים חזותית לאותיות ASCII ואינם הן. а קירילית, ο יוונית ו-a לטינית תופסות נקודות קוד שונות ונראות אותו דבר ברוב הגופנים. דומיין או שם משתמש הבנויים מהן נראים נכונים ומצביעים למקום אחר.
לסמן כל תו לא-ASCII שדומה לאחד היה חסר תועלת: זה היה צובע כל מילה רוסית, יוונית או עברית באדום, ואזהרה שנדלקת על טקסט רגיל היא אזהרה שאנשים לומדים לדלג עליה. לכן הכלל כאן הוא זה שתקן האבטחה של יוניקוד משתמש בו — מילה חשודה כשהאותיות בתוכה מגיעות מיותר ממערכת כתב אחת:
- «paypal.com» כתוב כולו באותיות לטיניות: אין מה לומר עליו.
- אותה מילה עם ר׳ ו-א׳ קיריליות לפני «ypal» הלטיני: מילה אחת, שתי מערכות כתב, מסומן.
- מילה רוסית שלמה בקירילית: מערכת כתב אחת, טקסט רגיל, לא מסומן.
- משפט יפני המשתמש בהאן, בהיראגנה ובקטקאנה יחד: שלושה כתבים, מערכת כתב אחת, לא מסומן.
המקרה האחרון הוא הסיבה שהכלל דורש זהירות. יפנית נכתבת בשלושה כתבים בבת אחת וקוריאנית מערבבת שניים, ולכן מבחן נאיבי של «יותר מכתב אחד הוא חשוד» היה מכריז על כל משפט יפני כמתקפה. הצירופים שהם באמת מערכת כתב אחת מטופלים כאחת, וזה מה ששומר על האזהרה משמעותית.
נורמליזציה, או למה שתי מחרוזות זהות שונות
יש תווים שאפשר לכתוב ביותר מדרך אחת. האות é היא או נקודת הקוד היחידה U+00E9 או הזוג U+0065 U+0301 — e רגילה ואחריה סימן צירוף של אקוט. שתיהן נראות זהות. אף אחת אינה שגויה. הן אינן שוות.
נורמליזציה היא התהליך של בחירת כתיב אחד, ויש לה ארבע צורות. NFC מרכיבה היכן שאפשר והיא מה שהרשת מניחה כברירת מחדל; NFD מפרקת במקום. שתי צורות ה-K הולכות רחוק יותר ומקפלות גם תווי תאימות — A ברוחב מלא הופכת ל-A רגילה, והליגטורה fi הופכת ל-fi — מה ששימושי לחיפוש ולהתאמה ומאבד מידע באחסון.
הפאנל כאן מציג את כל ארבע הצורות של מה שהדבקתם ומסמן אילו מהן שונות מהקלט. אם אף אחת אינה שונה, הטקסט כבר מנורמל והאי-התאמה שאתם רודפים אחריה נמצאת במקום אחר. אם חלקן כן, מצאתם אותה.
שמות, קטגוריות, כתבים ובלוקים
לכל תו מוקצה יש ארבעה פריטי זהות, והם עונים על שאלות שונות. השם הוא התו עצמו. הקטגוריה הכללית אומרת איזה סוג דבר הוא — אות קטנה, סימן צירוף, סמל מטבע, פקדת עיצוב. הכתב הוא מערכת הכתיבה שאליה הוא שייך. הבלוק הוא טווח נקודות הקוד שבו הוא הוקצה, וזו עובדה ארגונית ולא לשונית.
קל לבלבל בין בלוק לכתב והם חולקים לעיתים קרובות. אותיות לטיניות חיות בלפחות חצי תריסר בלוקים, ובלוק אחד יכול להכיל תווים מכמה כתבים. כשרוצים לדעת «איזו אלפבית זו», הכתב הוא התשובה; כשרוצים לדעת «איפה בתקן זה יושב», הבלוק הוא.
שמות, כתבים ובלוקים מוצגים כאן באנגלית בכל שפה, במכוון. אלה מזהים שהתקן מגדיר, לא פרוזה — U+200D נקרא ZERO WIDTH JOINER בכל העולם, ותרגום שלו היה הופך אותו לבלתי אפשרי לחיפוש.
קידודים והבריחות
נקודת קוד היא מספר; היא הופכת לבייטים רק ברגע שנבחר קידוד. UTF-8 משתמש בין בייט אחד לארבעה ומשאיר את ASCII ללא שינוי, ולכן הוא ניצח. UTF-16 משתמש ביחידה אחת או שתיים של 16 ביט, והוא מה שמחרוזות JavaScript עשויות ממנו — ולכן .length מדווח 2 על אמוג׳י יחיד.
U+0041 41 # בייט אחד, זהה ל-ASCII U+00E9 C3 A9 # שני בייטים U+20AC E2 82 AC # שלושה בייטים U+1F600 F0 9F 98 80 # ארבעה בייטים
כל שורה כאן נותנת גם את התו כתוב כהבריחה ב-JavaScript, ב-JSON, ב-HTML, ב-CSS, בכתובות URL וב-Python, מוכן להעתקה. אלה אינם זהים, וההבדל חשוב מעל ה-BMP: ל-JavaScript ול-Python יש הבריחה לפי נקודת קוד והם משתמשים בה, ואילו ל-JSON אין ולכן היא חייבת לאיית אמוג׳י כשני חצאי הסורוגייט שלו.
שאלות נפוצות
- למה המחרוזת שלי ארוכה ממספר התווים שאני רואה?
- או שהיא מכילה תווים בלתי נראים, או שהיא מכילה גרפימות הבנויות מכמה נקודות קוד. שניהם מוצגים כאן: תווים בלתי נראים מסומנים, וגרפימות מרובות נקודות קוד מקובצות כך שאפשר לראות את התו האחד ואת הערכים האחדים שהוא בנוי מהם.
- שתי מחרוזות נראות זהות ואינן שוות. למה?
- בדרך כלל נורמליזציה. אות עם ניקוד יכולה להיות נקודת קוד אחת או אות ועוד סימן צירוף, ושתיהן נראות אותו דבר. הדביקו כל מחרוזת כאן והשוו את רשימות נקודות הקוד, או הסתכלו אילו צורות נורמליזציה שונות מהקלט.
- מה זה רווח באפס רוחב ואיך הוא נכנס לטקסט שלי?
- זה תו בלי רוחב ובלי סימן, שמשמש כדי להציע הזדמנות לשבירת שורה. הוא מגיע בדרך כלל מהעתקת טקסט מדף אינטרנט, מעורך מסמכים או מלקוח צ׳אט, ומכיוון ששום דבר לא מוצג, הוא שורד כל סקירה עד שמשהו משווה מחרוזות ונכשל.
- מה ההבדל בין נקודת קוד לתו?
- נקודת קוד היא ערך אחד ביוניקוד. תו במובן היומיומי הוא גרפימה — מה שקורא סופר כאחד — וגרפימה יכולה להיות כמה נקודות קוד. אמוג׳י של משפחה הוא חמש נקודות קוד וגרפימה אחת.
- למה הכלי אומר שלתווים מסוימים אין שם?
- מארבע סיבות שונות, והוא אומר איזו. לסורוגייטים ולנקודות קוד לשימוש פרטי אין שם בתקן, לנקודות לא מוקצות אין מה לקרוא בשם, ולתו בבלוק היסטורי שנבדק לעיתים רחוקות יש שם שהגרסה הזאת אינה נושאת — טבלת השמות שומרת את הבלוקים שאנשים באמת מדביקים מהם במקום לשלוח את כולם.
- האם אזהרת ערבוב כתבים היא הוכחה למתקפה?
- לא. היא אומרת שמילה אחת שואבת את אותיותיה מיותר ממערכת כתב אחת, וזו הצורה שיש לשם מזויף וגם משהו שטקסט רגיל עושה לפעמים. זו סיבה להסתכל, לא פסק דין.
- האם משהו שאני מדביק נשלח לשרת?
- לא. מסד נתוני התווים יורד עם הדף והכול רץ בדפדפן שלכם; שום דבר שאתם מקלידים אינו מועלה או נרשם, וזה עובד בלי חיבור רשת.