PDFium Component có thể mở một PDF nằm bên trong một buffer lớn hơn trực tiếp từ một dải byte. Overload LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) địa chỉ hóa một cửa sổ tại chỗ, nên không cần Copy sơ bộ nào. Đổi lại nó yêu cầu bạn hiểu một quy tắc: khi Buffered là False, mảng nền được mượn, không được copy
Đây là một cơ chế khác với cách tiếp cận điều khiển bằng callback được mô tả trong stream PDF lớn theo yêu cầu với PDFium VCL, nơi đưa cho PDFium một trình đọc FPDF_FILEACCESS và để nó kéo các khối từ đĩa khi cần. Cái đó dành cho các tài liệu quá lớn để giữ trong RAM. Cái này dành cho các tài liệu đã ở trong RAM, nằm tại một offset đã biết bên trong thứ gì đó khác. Hai cái bổ sung cho nhau, và phần cuối bài giải thích tình huống nào thuộc về cái nào
Bản copy 40 MB không ai yêu cầu
Kịch bản này xuất hiện ở bất cứ đâu PDF di chuyển bên trong các định dạng khác. Một kho lưu trữ mail giữ nội dung tin nhắn và tệp đính kèm trong một bản ghi. Một container lưu trữ nối một manifest, vài ảnh và một PDF. Một giao thức truyền dữ liệu tùy chỉnh đóng khung một tài liệu sau một header có tiền tố độ dài. Trong mọi trường hợp, bạn đều kết thúc với việc giữ một TBytes lớn và biết rằng PDF bắt đầu tại byte 1.182.336 và chạy dài 312 kilobyte
Trước khi overload dải byte tồn tại, câu trả lời quen thuộc là Copy(Data, Index, Count), hàm này cấp phát một mảng thứ hai và memcpy cửa sổ vào đó. Sau đó bạn đưa lát cắt đó cho LoadDocument với Buffered = True, hàm này copy nó lần nữa vào buffer riêng của component. Hai bản copy của cùng các byte, một trong số đó thuần túy nghi thức, và trên một lần quét hộp thư lớn lặp lại cho mỗi tin nhắn. Overload dải byte loại bỏ bản copy đầu tiên vô điều kiện và bản thứ hai một cách tùy chọn
Overload dải byte thực sự làm gì
Overload này mỏng theo thiết kế: nó xác thực, tính một con trỏ, và ủy quyền cho dạng con trỏ của LoadDocument mà cả họ đã đi qua sẵn. Index tính từ không, Count là một độ dài byte, và Buffered mặc định là True y hệt như trên các overload khác. LoadDocument(const Data: TBytes; Buffered: Boolean) tham số đơn giờ tự nó chỉ là một lệnh gọi đến cái này với Index = 0 và Count = Length(Data), nên có một đường xác thực thay vì hai
Gọi nó trông giống code bạn đã viết sẵn, trừ đi phần cắt lát
var
Frame: TBytes; // whole container record, tens of megabytes
Offset, Size: Integer;
begin
Frame := LoadContainerRecord('mailbox.dat');
LocateEmbeddedPdf(Frame, Offset, Size); // your container parser
// No Copy(Frame, Offset, Size) here - the window is addressed in place
Pdf.LoadDocument(Frame, Offset, Size, True);
try
RenderPreview(Pdf);
finally
Pdf.UnloadDocument;
end;
end;
Vì sao Index cộng Count làm tràn phép kiểm tra biên?
Vì Index và Count đều là Integer, và tổng của hai giá trị Integer dương lớn không nhất thiết là một Integer dương lớn. Đây là lõi kỹ thuật của overload này, và là nơi duy nhất mà một phép kiểm tra trông có vẻ tự nhiên lại là một lỗ hổng an toàn bộ nhớ. Công thức hiển nhiên là sai
// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
DataPtr := @Data[Index];
// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
Check(Index >= 0, 'PDF byte range index cannot be negative');
Check(Count >= 0, 'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');
Hãy suy xét trường hợp thất bại. Lấy Index = 2000000000 và Count = 2000000000. Tổng thật của chúng là bốn tỷ, nhưng trong số học có dấu 32-bit, kết quả cuộn vòng thành đúng âm 294.967.296. Giá trị đó nhỏ hơn thoải mái so với Length(Data), nên phép kiểm tra sai thông qua, @Data[Index] được lấy vượt xa ra ngoài mảng, và PDFium được đưa cho một con trỏ hoang cùng một độ dài hai gigabyte. Điều tiếp theo là một access violation vào ngày may mắn và âm thầm phân tích bộ nhớ tiến trình không liên quan vào ngày xui xẻo
Thứ tự đúng sửa lỗi này bằng cách không bao giờ cộng. Số âm bị từ chối trước khi bất cứ thứ gì được đánh chỉ số, nên @Data[Index] không bao giờ có thể được lấy dưới mảng. Rồi Index được giới hạn riêng biệt so với Length(Data), đảm bảo Length(Data) - Index là một Integer không âm. Chỉ sau đó Count mới được so sánh với phần dư đó. Mọi giá trị trung gian đều nằm trong phạm vi biểu diễn được, nên không cấu hình build nào có thể thay đổi kết quả. Cũng đừng bị cám dỗ dựa vào kiểm tra tràn {$Q+} như một mạng lưới an toàn: các bản build release thường được vận chuyển với nó tắt, và ngay cả khi nó bật, bạn đã biến một lỗi an toàn bộ nhớ thành một EIntOverflow thoát ra từ giữa một thủ tục xác thực. PDFium Component xử lý số học độ dài không tin cậy theo cùng cách nó xử lý phần còn lại của ranh giới, một kỷ luật được trình bày rộng hơn trong tăng cường ABI PDFium VCL và an toàn bộ nhớ trong Delphi
Vì sao một cửa sổ độ dài bằng không phải truyền nil?
Vì @Data[Index] không phải một biểu thức hợp pháp cho mọi Index mà xác thực chấp nhận. Index = Length(Data) với Count = 0 là một cửa sổ rỗng hoàn toàn hợp lệ ở đuôi buffer, và một TBytes rỗng cho Index = 0 trên một mảng hoàn toàn không có phần tử số không. Lấy địa chỉ trong cả hai trường hợp đều đánh chỉ số vượt quá cuối, hoặc dereference một mảng động nil. Vì vậy overload phân nhánh: Count = 0 cho ra một con trỏ nil, bất kỳ count nào khác cho ra @Data[Index]. Nil sau đó chảy vào overload con trỏ, mà bản thân điều kiện canh gác của nó chấp nhận một con trỏ nil khi kích thước bằng không, và việc tải kết thúc bằng lỗi thông thường "Cannot load PDF document" thay vì một access violation. Một bên gọi tính ra một cửa sổ không byte từ một container hỏng nhận được một EPdfError sạch sẽ, có thể bắt được, như bất kỳ đầu vào xấu nào khác
Mượn hay copy: Buffered quyết định điều gì
Buffered chọn hợp đồng sở hữu, và nó là tham số duy nhất ở đây có hệ quả vượt ra ngoài lệnh gọi. Với Buffered = True, PDFium Component copy cửa sổ đã chọn, và chỉ cửa sổ đó, vào buffer nội bộ của nó trước khi tải. Container 40 MB không bị copy; PDF 312 KB bị copy. Ngay khi LoadDocument trả về, bạn có thể giải phóng, tái sử dụng hoặc ghi đè container ngay lập tức, vì component không còn tham chiếu đến nó nữa. Đây là mặc định và lựa chọn đúng cho hầu hết code
Buffered = False truyền thẳng @Data[Index] cho FPDF_LoadMemDocument64, và PDFium giữ con trỏ đó trong suốt vòng đời tài liệu thay vì copy các byte. Điều đó khiến việc tải không cần cấp phát, và khiến toàn bộ TBytes nền trở thành một tài nguyên được mượn. Nó phải sống và không bị sửa đổi cho đến khi UnloadDocument chạy hoặc Active thành False. Không phải chỉ cửa sổ, mà toàn bộ mảng: một mảng động được đếm tham chiếu như một đơn vị, và để tham chiếu cuối cùng biến mất ở bất cứ đâu trong code của bạn sẽ giải phóng bộ nhớ mà PDFium vẫn đang đọc. Đặt Length trên nó cũng gây tử vong tương tự, vì một lần cấp phát lại có thể di chuyển khối. Hãy nêu rõ điều này trong tài liệu API của riêng bạn ở bất cứ đâu bạn phơi bày một lần tải như vậy, cùng tinh thần với bất kỳ ranh giới mượn-so-với-sở-hữu nào khác trong code Pascal; kiểu thất bại này giống hệt các hiểm họa aliasing được mô tả trong rò rỉ FillChar và result string trong Delphi, nơi một buffer trông có vẻ được sở hữu nhưng không phải vậy
type
TFrameSession = class
private
FFrame: TBytes; // owns the backing storage for as long as FPdf is loaded
FPdf: TPdf;
public
procedure OpenEmbedded(Offset, Size: Integer);
destructor Destroy; override;
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
// Buffered = False: FFrame must outlive the loaded document
FPdf.LoadDocument(FFrame, Offset, Size, False);
end;
destructor TFrameSession.Destroy;
begin
FPdf.UnloadDocument; // release the borrow first
FFrame := nil; // only now may the storage go
inherited;
end;
Khi cửa sổ dải byte là công cụ sai
Hãy trung thực về ranh giới. Overload dải byte giả định container đã hoàn toàn nằm trong bộ nhớ, và Count là một Integer, nên một cửa sổ đơn không thể vượt quá hai gigabyte. Nếu container là một kho lưu trữ 6 GB trên đĩa, hoặc đến qua một socket bạn không thể tua lại, overload này không thể giúp bạn và việc đọc toàn bộ nó vào TBytes chỉ để địa chỉ hóa một cửa sổ bên trong nó đánh bại mục đích. Đó chính xác là nơi đường FPDF_FILEACCESS thuộc về, và bài về stream theo yêu cầu chỉ cách phơi bày một view dịch offset của một file như một nguồn tài liệu tùy chỉnh. Tương tự, nếu các byte nhúng cần biến đổi trước khi PDFium nhìn thấy chúng, giải nén, giải mã, một bước tháo gói, thì một bản copy thật là không thể tránh khỏi và Buffered = True trên mảng đã biến đổi là câu trả lời trung thực. Cửa sổ dải byte đem lại lợi ích trong đúng một tình huống: các byte PDF liền mạch, chưa sửa đổi, đã thường trú, tại một offset đã biết
Nếu bạn đang đánh giá điều này cho một trình xem, một khung xem trước hoặc một pipeline nhập liệu hàng loạt, overload dải byte và trình tải dạng stream là hai trong số các chiến lược tải mà PDFium Component đi kèm cùng với tải file, stream và con trỏ thô. Toàn bộ bề mặt API, giấy phép và hỗ trợ phiên bản Delphi và C++Builder được tài liệu hóa trên trang sản phẩm PDFium Component