Teknik Makale

PDFDocEncoding Kaydırması Delphi'de FDF İçe Aktarmayı Bozar

Bir FDF'i bir forma içe aktar, alan değerleri incelikle yanlış geri gelir: bir bullet bir dagger olarak gelir, bir ellipsis em dash olarak. PDFium Component bu semptom ailesinin tamamını PDFDocEncoding tablosundaki tek bir satıra kadar izledi — ISO 32000-1 Annex D.2'deki özel karakter bloğu $7F değil $80 ile başlar

Bir FDF içe aktarması neden komşu karakteri döndürür?

Çünkü bir konum kaymış bir kodlama tablosu hâlâ kusursuz görünümlü bir tablodur ve içindeki her arama başarılı olur. Annex D.2'nin $80'dan $9E'ye atadığı blok bir satır erken yazılmıştı; böylece $80 baytı $81'e ait girdiye, $81 baytı $82 girdisine çözüldü ve bu, on dokuz ardışık kod konumu boyunca böyle sürdü. Hiçbir şey yükselmedi, hiçbir şey loglanmadı ve hiçbir alan asla reddedilmedi — TPdf.ImportFDF her zaman verdiği alan sayısını verdi

Hasar tekdüze ve sessizdi. $80 taşıyan bir bayt — Annex D.2'nin BULLET (U+2022) olarak tanımladığı — DAGGER (U+2020) olarak geri geldi. $83 baytı, HORIZONTAL ELLIPSIS (U+2026), EM DASH (U+2014) olarak geri geldi. $8D baytı, LEFT DOUBLE QUOTATION MARK (U+201C), RIGHT DOUBLE QUOTATION MARK (U+201D) olarak geri geldi — kimsenin fark etmediği durum, çünkü yanlış bir kıvrımlı tırnak bir form alanında yine de kıvrımlı tırnak gibi görünür. Alan adları alan değerleriyle aynı kaderi yaşadı, ki bu daha kötüdür: belgedeki widget ile artık eşleşmeyen bir ad sessizce uygulanmaz

PDFium Component'te FDF içe aktarma hasarının arkasındaki PDFDocEncoding kaymasının şeması: ISO 32000-1 Annex D.2, $80 baytını BULLET'a, $83'ü HORIZONTAL ELLIPSIS'e ve $8D'yi sol çift tırnak işaretine eşler; bir satır erken yazılan tablo ise aynı baytlar için DAGGER, EM DASH ve sağ tırnak işareti döndürdü, her arama yine de başarılıydı
Kaymış bir kodlama tablosu, içinde her aramanın başarılı olduğu geçerli görünümlü bir tablodur; bu yüzden bir bullet dagger olarak gelir ve artık eşleşmeyen bir alan adı sessizce uygulanmaz

PDF metin stringleri iki farklı kodlama taşır

ISO 32000-1 §7.9.2.2 metin string türünü tam olarak iki kodlamadan biri olarak tanımlar; seçim herhangi bir sözlük girdisinden çok ilk iki baytın incelenmesiyle yapılır. Bayt sırası işareti FE FF ile açılan bir string UTF-16BE'dir; geri kalan her şey PDFDocEncoding'dir. Bu tek dal, tamamen farklı iki decoder'dan hangisinin çalışacağına karar verir ve PDFium Component bunu FDF parser'ının içinde, tek bir karaktere dokunmadan önce uygular

PDFium Component'teki ISO 32000-1 bölüm 7.9.2.2 metin string dalının şeması: bayt sırası işareti FE FF ile açılan bir string, doğrulanmış surrogate çiftleriyle UTF-16BE olarak çözülür; diğer her şey tek baytlık FdfPdfDocEncodingChar tablosuna gider; bu yüzden UTF-16 yazarları ve saf ASCII adlar kaymış bloğu tek bir tipografik sembol gelene kadar hiç açığa çıkarmadı
Hangi decoder'ın çalışacağına herhangi bir sözlük girdisi değil ilk iki baytın incelenmesi karar verir; varsayılan olarak UTF-16BE yazan araçlar yanlış olan tek tabloyu hiç kullanmadı
// FDF metin stringi: BOM varken UTF-16, aksi halde PDFDocEncoding
if (Length(Raw) >= 2) and
   (((Byte(Raw[1]) = $FE) and (Byte(Raw[2]) = $FF)) or
    ((Byte(Raw[1]) = $FF) and (Byte(Raw[2]) = $FE))) then
