UDF dosya biçimi: içinde ne var, nasıl editör yazılır
UDFbiçimgeliştirici
.udf dosyasını açan bir şey yazmaya kalktığınızda ilk fark ettiğiniz şey, ortada bir şartname olmadığı oluyor. Uzantıyı .zip yapıp içine bakıyorsunuz, content.xml çıkıyor, bir süre XML'e bakıyorsunuz, sonra mantığı kendiniz çıkarmaya başlıyorsunuz.
Biz de öyle yaptık. Elimizde bir yedek diskte 71.259 tane gerçek .udf vardı, ayrıştırıcıyı yazarken hepsinden geçirdik. Aşağıdakiler o turdan kalan notlar: biçimin nasıl çalıştığı ve gerçek dosyaların onu nerelerde bozduğu.
Önce kabuk
.udf bir zip. İçinde en fazla üç şey var:
belge.udf
├── content.xml her zaman var
├── documentproperties.xml isteğe bağlı — uyapdogrulamakodu, uyapsicil
└── sign.sgn isteğe bağlı — ayrık CMS/PKCS#7 imza
Çoğu dosyada yalnızca ilki bulunuyor.
İlk sürprizi ilk yüz dosyada yaşıyorsunuz: dosyaların yaklaşık %4'ü zip bile değil. UYAP Editör aynı uzantıyla, sıkıştırılmamış, çıplak bir content.xml de yazıyor. Üstelik bunların bir kısmı BOM'la ve başında boş satırlarla geliyor, yani data[:5] == b"<?xml" gibi bir kontrol yazdıysanız tam o dosyalarda düşüyor. BOM'u ve baştaki boşluğu kırpın, sonra <?xml ya da <template arayın; bulursanız dosyayı tek girdili bir arşiv gibi işleyin.
İkinci sürpriz daha az zarif: insanlar .docx dosyasının adını .udf yapıp yüklüyor. Arşivde [Content_Types].xml görürseniz bunu ayrıca yakalayın ve söyleyin. Bu durumda "bozuk dosya" demek yanlış yönlendiriyor — karşıdaki dosyanın bozuk olmadığını zaten biliyor, yanlış dosyayı yüklediğini bilmiyor.
Bir de güvenlik tarafı var. Yabancı birinin gönderdiği zip, aksi ispatlanana kadar sıkıştırma bombasıdır. Buradaki klasik hata arşivin beyan ettiği açılmış boyuta bakmak; o sayıyı da saldırgan yazıyor. Okumanın kendisini sınırlayın: bütçe kadar artı bir bayt okuyun, taştıysa durun. Böylece büyük sonuç belleğe hiç girmiyor. Bizim tavanımız bütün girdiler için toplam 64 MB. Ölçek fikri versin: 71 binlik örneklemdeki en büyük dosya açıldığında 40 MB'ın altında kalıyordu.
content.xml
Kök <template>. UYAP'ın kendi şeması — ODF ya da OOXML bekliyorsanız hayal kırıklığı olacak.
<template format_id="1.8">
<content><![CDATA[ belgenin bütün metni ]]></content>
<properties><pageFormat mediaSizeName="1" ... /></properties>
<elements> paragraph | table | header | footer </elements>
<styles> style* </styles>
<webID id="..." /> <!-- isteğe bağlı -->
<data> ... </data> <!-- isteğe bağlı, UYAP form verisi -->
</template>
format_id bugün yazılan dosyalarda 1.8.
Asıl fikir: yapı metin taşımıyor
Biçimi anlamak istiyorsanız kilit nokta burası. Belgenin bütün metni tek bir CDATA tamponunda duruyor. <elements> altındaki hiçbir etiketin içinde yazı yok; satır içi öğeler tampona startOffset ve length ile bakıyor, o kadar.
Satır içi öğeler: content, space, tab, field, image.
<paragraph Alignment="1">
<content resolver="hvl-default" bold="true" startOffset="0" length="24" />
<content resolver="hvl-default" startOffset="24" length="18" />
</paragraph>
71 bin dosyada doğrulanan değişmez şu: satır içi öğeleri belge sırasına göre uç uca eklerseniz tampon birebir geri çıkıyor. Boşluk yok, çakışma yok. Ayrıştırıcıyı bunun üzerine kurduk.
Buradan iki şey çıkıyor. Paragraf sonu, tamponda sıradan bir \n karakteri ve paragrafın son runu'nun sahipliğinde; <br> benzeri bir etiket aramayın, yok. Gömülü görsel ise tamponda tam bir karakter yer kaplıyor: U+FFFC, yani "object replacement character". Metni çıkarırken onu boşlukla değiştirmek isteyeceksiniz.
Ofsetler karakter sayıyor
Türkçe belgelerde hemen patlayan ayrıntı bu. UTF-8'de ş, ğ, İ iki bayt tutuyor; tamponda bayt indeksiyle gezerseniz ilk Türkçe karakterden sonra kayıyorsunuz ve kayma belge boyunca büyüyor. Belirtisi de tanıdık: belgenin başı düzgün, ortası kaymış, sonu çöp.
Python'da str üzerinde çalıştığınız sürece bunu hiç fark etmiyorsunuz. Go'da []rune, Rust'ta chars() gerekiyor. JavaScript'te ayrıca UTF-16 tuzağı var: String.length BMP dışındaki karakterleri iki sayıyor. Hukuk belgelerinde nadir, ama bazı matematik sembolleri o aralığa düşüyor.
Gerçek dosyaların tuhaflıkları
Yukarısı biçimin nasıl çalıştığıydı. Burası onu bozan dosyalar, hepsini gerçekten gördük.
length negatif olabiliyor. Özellikle altbilgilerde UYAP, startOffset'ten geriye doğru gösteren runlar yazıyor. text[start:start+length] bu durumda sessizce boş string döndürüyor — hata almıyorsunuz, sadece bazı paragraflar boş çıkıyor ve nedenini anlamak için XML'e tek tek bakmanız gerekiyor. Çözümü basit: length < 0 ise aralığı (start+length, start) diye sıralayın.
Runlar her zaman bitişik de olmuyor; aynı paragrafın runları arasında delik kalabiliyor. Okurken sorun çıkarmıyor. Düzenlerken çıkarıyor, çünkü ofsetleri yeniden numaralandırdığınızda deliğin içindeki metni nereye koyacağınızı bilemiyorsunuz. Bazı satır içi öğelerde ise ofset hiç yok.
Üçünde de aynı şeyi yapıyoruz: tahmin etmiyoruz, paragrafı salt okunur işaretleyip olduğu gibi geri yazıyoruz. Paragraph nesnemizdeki editable bayrağı tam olarak bu işi görüyor.
| Durum | Davranış |
|---|---|
length negatif | span sıralanıyor, paragraf salt okunur |
| runlar arasında delik var | paragraf salt okunur |
startOffset / length yok | run atlanıyor, paragraf salt okunur |
paragrafta <image> var | paragraf salt okunur |
Tablodaki son satırın sebebi editörde: tarayıcı size gömülü bir görseli geri veremiyor, dolayısıyla tarayıcıdan gelenle paragrafı yeniden kurarsanız resmi düşürüyorsunuz. Paragrafı komple kilitlemek, resmin sessizce kaybolmasına yeğdir.
Düzenleme: runları değiştir, ofsetleri yeniden numarala
Bir paragrafı düzenlemek dört adım:
- Paragrafın runlarını yenileriyle değiştirin.
- Tamponu yeniden kurun: paragrafın öncesi + yeni metin + sonrası.
- Düzenlenen paragrafın runlarını paragrafın başlangıcından itibaren yeniden
numaralayın.
- Kendisinden sonra gelen bütün runların
startOffsetdeğerlerini birikmiş
farka göre kaydırın.
Dördüncü adımdaki tuzak şu: akla gelen ilk çözüm bütün belgeyi baştan numaralamak ve bu ilk bakışta çalışıyor gibi görünüyor. Sorun, hiçbir öğenin sahiplenmediği bölgelerin olması — çoğu dosyanın sonundaki fazladan satır sonu, arada kalmış sahipsiz metin parçaları. Baştan numaralarsanız bunlar sessizce kayboluyor, kaybolduklarını da dosyayı UYAP Editör'de açana kadar görmüyorsunuz. Onun yerine yalnızca düzenlenen paragraflar yer değiştirsin; geri kalan herkes kendi metnini korusun ve birikmiş fark kadar kaysın.
XML ağacını da yerinde değiştirin, sıfırdan kurmayın. Belgelerde resolver, fieldGroupName, isErasable, isList gibi hiç duymadığınız öznitelikler var ve ne işe yaradıklarını bilmeden silmek istemezsiniz. Ağacı ElementTree ile ayrıştırıp yalnızca dokunduğunuz düğümleri değiştirirseniz geri kalan her şey turu sağ çıkıyor.
Bir de tarayıcı tarafı. contenteditable bir alanın ürettiği HTML tarayıcıdan tarayıcıya değişiyor ve onu runlara geri çevirmeye çalışmak uzun vadede kaybedilen bir savaş. Biz sayfanın yalnızca değişen paragrafların listesini göndermesini tercih ettik: her paragraf, (metin, biçim) ikililerinden oluşan bir dizi. Böylece contenteditable'ı anlamak zorunda olan tek şey tarayıcının kendisi oluyor.
İmza: dokunulmamış dosya bit bit aynı çıkmalı
Hata yapmanın en pahalı olduğu yer burası.
Bir .udf'yi açıp hiçbir şey değiştirmeden geri yazdıysanız orijinal baytları döndürün. Yeniden serileştirmek — öğe öğe birebir aynı XML'e bile — öznitelik sırasını ve boşlukları değiştiriyor, imza da baytların üzerinde olduğu için UYAP Editör dosyaya "geçersiz imza" diyor. Bunu resmî editörle denedik: yeniden kurulmuş kopyayı reddetti, dokunulmamış olanı kabul etti.
Belge gerçekten değiştiyse sign.sgn dosyasını atın. Yanında documentproperties.xml içindeki uyapdogrulamakodu da gitmeli; o kod UYAP tarafında orijinal belgeyi işaret ediyor, sizin ürettiğinizi değil. İkisini bırakırsanız ortaya sahip olmadığı bir imzayı iddia eden bir dosya çıkıyor — kullanıcıyı en kötü anda zor durumda bırakacak türden bir hata.
Tek bir boşluğun imzayı neden düşürdüğü ayrı bir konu; ona e-imzalı UDF'yi düzenleyince imza neden bozulur yazısında girdik.
Sıfırdan .udf yazmak
Okumaktan kolay, çünkü aykırı durumları bu sefer siz üretmiyorsunuz. Dikkat edilecek birkaç şey var:
- Her paragrafın son runu kendi
\nkarakterini taşısın; tamponu ve
ofsetleri buna göre kurun.
Alignment:0sol,1orta,2sağ,3iki yana.<styles>bloğunu boş bırakmayın. Resmî editörün yazdığı tabloyu kopyalayın
— en azından default ve hvl-default bulunsun, runlarda resolver="hvl-default" ile onlara atıf yapın.
- Kenar boşluğu için A4 dikey ve dört yandan
70.8661413192749. Tuhaf
görünüyor, biliyoruz; 25 mm'nin punto karşılığı ve editörün kendi varsayılanı.
Asgari bir iskelet:
<?xml version="1.0" encoding="UTF-8" ?>
<template format_id="1.8" >
<content><![CDATA[T.C.
ANKARA 1. ASLİYE HUKUK MAHKEMESİ
]]></content>
<properties><pageFormat mediaSizeName="1" leftMargin="70.8661413192749"
rightMargin="70.8661413192749" topMargin="70.8661413192749"
bottomMargin="70.8661413192749" paperOrientation="1"
headerFOffset="20.0" footerFOffset="20.0" /></properties>
<elements resolver="hvl-default" >
<paragraph Alignment="1">
<content resolver="hvl-default" startOffset="0" length="5" />
</paragraph>
<paragraph Alignment="1">
<content resolver="hvl-default" bold="true" startOffset="5" length="32" />
</paragraph>
</elements>
<styles>...</styles>
</template>
Dosyayı content.xml adıyla, deflate ile sıkıştırılmış tek girdili bir zip olarak yazın.
Kontrol listesi
- Çıplak XML ve BOM'lu dosyalar okunuyor mu
.docxuzantısı değiştirilmiş dosya ayrı bir hata veriyor mu- Açılmış boyut için tavan var mı, beyan edilen boyuta güvenilmiyor mu
- Ofsetler karakter cinsinden mi işleniyor
- Negatif
lengthsıralanıyor mu - Düzenlenemeyecek paragraflar işaretlenip olduğu gibi geri yazılıyor mu
- Dokunulmamış belge bit bit aynı dönüyor mu
- Düzenlenmiş belgede
sign.sgnve doğrulama kodu düşüyor mu
Bir de test tarafı. Yukarıdaki tuhaflıkların hiçbiri ilk on dosyada çıkmıyor, hepsi yüzlerce dosyadan sonra çıkıyor. Elinizde bir arşiv varsa ayrıştırıcıyı tamamının üzerinden bir kere geçirin ve iki sayıyı takip edin: kaç dosya ayrıştırılabildi, kaç paragraf salt okunur işaretlendi. Bu iki sayının nasıl değiştiğine bakmak, tek tek dosya açmaktan daha çok şey öğretiyor.
Devamı
- E-imzalı UDF'yi düzenleyince imza neden bozulur?Elektronik imza dosyanın baytlarına bağlı. Tek bir boşluk bile doğrulamayı düşürüyor; nedeni ve doğru çalışma sırası.
- UDF dosyası nasıl açılır?UYAP'tan indirdiğiniz .udf dosyasını açmanın üç yolu, hangisinin ne zaman işe yaradığı ve Word'ün neden açmadığı.
- UYAP'tan gelen belgeyi Word'e çevirme.udf dosyasını Word'e aktarırken hangi biçimlendirme taşınır, hangisi kayar ve dönüştürmeden önce neye bakmalısınız.