PDFlibPas retry รหัสผ่านที่ผิดของ PDF ที่เข้ารหัสไว้ ด้วยการทิ้ง TPDFDocument ที่เพิ่งล้มเหลวและสร้างตัวใหม่ทั้งหมดสำหรับความพยายามครั้งถัดไป ขับเคลื่อนด้วย callback OnPassword (TPDFlibPasswordEvent) ที่รันได้สูงสุดสิบหกครั้งก่อนที่จะยอมแพ้ นั่นเป็นการเบี่ยงเบนที่จงใจจากสัญชาตญาณที่นักพัฒนา Delphi ส่วนใหญ่มักหยิบมาใช้ก่อน คือเก็บ document object ที่นั่งอยู่ในหน่วยความจำแล้วไว้ ป้อนรหัสผ่านที่แก้ไขแล้วเข้าไป และโหลดใหม่ตรงที่ แทนที่จะเริ่มต้นใหม่จากความว่างเปล่า loop การ retry ของ PDFlibPas ที่เพิ่มเข้ามาใน v3.245.0 ยืนอยู่ในตำแหน่งตรงข้าม ด้วยเหตุผลเฉพาะเจาะจงกับสิ่งที่ความพยายามรหัสผ่านที่ล้มเหลวทิ้งไว้เบื้องหลัง สถานการณ์เบื้องหลังมันเป็นเรื่องธรรมดาพอที่แอปพลิเคชัน Delphi ที่หนักด้านเอกสารส่วนใหญ่จะเจอในที่สุด หน้าจอรับเอกสารรับ PDF เข้ามา trailer ที่เข้ารหัสไว้บังคับให้เกิด dialog รหัสผ่าน ผู้ปฏิบัติงานพิมพ์ผิด และ dialog ก็ปรากฏขึ้นอีกครั้งสำหรับความพยายามครั้งที่สอง ไม่มีอะไรผิดปกติเกี่ยวกับประสบการณ์ผู้ใช้แบบนั้นเลย ดังนั้นโค้ดที่อยู่เบื้องหลังจึงต้องยอมรับรหัสผ่านผู้สมัครมากกว่าหนึ่งตัวสำหรับไฟล์เดียวกัน และมันต้องทำแบบนั้นอย่างปลอดภัย โดยไม่รั่วไหล state จากความพยายามที่ถูกปฏิเสธเข้าไปยังตัวที่ตามมา
ทำไมคุณ retry บน document object เดียวกันตรงๆ ไม่ได้
การใช้ TPDFDocument ซ้ำข้ามความพยายามรหัสผ่านใช้ไม่ได้ เพราะความพยายามที่ล้มเหลวได้รื้อ object นั้นลงไปแล้วภายใน แทนที่จะทิ้งมันไว้ใน state ที่หยุดชั่วคราวและกลับมาทำต่อได้ การเปิด PDF ที่เข้ารหัสไว้หมายถึงการ parse cross-reference table, การสร้าง reader ทับแหล่งข้อมูลข้างใต้ และการสร้าง crypt handler จากรหัสผ่านใดก็ตามที่ให้มา ทั้งหมดนี้ก่อนที่ PDFlibPas จะทดสอบได้ด้วยซ้ำว่ารหัสผ่านนั้นถูกต้องหรือไม่ เมื่อรหัสผ่านกลายเป็นว่าผิด รูทีนการโหลดภายในของเอกสารจะล้าง reader, cross-reference table และ crypt handler เป็นส่วนหนึ่งของการล้มเหลวออกมา ตามที่มันควรทำ ซึ่งหมายความว่าไม่มี parser ที่สร้างไว้ครึ่งเดียวนั่งรออยู่สำหรับรหัสผ่านที่แก้ไขแล้วในการเรียกครั้งที่สอง ขับเคลื่อน object เดียวกันนั้นผ่านความพยายามโหลดอีกครั้งอยู่ดี แล้วโหมดความล้มเหลวก็เป็นประเภทพอดีที่ debug ได้ยากมาก error ปรากฏขึ้นจาก internal state ที่สร้างขึ้นสำหรับการ parse ที่ล้มเหลวไปแล้วอีกตัวหนึ่ง โดยไม่มีอะไรชัดเจนที่ชี้กลับไปยังรหัสผ่านสามการเรียกก่อนหน้า PDFlibPas หลีกเลี่ยงปัญหาทั้งประเภทนี้ด้วยการไม่เคยพยายามกู้คืน document object เมื่อมันเปิดล้มเหลวไปแล้วเลย ทุกความพยายามได้ document ที่ไม่เคยเห็นรหัสผ่านที่ผิด รวมถึง reader และ cross-reference table ด้วย
callback OnPassword ขอรหัสผ่านถัดไปอย่างไร
TPDFlibPasswordEvent เป็นประเภท callback ที่ PDFlibPas เรียกผ่าน TPDFlib.LoadFromFile, LoadFromStream และ LoadFromString ทุกครั้งที่รหัสผ่านที่เพิ่งลองกลายเป็นว่าผิด และมันส่งสามอย่างให้ handler คือ ความพยายามไหนกำลังจะรัน, พารามิเตอร์ Password ให้เขียนทับด้วยผู้สมัครถัดไป และ flag Retry ที่ค่าเริ่มต้นเป็น false
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
รหัสผ่านที่ส่งเข้าการเรียก LoadFromFile เดิมนับเป็นความพยายามที่หนึ่ง ดังนั้นครั้งแรกที่ OnPassword ยิงเลย AttemptNumber จะมาถึงเป็น 2 ปล่อย Retry ไม่ตั้งค่า แล้วการโหลดจะล้มเหลวอย่างสะอาดด้วย LastErrorCode 404 ตั้งมันเป็น true แล้ว PDFlibPas จะลองอีกครั้งด้วยอะไรก็ตามที่ handler เพิ่งเขียนเข้า Password
ภายใน loop การ retry: TPDFDocument ใหม่สำหรับทุกความพยายาม
ภายใน PDFlibPas ตอบคำถามด้านวงจรชีวิต object แบบเดียวกันสำหรับ LoadFromFile, LoadFromStream และ LoadFromString ทุกความพยายาม รวมถึงครั้งแรก สร้าง TPDFDocument ใหม่ รันมันผ่านลำดับการเปิดที่สมบูรณ์ด้วยรหัสผ่านใดก็ตามที่ความพยายามนั้นใช้อยู่ และเก็บ object ไว้ก็ต่อเมื่อรหัสผ่านยืนยันเท่านั้น TPDFDocument ของความพยายามที่ถูกปฏิเสธถูก free ทันที พาเอา reader, cross-reference table และ crypt handler ของมันลงไปด้วย และความพยายามถัดไปเริ่มต้นใหม่ด้วย object ที่ไม่มีประวัติใดๆ เลย
// Simplified excerpt from inside LoadFromFile: every attempt gets a
// document that has never seen a previously rejected password. FileName,
// AttemptNumber and AttemptPassword come from the enclosing method.
Var
Doc: TPDFDocument;
LoadResult: TPLLoadResult;
Success: Boolean;
Begin
Success := False;
Repeat
Doc := TPDFDocument.Create;
Doc.DecodeMode := FDefaultDecodeMode;
Try
LoadResult := Doc.LoadFromFile(FileName, AttemptPassword);
Success := LoadResult = lrOkay;
if Success then
begin
FDocs.Add(Doc); // hand the verified document to the
Doc := nil; // caller's collection; skip the Free below
end;
Finally
Doc.Free; // a rejected attempt's reader, xref table
End; // and crypt handler are torn down right here
if Success or (LoadResult <> lrWrongPassword) then
Break; // success, or a non-password failure: stop
Inc(AttemptNumber);
Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;
บรรทัด Doc := nil นั้นก่อน block Finally คือสัญญาด้านวงจรชีวิต object ทั้งหมดในหนึ่งคำสั่ง เอกสารที่ล้มเหลวพา parser state ที่สร้างไว้ครึ่งเดียวลงหลุมไปกับมันโดยการออกแบบ และเอกสารที่สำเร็จเป็นตัวเดียวที่เคยถูกเพิ่มเข้า FDocs ซึ่งเป็น collection ที่ TPDFlib เก็บไว้สำหรับทุกเอกสารที่ผู้เรียกเปิดอยู่ ไม่มีอะไรจากความพยายามที่ถูกปฏิเสธที่มองเห็นได้จากภายนอก loop การ retry เลย ไม่มี reader ที่สร้างไว้ครึ่งเดียว, ไม่มีจำนวนหน้าที่ค้าง, ไม่มี crypt handler ที่สร้างจากคีย์ที่ผิด
PDFlibPas จะ retry รหัสผ่านที่ผิดกี่ครั้ง
PDFlibPas อนุญาตให้ลองรวมสิบหกครั้งต่อการเรียก LoadFromFile, LoadFromStream หรือ LoadFromString หนึ่งครั้ง โดยนับรหัสผ่านที่ส่งเข้าการเรียกเองเป็นความพยายามที่หนึ่ง OnPassword ยิงแค่สำหรับความพยายามที่สองถึงสิบหกเท่านั้น ซึ่งจำกัด callback ไว้ที่สิบห้าครั้ง ขอความพยายามครั้งที่สิบเจ็ดและ PDFlibPas จะปฏิเสธโดยไม่แม้แต่จะเรียก handler เลย ปล่อย Retry ไว้ที่ค่าเริ่มต้น false ณ จุดใดก็ตาม หรือใช้ครบสิบหกครั้งโดยไม่มีรหัสผ่านที่ถูกต้อง แล้ว LoadFromFile จะคืน 0 พร้อม LastErrorCode ตั้งเป็น 404 ซึ่งเป็นรหัสของ PDFlibPas สำหรับรหัสผ่านที่ถูกปฏิเสธ เพดานนี้มีอยู่ด้วยเหตุผลที่มากกว่าความเป็นระเบียบเรียบร้อย loop การ retry ที่ไม่มีขอบเขตเป็นวิธีง่ายๆ ที่จะเปลี่ยนรหัสผ่านที่พิมพ์ผิดหนึ่งครั้งให้กลายเป็น denial-of-service โดยไม่ตั้งใจต่อ thread ใดก็ตามที่กำลังรันการโหลด โดยเฉพาะเมื่อ handler ถูกต่อสายเข้ากับอะไรบางอย่างที่เป็นอัตโนมัติ เช่น รายการรหัสผ่านที่เคยเห็นมาก่อน แทนที่จะเป็นมนุษย์ที่คลิกผ่าน dialog PDFlibPas ยังเคารพ Abort ที่เรียกบน instance TPDFlib จากภายใน handler ด้วย เพราะ Sender มาถึงเป็น object เดียวกันนั้น มีประโยชน์อยู่หลังปุ่ม Cancel บน dialog รหัสผ่าน และหยุด loop การ retry ที่การตรวจสอบครั้งถัดไปไม่ว่า Retry จะถูกตั้งเป็นอะไรก็ตาม การโหลดที่ล้มเหลวด้วยเหตุผลอื่นที่ไม่ใช่รหัสผ่านผิด เช่น cross-reference table ที่เสียหาย จะไม่เข้า loop การ retry เลย PDFlibPas รายงาน LastErrorCode 401 และหยุดหลังความพยายามครั้งแรก เพราะไม่มีจำนวนการเดารหัสผ่านใดจะแก้ไฟล์ที่เสียหายเชิงโครงสร้างได้
loop การ retry ทำงานเหมือนกันสำหรับไฟล์, สตรีม และ string หรือไม่
callback OnPassword และเพดานสิบหกครั้งทำงานเหมือนกันเป๊ะข้าม LoadFromFile, LoadFromStream และ LoadFromString แม้ว่าจุดเข้าทั้งสามจะถือแหล่งข้อมูลของตัวเองต่างกันระหว่างความพยายาม path ไฟล์กลับมาเยี่ยมได้ถูก เพราะแต่ละความพยายามแค่เปิดไฟล์ที่ตั้งชื่อไว้ใหม่ และแหล่งข้อมูล string นั่งอยู่ในหน่วยความจำเป็นสำเนาของผู้เรียกเองอยู่แล้ว ดังนั้นทั้งสองจึงไม่ต้องการความช่วยเหลือจากผู้เรียกระหว่างการลองเลย สตรีมที่ผู้เรียกให้มาเป็นกรณีเดียวที่ควรค่าแก่การหยุดพิจารณา LoadFromStream seek สตรีมนั้นกลับไปตำแหน่งศูนย์และ copy มันภายในก่อนความพยายาม parse ครั้งแรก ดังนั้นทุกความพยายามที่ตามมา และ TPDFDocument ที่สร้างขึ้นใหม่เบื้องหลังมัน จะเล่นซ้ำจากสำเนาภายในนั้น แทนที่จะเป็นตำแหน่งใดก็ตามที่การ parse ที่ล้มเหลวทิ้งตำแหน่งของสตรีมไว้ ส่ง TFileStream หรือ TMemoryStream ให้ PDFlibPas สำหรับเอกสารที่ป้องกันด้วยรหัสผ่าน แล้วไม่ต้องย้อนกลับมันระหว่างการ retry เลย PDFlibPas คำนึงถึงตำแหน่งที่ความพยายามแรกที่ล้มเหลวอาจย้ายไปแล้ว
การใส่การ retry รหัสผ่านเข้ากับหน้าจอรับเอกสาร
workflow การรับเอกสารเป็นบ้านตามธรรมชาติสำหรับ callback นี้ เพราะมันเป็นรูปร่างของปัญหาที่ OnPassword ถูกสร้างมาเพื่อแก้พอดี ไฟล์มาถึงจากภายนอกแอปพลิเคชัน รหัสผ่านของมันไม่เป็นที่รู้แน่ชัดล่วงหน้า และคนที่ให้ผู้สมัครต้องการมากกว่าหนึ่งการเดา โดยไม่ต้องให้โค้ดรอบข้างเขียน loop การ retry ของตัวเองรอบ LoadFromFile
procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean);
var
Typed: string;
begin
// AttemptNumber counts from 2: the password already tried was attempt 1.
Typed := '';
Retry := InputQuery('Password required',
Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
if Retry then
Password := Typed;
// Retry is False when the operator cancels, which leaves
// LastErrorCode at 404 for the caller to report.
end;
procedure TIntakeForm.LoadInboundDocument;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.OnPassword := SupplyPassword;
if Lib.LoadFromFile('inbound-invoice.pdf', '') = 1 then
RegisterIntakeDocument(Lib) // only a verified document reaches here
else
LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
finally
Lib.Free;
end;
end;
RegisterIntakeDocument รับ Lib ก็ต่อเมื่อ LoadFromFile คืนค่า 1 แล้วเท่านั้น หมายความว่ารหัสผ่านตัวใดตัวหนึ่งในการแลกเปลี่ยนนั้นยืนยันกับ crypt handler ของไฟล์ได้จริงๆ ความพยายามที่ถูกปฏิเสธไม่เคยไปถึงบรรทัดนั้นเลย และเอกสารที่เปิดครึ่งเดียวก็เช่นกัน สิ่งที่ตามมาต่อไป เมื่อเอกสารแบบนี้ได้รับการยืนยันว่าเปิดแล้ว ควรค่าแก่การมองครั้งที่สองที่การตั้งค่าการป้องกันของมัน แทนที่จะสมมติว่ารหัสผ่านที่ใช้ได้คือเรื่องราวความปลอดภัยทั้งหมด การตรวจสอบว่า dictionary /Encrypt ของเอกสารประกาศอะไรจริงๆ ครอบคลุมการอ่านอัลกอริทึม, revision และบิตสิทธิ์ที่ PDFlibPas เปิดออกมาเมื่อไฟล์แบบนี้โหลดแล้ว
การ retry รหัสผ่านยังเป็นตัวอย่างที่แคบของวินัยที่กว้างกว่าที่ PDFlibPas ใช้ทั่วทั้งชั้นการ parse ของมัน ไฟล์ที่ยังไม่ได้พิสูจน์ตัวเองไม่ได้รับประโยชน์แห่งความสงสัยเลย ไม่ว่าคำถามจะเป็นว่ารหัสผ่านตัวไหนปลดล็อคมัน หรือฟิลด์ความยาวข้างในมันโกหกเกี่ยวกับขนาด buffer ที่มันต้องการหรือไม่ การเสริมความแข็งแกร่งให้ Pascal PDF parser ต้านไฟล์ประสงค์ร้าย ครอบคลุมอีกครึ่งหนึ่งของวินัยนั้น คือ decoder ที่ปฏิบัติต่อทุกโปรแกรมฟอนต์และสตรีมภาพใน PDF ขาเข้าเป็นอินพุตที่ประสงค์ร้าย แทนที่จะเป็นเอกสารที่มีรูปแบบถูกต้องซึ่งแค่ลืมรหัสผ่านของมัน
OnPassword และ loop การ retry เบื้องหลังมันเป็นส่วนหนึ่งของไลบรารี PDF PDFlibPas สำหรับ Delphi และ C++Builderรุ่นมาตรฐาน มีอยู่ทุกที่ที่ LoadFromFile, LoadFromStream หรือ LoadFromString มีอยู่แล้ว โดยไม่ต้องการโมดูลแยกหรือระดับใบอนุญาตแยกสำหรับเอกสารที่แค่ต้องการการเดารหัสผ่านครั้งที่สอง