begin
  BigEndian := Byte(Raw[1]) = $FE;
  // ... UTF-16 kod birimlerini çöz, surrogate çiftlerini doğrula
end
else
begin
  SetLength(Result, Length(Raw));
  for I := 1 to Length(Raw) do
    Result[I] := FdfPdfDocEncodingChar(Byte(Raw[I]));
end;

Bug'ın sıradan testlerde bu kadar uzun saklanmasının nedeni budur. Varsayılan olarak UTF-16BE yazan bir aracın ürettiği herhangi bir FDF, FdfPdfDocEncodingChar'a hiç ulaşmaz; böylece kaymış tablo uykuda kalır. Yalnızca tek baytlık stringler — ki çoğu FDF üretecinin düz Latin alan adları için yaydığı şey budur, çünkü daha kısadırlar — tablonun önemli olduğu dala iner. Saf ASCII alan adları o zaman da doğru çözülür, çünkü $80 altında PDFDocEncoding ve ASCII aynı fikirdedir. Görünür biçimde bir şeyin yanlış gitmesi için tek bir tipografik sembol içeren tek baytlık bir stringe ihtiyacın var

Düzeltilmiş tablo ve onu sabitleyen iki çapa

PDFium Component artık sınırı FdfPdfDocEncodingChar'ın içinde açıkça yazar, çünkü bloğun iki ucu tam olarak uygulayıcının yanlış yaptığı yerdir. $7F DEL'dir ve hiçbir PDFDocEncoding anlamı yoktur; blok $80'de BULLET ile açılır ve $9E'de zcaron ile biter; $9F atanmamıştır; Euro işareti $A0'da tek başına durur

PDFium Component'te Annex D.2 tablosunu sabitleyen sınır çapalarının şeması: $7F hiçbir PDFDocEncoding anlamı olmayan DEL'dir, $80 bloğu BULLET ile açar, $9E bloğu zcaron ile kapatır, $9F atanmamıştır ve Euro işareti $A0'da tek başına durur; böylece tam blok kayması uçları ve boşlukları aynı anda tatmin edemez
İlk satırı, son satırı ve boşlukları doğrulamak iki ucu birden sabitler; bu da tam olarak bir tam-bloğun sağ kalamayacağı testtir
function FdfPdfDocEncodingChar(Value: Byte): WideChar;
begin
  case Value of
    // ISO 32000-1 Annex D.2. Blok $7F değil $80'den başlar: $7F DEL'dir ve
    // hiçbir PDFDocEncoding anlamı yoktur ve Euro $A0'da durur; varsayılan
    // dal onu aksi halde no-break space olarak geri verirdi
    $7F: Result := WideChar($FFFD);
    $80: Result := WideChar($2022);   // BULLET, bloğun ilk satırı
    $81: Result := WideChar($2020);   // DAGGER
    // ... $82 through $9D
    $9E: Result := WideChar($017E);   // zcaron, tanımlı son satır
    $9F: Result := WideChar($FFFD);   // Annex D.2'de atanmamış
    $A0: Result := WideChar($20AC);   // EURO SIGN
  else
    Result := WideChar(Value);
  end;
end;

Kaymanın o iki uca ne yaptığına bak, çünkü off-by-one tam orada kozmetik olmaktan çıkar. zcaron şartnamede $9E'de ama kaymış tabloda $9D'de duruyordu; böylece belgedeki $9E baytı, herkesin yazdığı son satırın ötesine düştü ve doğrudan kimlik fallback'ine değdi, bir harf yerine bir C1 kontrol karakteri döndürdü. Ve $A0, yani Euro, tabloya en başta hiç yazılmamıştı — kimlik dalı onu U+00A0 NO-BREAK SPACE olarak geri verdi; sonraki her doğrulamadan sağ çıkan, hiçbir şey basmayan ve aradığın alan adıyla asla eşleşmeyen bir karakter

Yazılmış bir kodlama tablosunu nasıl test edersin?

İlk satırı, son satırı ve boşlukları test et — ortadan bir örnek değil. Bu, düzeltmenin bir şartnameden kopyaladığın diğer her tabloya taşımaya değer kısmıdır, çünkü bloğun ortasındaki bir nokta kontrolü tam olarak tam-bir bloğun sağ kaldığı testtir. $8D'nin sol çift tırnak verdiğini doğrulamak, $8C de makul bir şey verdiğinde hiçbir şey kanıtlamaz; $80'in BULLET ve $9E'nin zcaron verdiğini doğrulamak iki ucu da sabitler ve kaymış bir blok ikisini aynı anda tatmin edemez

