Technical Article

خروجی صفحه گسترده امن از نظر یونیکد در Delphi: فرمت‌های RTF و HTML

یک صفحه گسترده حاوی ستونی از نام‌های مشتریان است. برخی به زبان چینی هستند، برخی به الفبای سیریلیک، چند مورد حامل اوملاوت آلمانی یا یک آکسان فرانسوی هستند. شما آن را به CSV صادر می‌کنید و نتیجه را باز می‌نمایید، و هر کاراکتری سالم است. شما همان کتاب کار را برای قالب ادغام پستی (mail-merge) به RTF صادر می‌کنید، آن را در یک واژه‌پرداز باز می‌نمایید و نام‌های غیر ASCII به ردیف‌هایی از علامت سوال تبدیل شده‌اند. داده‌ها هرگز تغییر نکرده‌اند. آنچه تغییر کرده قرارداد کدگذاری فرمتی است که نوشته‌اید و هر مسیر خروجی قرارداد متفاوتی را حمل می‌کند.

این تله‌ای است که کتابخانه‌ای را گرفتار می‌کند که در ظاهر کاملاً سازگار با یونیکد به نظر می‌رسد. متن سلول به صورت داخلی به عنوان WideString نگه داشته می‌شود، بنابراین مدل هرگز کاراکتری را از دست نمی‌دهد. از دست رفتن در مرز اتفاق می‌افتد، در نویسنده‌ای که باید آن متن را در فرمتی با قوانین خاص خود درباره بایت‌های مجاز و نحوه کدگذاری هر چیزی در خارج از محدوده مجاز، سریال‌سازی کند. یک نویسنده را درست کنید و همچنان می‌توانید نویسنده دیگری را ارسال کنید که همان متن را خراب می‌کند. راه حل یک سوئیچ سراسری نیست. این یک تصمیم جداگانه و درست در هر مسیر است.

RTF از نظر طراحی یک فرمت امن 7 بیتی است

فرمت Rich Text Format قبل از یونیکد وجود داشته و برای زنده ماندن در انتقال‌هایی که فقط ASCII قابل چاپ را عبور می‌دهند، مشخص شده است. یک سند RTF یک صفحه کد را در هدر خود اعلام می‌کند و هر کاراکتری که نویسنده نتواند در آن صفحه کد نشان دهد، باید به عنوان یک کاراکتر گریز (escape) صادر شود تا یک بایت خام. کاراکتر گریز مربوطه \u است که یک واحد کد ۱۶ بیتی علامت‌دار را به همراه یک کاراکتر جایگزین ASCII برای خوانندگانی که برای درک این کاراکتر گریز خیلی قدیمی هستند، حمل می‌کند.

مجموعه HotXLS فرمت RTF را به این روش می‌نویسد. هدر سند با اعلام صفحه کد شروع می‌شود، به شکل \ansi\ansicpg1252\uc1، و نویسنده در واحد lxRTF در هر رشته حرکت کرده و هر کاراکتر بالاتر از ASCII ساده را به صورت یک گریز \u صادر می‌کند تا جریان بایت بدون توجه به آنچه صفحه کد اعلام‌شده می‌تواند نگه دارد، ۷ بیتی و پاک باقی بماند. یک نقطه کد مانند U+4E2D به توالی واقعی  3? تبدیل می‌شود، نه یک بایت خام که نمایشگر سپس سعی کند آن را از طریق هر صفحه کدی که فرض کرده تفسیر کند. بدون آن روال، هر چیزی خارج از صفحه کد اعلام‌شده فاقد نمایش بایت قانونی است و نویسنده‌ای که مقدار خام را صادر می‌کند، علامت‌های سوالی را تولید می‌نماید که این مقاله با آن‌ها شروع شد.

جزئیاتی که باید به خاطر بسپارید این است که صفحه کد اعلام‌شده و گریزها دو نیمه از یک قرارداد هستند. اعلام صفحه کد به تنهایی به متنی که خارج از آن قرار دارد کمک نمی‌کند. صادر کردن گریزها بدون صفحه کد اعلام‌شده، کاراکترهای جایگزین را مبهم رها می‌کند. هر دو باید با هم درست باشند، به همین دلیل است که نویسنده‌ای که تنها یکی از آن‌ها را مدیریت می‌کند همچنان در اولین کتاب کار چندزبانه شکست می‌خورد.

گریز HTML در مورد چیزی بیشتر از علامت‌های کوچکتر بزرگتر است

