Bir QDF dosyasını elle düzenleyin, aynı nesne için ikinci bir stream işareti araya sokun ve yazabileceğiniz en doğal Delphi koduyla yerinde bir onarım çalıştırın. Onarım adımı hasarı fark eder ve reddeder. Sonrasında dosyaya baktığınızda uzunluğu 0 bayttır; çünkü TFileStream.Create(OutputFileName, fmCreate), doğrulayıcı girdiyi daha görmeden bir satır önce o yolun içini boşaltmıştır. Baytları bellekte onarmak işin kolay yarısıdır. Onları diske, hiçbir zaman sıfır uzunluklu, yarım yazılmış ya da yanlış izinli bir dosya bırakmadan geri koymak ise öteki yarısıdır ve bunun da kendi arıza biçimleri vardır
Yerinde bir onarım dosyayı neden boşaltır?
Yerinde onarım dosyayı, işlem sırası yanlış olduğu için boşaltır: fmCreate açılışta içeriği keser, dolayısıyla açıştan sonra gelen her ret çok geçtir. InputFileName ile OutputFileName aynı yol olduğunda reddedilen bir girdi, kaybolmuş bir belgeye dönüşür. Özenli bir ayrıştırıcı sizi kurtaramaz. Belirsiz işaretleri reddettiğinde kendi çıktı akışına dokunmayabilir, ama çağıran hedefi bir çağrı önce imha etmiştir
// Bariz sürüm: hiçbir şey doğrulanmadan hedef kesilir
Input := TMemoryStream.Create;
try
Input.LoadFromFile(InputFileName); // OutputFileName ile aynı yol
Output := TFileStream.Create(OutputFileName, fmCreate); // dosya şu an 0 bayt
try
if not MyRepairQDF(Input, Output) then // kendi doğrulayıcınız: hayır demek için çok geç
raise Exception.Create('QDF structure rejected');
finally
Output.Free;
end;
finally
Input.Free;
end;
Sıradaki alışılmış hamle, onarımı bir TMemoryStream içine yapmak ve çıktıyı onarım başarılı olduktan sonra açmaktır. Bu, kötü girdi deliğini kapatır; başka hiçbir şeyi kapatmaz. Yazma aşaması hâlâ önce kes sonra kopyala biçimindedir; dolayısıyla dolu bir disk, yarı yolda çıkan bir paylaşım ihlali ya da kesme ile son WriteBuffer arasındaki bir istisna hedefi yine hasarlı bırakır. Ayrıştırma ile yayımlama iki ayrı sınırdır ve her birinin kendi koruması gerekir
PDF Library for Delphi'de ikisini de TPDFlib.RepairQDFFile(InputFileName, OutputFileName) üstlenir. Girdiyi okur, tamamen bellekte onarır ve QDF yapısı reddedilirse hedefte hiçbir şey açmadan erken döner. Yalnızca başarılı bir onarım yayımlamaya ilerler ve yayımlama hedefi yazmak için hiç açmaz
Onarılmış baytlar ile hedef arasındaki dört adım
RepairQDFFile, sürecin kendisinin gözleyebildiği her arıza için hedef yolun ya eksiksiz eski dosyayı ya da eksiksiz yeni dosyayı tutacağını garanti eder. Buna dört adımda ulaşır ve hiçbiri, bir önceki bitmeden başlamaz. Hedef, GetFullPathNameW ile tam yola çözülür; fonksiyon önce gerekli uzunluğu öğrenmek için, sonra da tam o boyutta bir arabelleğe çağrılır, böylece uzun bir yol MAX_PATH'te kırpılmaz. Ardından hedef dizinde .pdflib-qdf- artı bir GUID artı .tmp adında geçici bir dosya oluşturulur: Windows'ta CreateFileW ve CREATE_NEW, POSIX'te O_CREAT or O_EXCL ve 0600 kipiyle open(2) kullanılır. Her iki bayrak da ad zaten varsa oluşturmayı başarısız kılar. Bir şekilde aynı GUID'yi çeken iki süreç, bir tanıtıcıyı paylaşarak çıkamaz
Onarılmış akış ardından WriteBuffer üzerinden 64 KiB'lik parçalar hâlinde kopyalanır; fonksiyon, kimsenin denetlemediği bir sayı döndürmek yerine kısa yazmada istisna fırlatır. FlushFileBuffers ya da fsync(2) veriyi diske indirir ve tanıtıcı kapatılır. Kapatma bilinçli olarak istisna fırlatmaz: flush bir kez başarılı olduktan sonra başarısız bir kapatma veri için tehdit değildir ve oradaki bir istisna, gerçek yazma ya da flush hatasını teşhis edecek kişiden gizlerdi. Son adım yeniden adlandırmadır
Yeniden adlandırma, ev yapımı güvenli kaydetme yordamlarının çoğunun kırıldığı adımdır. Windows'ta geçici dosya, hedefin yerini MoveFileExW ile MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH bayraklarıyla alır; aynı birimde bu tek bir dosya sistemi işlemidir. MOVEFILE_COPY_ALLOWED bilinçli olarak dışarıda bırakılır. Birimler arası taşıma sessizce kopyala-sonra-sil'e dönüşür; tasarımın bütünüyle kaçındığı atomik olmayan dizi tam olarak budur ve makine giderken yarısı bitmiş "başarılı" bir kopyadan çok, reddedilen bir rename görmeyi yeğlerim. Geçici dosya hedef dizinde durduğu için zaten hedef birimdedir. Eski dosya önce silinmez; sil-sonra-adlandır çifti, yolun hiç var olmadığı bir pencere bırakır ve o pencerenin içindeki bir çökme belgeyi kaybettirir. MOVEFILE_WRITE_THROUGH, rename diske ulaşmadan çağrının dönmesini engeller. POSIX'te rename(2) var olan bir adın yerini zaten atomik olarak alır ve aynı dizine yerleştirme, çağrıyı EXDEV'den uzak tutar
Temizlik her yolda bir finally bloğunda çalışır. Başarılı bir rename'den sonra geçici ad artık var olmadığından silme hiçbir şey yapmaz; başarısızlıkta ise kısmi dosyayı kaldırır. Dizin, çağrıdan önce sahip olduğu dosyalarla birebir aynı hâle gelir
Geçici bir dosya Windows'ta izinleri neden gevşetir?
Boş bir güvenlik tanımlayıcısıyla oluşturulan dosya DACL'sini üst dizinden devralır; yerini alacağı dosyanın bunda söz hakkı yoktur. Yepyeni bir belge için bu varsayılan doğrudur. Yerinde onarım için yanlıştır. Bir operatörün contract.pdf'i korumalı, devralınmayan bir DACL ile tek bir hesaba kilitleyiverdiğini düşünün. Yanındaki sıradan bir geçici dosya dizinin daha geniş izinlerini devralır; rename'den sonra yayımlanan contract.pdf o geniş DACL'yi taşır, çünkü NTFS güvenliği adda değil dosya nesnesindedir. Baytlar doğrudur, dönüş değeri 1'dir. Operatörün kurduğu erişim kısıtlaması ise sade bir şekilde yok olmuştur
RepairQDFFile bu yüzden geçici dosyayı oluşturmadan önce hedefin DACL'sini okur ve CreateFileW'ye lpSecurityAttributes üzerinden verir; böylece yeni dosya eski dosyanın izinleriyle doğar ve rename, operatörün fark edeceği hiçbir şeyi değiştirmez. Okuma, DACL_SECURITY_INFORMATION ile GetFileSecurityW'dir; arabellek ilk çağrının ERROR_INSUFFICIENT_BUFFER yanıtına göre boyutlanır. Hedef henüz yokken — ayrı bir çıktı yolu için normal durum budur — tanımlayıcı verilmez ve yeni dosya, her öbür yeni dosya gibi kendi dizininden devralır
Üç koşul, kodu tahmin yürütmek yerine yayımlamayı durdurur ve her biri LastErrorCode 305 bildirir:
- DACL okunamaz; örneğin çağıran hesabın hedef üzerinde
READ_CONTROLhakkı yoktur - Dönen tanımlayıcıda
SE_DACL_PRESENTset değildir; böyle bir tanımlayıcıyıCreateFileW'ye vermek, Windows'un süreç varsayılan DACL'sine geri düşmesine ve kimsenin değişmesini istemediği erişim semantiğini değiştirmesine yol açardı - Hedef
FILE_ATTRIBUTE_ENCRYPTEDtaşır; geçici dosya düz metin olurdu ve onu EFS korumalı bir dosyanın üzerine adlandırmak, kullanıcının şifrelemeyi seçtiği bir şeyin şifresiz bir ikamini yayımlamak olurdu
EFS bir dosya sistemi özelliğidir ve PDF güvenlik işleyicileriyle ilgisi yoktur; onlar şifreli belge yükleme yazısında ele alınır. Yine de arıza biçimi aynı tür sessiz düşüştür ve onu reddetmek doğru varsaylandır. EFS payında ara sıra hayır diyen bir onarım bir destek kaydıdır. Kullanıcının şifrelediği bir dosyanın şifresini çözen onarım ise bir olaydır
Kendiniz bir DACL koruma testi yazacaksanız bir Windows ayrıntısı, kolayca yanlış kurulan cinstendir. Kısıtlı test dosyasını kurmak için yalnızca sahibine uygulanan bir DACL verin ve tanımlayıcı denetiminde SE_DACL_PROTECTED'i açıkça set edin; SetFileSecurityW'ye PROTECTED_DACL_SECURITY_INFORMATION geçirmek, korumasız bir tanımlayıcıyı korumalıya çevirmez. Ardından yayımlanan dosyanın hâlâ korumalı biti ve açık, boş olmayan bir DACL bildirdiğini doğrulayın; bir kez ayrı çıktı yolu için, bir kez de kaynağın kendi üzerinden onarım için
Hangi LastErrorCode neyin başarısız olduğunu söyler?
RepairQDFFile başarıda 1, başarısızlıkta 0 döndürür ve reddeden aşamayı TPDFlib.LastErrorCode adlandırır. Var olmayan, açılamayan ya da 2 GB'den büyük bir girdi 401 bildirir; okuma sırasındaki bir istisna da yazma koduna sızmak yerine 401'e eşlenir. O yinelenen stream işareti gibi geçersiz ya da belirsiz bir QDF yapısı PDFLIB_ERROR_QDF_REPAIR (107) bildirir; hedef hiç açılmamıştır. Yol çözümünden flush ve rename'e kadar onarımdan sonraki her şey PDFLIB_ERROR_QDF_WRITE (305) bildirir. Gerçekçi yazma arızalarının hepsi 305'e düşer: silme paylaşımı olmadan başka bir tanıtıcıyla açık tutulan bir hedef, salt okunur bir hedef, olmayan bir hedef dizini, bir de yukarıdaki DACL ve EFS retleri. 305 olan her durumda geride yeni ya da kısmi bir hedef kalmaz
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
// Yerinde onarım: aynı yol hem girdi hem çıktı
if Pdf.RepairQDFFile('edited.qdf.pdf', 'edited.qdf.pdf') = 1 then
Log('published; the previous bytes were replaced in one rename')
else
case Pdf.LastErrorCode of
401: Log('could not read the input; it was not modified');
PDFLIB_ERROR_QDF_REPAIR:
Log('QDF structure rejected; the destination was never opened');
PDFLIB_ERROR_QDF_WRITE:
Log('write, flush or replace refused; the destination keeps its old bytes');
end;
finally
Pdf.Free;
end;
end;
305 yolu, hiçbir dosya API'sini kancalamadan kendi test projenizden kolayca yeniden üretilir. Delphi'nin fmShareDenyNone'u bir dosyayı okuma ve yazma paylaşımıyla ama FILE_SHARE_DELETE olmadan açar ve Windows, bu şekilde tutulan bir dosyanın üzerine hiçbir şeyin adlandırılmasını reddeder. Böyle bir tanıtıcıyı çağrı boyunca açık tutun; rename, veri yazılıp flush edildikten sonraki en son adımda başarısız olur — en çok işlendiğini görmek istediğiniz durum da tam olarak budur
var
Before: TBytes;
Holder: TFileStream;
Search: TSearchRec;
begin
Before := TFile.ReadAllBytes(Target); // System.IOUtils
Holder := TFileStream.Create(Target, fmOpenRead or fmShareDenyNone);
try
Assert(Pdf.RepairQDFFile(Source, Target) = 0);
Assert(Pdf.LastErrorCode = PDFLIB_ERROR_QDF_WRITE);
finally
Holder.Free;
end;
// Bu denetimler kütüphane çağrısı değil, kendi testiniz için önerilerdir
Assert(BytesEqual(Before, TFile.ReadAllBytes(Target))); // BytesEqual: sizin yardımcınız
// Geride geçici dosya kalmamalı
if FindFirst(ExtractFilePath(Target) + '.pdflib-qdf-*.tmp',
faAnyFile, Search) = 0 then
try
Assert(False, 'temporary file left behind: ' + Search.Name);
finally
FindClose(Search);
end;
end;
0 dönüş koduna tek başına güvenmek yerine kodu okuma alışkanlığı için, kütüphanedeki sessiz arızaları tanılama yazısı aynı örüntüyü API'nin geri kalanında da ele alır
Garantinin nerede bittiği
Vaat, sürecin görebildiği arızaları kapsar. Süreç, geçici dosyanın oluşturulması ile rename arasında öldürülürse finally bloğu hiç çalışmaz ve dizinde bir .pdflib-qdf-<GUID>.tmp dosyası kalır. Hedef hâlâ sağlamdır; önemli olan da budur, ama o kalıntıyı süpürmek size düşer. Elektrik kesintisi bir dosya sistemi sorusudur: veri flush edilir, rename write-through'dur; yine de POSIX'te dizin girdisi ayrıca sync edilmez, dolayısıyla dayanıklılık dosya sisteminin bunun üzerine ne sunduysa odur
Başarılı bir rename aynı zamanda yeni bir dosya nesnesi üretir ve eskinin birkaç özelliği onunla birlikte gelmez. DACL bilinçli olarak taşınır. Sahiplik taşınmaz; yalnızca DACL_SECURITY_INFORMATION okunduğu için yayımlanan dosyayı onarımı çalıştıran hesap sahibilenir. Geçici dosya FILE_ATTRIBUTE_NORMAL ile oluşturulur; gizli, arşiv ve benzeri öznitelik bitleri sıfırdan başlar ve eski dosyadaki alternate data stream'ler kaybolur. POSIX'te yayımlanan dosya geçici dosyanın 0600 kipini taşır; bir grubun okuma erişimi gerekiyorsa sonradan chmod çağırın. Hedefin öznitelikleri ve DACL'si, geçici dosya daha var olmadan önce okunur; aynı yola ikinci bir yazıcıyı eşgüdümlemek size düşer
SaveQDFToFile ve ConvertFileToQDF doğrudan yazar
Geçici bir dosya üzerinden yayımlayan tek QDF giriş noktası RepairQDFFile'dır. SaveQDFToFile ile ConvertFileToQDF çıktılarını fmCreate ile açar ve dönüşümü doğrudan içine akıtır; bir akışa güncelleme ekleme yazısındaki artımlı yolun, kendisine verdiğiniz akışa yazması gibi. Ayrım bilinçlidir. O iki çağrı, çoktan yüklenip doğrulanmış bir belgeyi yeni bir hata ayıklama çıktısına çevirir; yarı yolda reddedilebilecek güvenilmeyen bir girdi yoktur ve çıktı yolları çoğunlukla tazedir. RepairQDFFile ise elle düzenlenmiş, güvenilmeyen bir dosya alır ve rutin olarak aynı yola geri işaretlenir; ek makineyi hak eden bileşim budur
Bir QDF dökümü için atomik değiştirmeyi gerçekten istediğinizde, kendi yazacağınız kısa bir sarmalayıcı yeter:
// Kendi yardımcınız: QDF'yi hedefin yanına yaz, sonra tek bir rename ile değiştir
function SaveQDFReplacing(Pdf: TPDFlib; const FileName: string): Boolean;
var
ID: TGUID;
TempName: string;
begin
CreateGUID(ID);
TempName := ExtractFilePath(ExpandFileName(FileName)) +
'.myapp-qdf-' + GUIDToString(ID) + '.tmp';
try
Result := (Pdf.SaveQDFToFile(TempName) = 1) and
MoveFileExW(PWideChar(WideString(TempName)), PWideChar(WideString(FileName)),
MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH);
finally
if FileExists(TempName) then
DeleteFile(TempName); // yalnızca bir şey başarısız olduğunda bulunur
end;
end;
Bu sürüm, RepairQDFFile'ın yaptığı DACL kopyasını ve açık veri flush'ini atlar. Çalışma ya da hata ayıklama klasörüne düşen QDF dökümleri için makinenin tam doğru dozda olduğunu düşünüyorum; döküm, kilitli izinli bir dosyanın yerine geçecekse işi onarım yolundan geçirin ya da güvenlik tanımlayıcısını kendiniz kopyalayın
Atomik onarım çıktısı, QDF dönüşüm çağrıları ve yukarıdaki hata kodlarının tümü PDF Library for Delphi ile gelir; bu blogda başka yazılarda ele alınan çapraz başvuru kurtarma, artımlı güncelleme ve şifreleme desteğinin yanında