procedure CheckPdfDocEncodingAnchors;
begin
  // Annex D.2 özel bloğunun iki ucu: tam blok kayması bunları bozar
  Assert(FdfPdfDocEncodingChar($80) = WideChar($2022), 'first row: BULLET');
  Assert(FdfPdfDocEncodingChar($9E) = WideChar($017E), 'last row: zcaron');
  // Şartnamenin tanımsız bıraktığı kod konumları bir harfe çözülmemeli
  Assert(FdfPdfDocEncodingChar($7F) = WideChar($FFFD), '$7F is DEL, unmapped');
  Assert(FdfPdfDocEncodingChar($9F) = WideChar($FFFD), '$9F is unassigned');
  // Bloğun üzerindeki yalıtılmış girdi, tamamen atlamak kolaydır
  Assert(FdfPdfDocEncodingChar($A0) = WideChar($20AC), 'EURO SIGN');
end;

Tanımsız konumlar ilginç değil diye atlanmak yerine kendi assertion'larını hak eder. Bir off-by-one bir yerlere bir şey koymak zorundadır ve koyduğu yer neredeyse her zaman şartnamenin boş bıraktığı bir kod konumudur — önde $7F, arkada $9F. Yalnızca tanımlı konumları kontrol eden bir test, tablo her iki yönde de yanlışken geçebilir; delikleri sabitleyen bir test kaymayı ilk çalıştırmada yakalar

Bunun bir round-trip form iş akışına maliyeti ne?

Sembol başına bir karakteri sessizce yeniden yazan bir FDF içe aktarması, başarısız olandan daha kötüdür; çünkü başarısızlık aşağı akışta yüzeye çıkar ve başka bir şey gibi görünür. Alan adları buradaki keskin kenardır. TPdf.ImportFDF gelen adları açık belgedeki widget'larla eşleştirir ve bir karakteri yanlış çözülen bir ad basitçe eşleşmez — alan boş kalır, dönüş değeri alanı yine de ayrıştırılmış sayar ve hattan çıkan rapor, logda hiçbir şey açıklamayan tek bir değer eksiktir. XFDF form verilerini ve ek açıklamalarını PDFium Component ile dışa ve içe aktarmak yazısında anlatılan türden bir dışa-ve-yeniden-içe-aktarma döngüsü kuran herkes, döngüye güvenmeden önce düz ASCII değil, tipografik bir sembol içeren bir alan adını round-trip etmelidir

Düzeltmenin sınırları konusunda dürüst olmaya da değer. Tablo artık 256 bayt değerinin tümü için Annex D.2 ile eşleşir, bu da tek baytlık FDF stringlerinin doğru çözülmesini sağlar; ama PDFDocEncoding'in kendisi bir Latin repertuarını kapsar ve başka hiçbir şeyi — onun dışındaki her şey BOM'lu bir UTF-16BE olarak gelmek zorundadır, §7.9.2.2'nin niyeti tam olarak budur. Ve bir UTF-16 string'in Delphi tarafında hâlâ kendi ayrı tehlikeleri vardır, çünkü U+FFFF ötesindeki bir kod noktası tek bir WideChar yerine bir surrogate çifti gerektirir; emoji ve CJK karakterlerinin Delphi'de WideChar'ı neden bozduğu yazısında ele alınan problem. Daha geniş nokta kodlama tablolarının ötesine genelleşir: bir PDF parser, kabul etmesi gereken bir şeyi reddedebilir, ki bunu hemen bulursun; ya da bir şeyi kabul edip sessizce yanlış yapabilir, ki bunu üretimde bulursun. PDF sözlüklerini güvenle ayrıştırmak yazısında anlatılan ad-token tuzakları ikinci kategoride oturur; bir satır kaymış bir tablo da öyle — kod yolu başarılı olur, dönüş değeri makul görünür ve yalnızca uygulamaya karşı değil şartnameye karşı yazılmış bir test aksini söyler

TPdf üzerindeki ImportFDF, ExportFDF ve ImportXFDF, arkalarındaki FDF ve XFDF parser'larıyla birlikte, Delphi ve C++Builder için standart PDFium Component'in parçasıdır