خروجی HTML یک سند چندصفحه‌ای تولید می‌کند که فریم‌های ناوبری آن نام صفحات را به عنوان متن مرئی حمل می‌کنند. آن نام‌ها رشته‌های تحت کنترل نویسنده هستند که می‌توانند حاوی هر کاراکتری باشند، از جمله کاراکترهای مهم برای نشانه‌گذاری. صفحه‌ای که به معنای واقعی کلمه Q1 & Q2 <draft> نام دارد باید به صورت موجودیت‌های گریز‌ داده شده (escaped entities) به صفحه برسد، در غیر این صورت علامت‌های کوچکتر بزرگتر یک تگ فانتوم را باز می‌کنند و علامت امپرسند یک مرجع موجودیت را شروع می‌کند که هرگز در نظر گرفته نشده بود. این یک گریز معمولی HTML است و نادیده گرفتن آن در برچسب فریم از آن حذفیاتی است که از هر تست ساخته شده با نام صفحات فقط ASCII عبور می‌کند.

سوال کدگذاری یک لایه پایین‌تر از آن قرار دارد. هنگامی که کاراکترهای غیر ASCII در متنی فرود می‌آیند که تضمینی برای ارائه آن به عنوان UTF-8 وجود ندارد، نمایش امن یک مرجع کاراکتر عددی (numeric character reference) است، بنابراین U+00E9 به صورت é نوشته می‌شود تا یک بایت خام که معنای آن به مجموعه کاراکتر پاسخ بستگی دارد. تصویر آینه این قانون در مسیر ورودی اعمال می‌شود. کتاب کاری که از XLSX بازخوانی می‌شود، رشته‌های مشترکی را حمل می‌کند که در آن‌ها ممکن است یک کاراکتر از قبل به صورت یک موجودیت عددی XML ذخیره شده باشد و آن موجودیت باید قبل از ورود به مدل سلول به یک کاراکتر کامل رمزگشایی شود. رمزگشایی بی‌دقت آن با تقسیم یک نقطه کد به بایت‌های جداگانه، باعث می‌شود یک کاراکتر واحد به عنوان دو تکه mojibake ظاهر شود که هیچ خروجی بعدی نمی‌تواند آن را تعمیر کند.

ظرف XLSX یک فایل ZIP است و ZIP کدگذاری نام خود را دارد

یک فایل XLSX یک آرشیو ZIP است و آرشیو نامی را برای هر عضوی که نگه می‌دارد ذخیره می‌کند. فرمت ZIP به اندازه‌ای قدیمی است که مشخصات اصلی آن چیزی در مورد کدگذاری آن نام‌ها نگفته است، بنابراین خواننده‌ای که هیچ سیگنالی پیدا نمی‌کند، صفحه کد محلی آرشیو را فرض می‌کند. این فرض به محض اینکه نام یک عضو حاوی یک کاراکتر غیر ASCII باشد اشتباه است، که این اتفاق با نام بخش‌های بومی‌سازی شده صفحه کاری و رسانه‌های تعبیه‌شده که نام فایل آن‌ها حاوی آکسان یا خط غیرلاتین است رخ می‌دهد.

راه حل یک بیت واحد است. بیت ۱۱ عمومی در هر هدر فایل محلی اعلام می‌کند که نام عضو به صورت UTF-8 کدگذاری شده است. HotXLS دقیقاً همان بیت را در زمان خواندن یک آرشیو بررسی می‌کند و پرچم‌های عمومی را در برابر ماسک $0800 تست می‌نماید و خواننده یا نویسنده‌ای که آن را نادیده بگیرد، نامی را که یک پیاده‌سازی درست به عنوان UTF-8 ذخیره کرده اشتباه می‌خواند. این بیت برای تنظیم و احترام ارزان است و کل تفاوت بین نام عضوی است که در رفت و برگشت زنده می‌ماند و نامی که قبل از پارس شدن محتوای صفحه گسترده خراب می‌رسد.

تبدیل حروف کوچک بزرگ و اسکن اعداد همان خطر را پنهان می‌کنند

ارزیابی فرمول جایی است که امنیت یونیکد از موضوع سریال‌سازی خارج شده و به موضوع مقایسه تبدیل می‌شود. تابع SEARCH به حروف کوچک و بزرگ حساس نیست، که این بدان معناست که باید قبل از جستجوی زیررشته، حروف را یکسان‌سازی (fold case) کند. روش اشتباه برای این کار از طریق صفحه کد ANSI است، زیرا بزرگ کردن حروف متن غیر ASCII به آن روش، کاراکترها را از طریق یک صفحه کد باریک هدایت کرده و هر چیزی را در خارج از آن خراب می‌کند. روش درست بزرگ کردن حروف رشته‌های عریض (wide-string) است که محدوده کامل UTF-16 را حفظ می‌کند. HotXLS دقیقاً به همین دلیل با WideUpperCase کار یکسان‌سازی را انجام می‌دهد، بنابراین جستجوی متن آکسان‌دار یا غیرلاتین با همان کاراکترهایی که داده شده مطابقت پیدا می‌کند تا یک تقریب خراب‌شده با صفحه کد از آن‌ها.

توکنایزر فرمول تعهد مرتبطی را حمل می‌کند که هیچ ارتباطی با حروف ندارد و همه چیز به جایی مربوط می‌شود که یک توکن به پایان می‌رسد. نماد علمی مانند 1E3 یا 2.5E-3 یک لیترال عددی واحد است و اسکنر باید E، یک علامت اختیاری و رقم‌های بعدی را به عنوان بخشی از عدد تشخیص دهد تا اینکه ورودی را به یک نام و به دنبال آن یک عدد مجزا تقسیم کند. اسکنری که این مورد را اشتباه مدیریت کند، یک ثابت کاملاً معتبر را به خطای پارس یا بدتر از آن، به یک عبارت خاموش و نادرست تبدیل می‌کند. این موضوع به همین بحث تعلق دارد زیرا هر دو مورد درباره خواننده‌ای است که تصمیم درستی در سطح کاراکتر می‌گیرد: یکی درباره نحوه تبدیل کاراکتر برای مقایسه، دیگری درباره اینکه آیا یک کاراکتر توکن فعلی را ادامه می‌دهد یا خیر.

ساخت و خروجی گرفتن از یک کتاب کار چندزبانه

کلاس API عمومی از شما نمی‌خواهد به هیچ‌کدام از این موارد فکر کنید. شما کتاب کار را از مقادیر سلول WideString می‌سازید و نقطه ورود خروجی مورد نظر خود را فراخوانی می‌نمایید. تصمیمات کدگذاری در داخل هر نویسنده اتفاق می‌افتد. مثال زیر یک صفحه را با متن در چندین خط تغذیه می‌کند، سپس یک فایل RTF و یک فایل HTML را از همان کتاب کار می‌نویسد، بنابراین دو مسیر در برابر ورودی یکسان اجرا می‌شوند.

uses
  lxHandle;

procedure ExportMultilingualWorkbook;
var
  Book: IXLSWorkbook;
  Sheet: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Customers');

    Sheet.Cells[1, 1].Value := 'Name';
    Sheet.Cells[1, 2].Value := 'City';

    Sheet.Cells[2, 1].Value := '王伟';          // Chinese
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // German umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Cyrillic
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // French accents
    Sheet.Cells[5, 2].Value := 'Montréal';

    Book.SaveAsRTF('Customers.rtf');
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

هر دو فراخوانی وضعیت Integer را برمی‌گردانند و هر دو همان متن موجود در حافظه را مصرف می‌کنند. هیچ‌چیز در کد فراخوانی صفحه کد را اعلام نمی‌کند یا کاراکتری را گریز می‌دهد، زیرا مسئولیت با نویسنده‌ای است که فرمت خود را می‌شناسد. متد در سطح کتاب کار SaveAsCSV در صورت نیاز به خروجی جداشده از همان منبع، از همان شکل پیروی می‌کند.

// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');

امنیت یونیکد به ازای هر مسیر است، نه به ازای هر کتابخانه

درسی که ارزش بردن دارد این است که هیچ مکان واحدی برای امن بودن از نظر یونیکد وجود ندارد. فرمت RTF به یک صفحه کد اعلام‌شده به علاوه گریزهای \u نیاز دارد. HTML به گریز موجودیت برای کاراکترهای مهم نشانه‌گذاری و مراجع عددی در جایی که مجموعه کاراکتر تضمین نشده است، به علاوه رمزگشایی صحیح موجودیت‌هایی که به رشته‌های مشترک می‌رسند نیاز دارد. ظرف ZIP به تنظیم بیت ۱۱ عمومی نیاز دارد تا نام عضو UTF-8 به عنوان UTF-8 خوانده شود. ارزیابی فرمول نیاز به تبدیل حروف رشته‌های عریض و یک توکنایزر دارد که نماد علمی را در یک بخش نگه می‌دارد. هر یک از این‌ها قرارداد متفاوتی است و یک کتابخانه می‌تواند یکی را برآورده کند در حالی که بی‌سروصدا دیگری را نقض می‌نماید. این دلیلی است که ابزاری که CSV را درست انجام می‌دهد همچنان می‌تواند RTF پر از علامت سوال به شما تحویل دهد.

اگر خروجی‌های شما به فرمت‌های جداشده تکیه دارند، موازنه‌های بین آن‌ها در راهنمای ما برای خروجی CSV، TSV و HTML پوشش داده شده است، و هنگامی که منبع یک مجموعه نتیجه (result set) به جای یک صفحه دست‌ساز باشد، الگوهای موجود در خروجی دیتابیس برای گزارش‌های Delphi به طور طبیعی با قوانین کدگذاری توصیف‌شده در اینجا جفت می‌شوند. تمام این‌ها به عنوان بخشی از کامپوننت HotXLS برای Delphi و C++Builder در کنار APIهای خواندن، فرمول و قالب‌بندی که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند، ارائه می‌شوند.