Artykuł techniczny

Wczytywanie zakresu bajtów osadzonego PDF w Delphi z PDFium

PDFium Component potrafi otworzyć PDF żyjący wewnątrz większego bufora bezpośrednio z zakresu bajtów. Przeciążenie LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresuje okno w miejscu, więc nie jest potrzebne wstępne Copy. W zamian prosi, byś zrozumiał jedną regułę: gdy Buffered jest False, tablica bazowa jest pożyczona, nie kopiowana

To inny mechanizm niż podejście oparte na callbackach opisane w strumieniowaniu dużych plików PDF na żądanie z PDFium VCL, które podaje PDFium czytnik FPDF_FILEACCESS i pozwala mu pobierać bloki z dysku w miarę potrzeb. Tamto jest dla dokumentów zbyt dużych, by zmieściły się w RAM. To jest dla dokumentów już w RAM, siedzących pod znanym przesunięciem wewnątrz czegoś innego. Oba się uzupełniają, a ostatnia sekcja wyjaśnia, która sytuacja pasuje do którego

Kopia 40 MB, o którą nikt nie prosił

Scenariusz pojawia się wszędzie tam, gdzie PDF-y podróżują wewnątrz innych formatów. Magazyn poczty trzyma treści wiadomości i załączniki w jednym rekordzie. Kontener archiwum sczepia manifest, kilka obrazów i PDF. Niestandardowy protokół sieciowy ramkuje dokument za nagłówkiem z prefiksem długości. W każdym przypadku kończysz z jednym dużym TBytes i wiedzą, że PDF zaczyna się pod bajtem 1 182 336 i trwa przez 312 kilobajtów

Zanim istniało przeciążenie zakresu bajtów, idiomatyczną odpowiedzią było Copy(Data, Index, Count), które alokuje drugą tablicę i memcopy'uje okno do niej. Podajesz potem ten wycinek do LoadDocument z Buffered = True, co kopiuje go ponownie do prywatnego bufora komponentu. Dwie kopie tych samych bajtów, jedna z nich czystą ceremonią, i przy dużym skanie skrzynki pocztowej powtarzane dla każdej wiadomości. Przeciążenie zakresu bajtów usuwa pierwszą kopię bezwarunkowo, a drugą opcjonalnie

Co faktycznie robi przeciążenie zakresu bajtów

Przeciążenie jest z projektu cienkie: waliduje, oblicza jeden wskaźnik i deleguje do formy wskaźnikowej LoadDocument, przez którą już przechodzi cała rodzina. Index jest liczony od zera, Count to długość w bajtach, a Buffered domyślnie ma wartość True, dokładnie tak jak w innych przeciążeniach. Jednoargumentowe LoadDocument(const Data: TBytes; Buffered: Boolean) jest teraz samo w sobie wywołaniem tego z Index = 0 i Count = Length(Data), więc jest jedna ścieżka walidacji zamiast dwóch

Wywołanie wygląda jak kod, który już pisałeś, minus wycinanie

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;

Dlaczego Index plus Count przepełnia sprawdzenie granic?

Ponieważ Index i Count są oba Integer, a suma dwóch dużych dodatnich wartości Integer niekoniecznie jest dużą dodatnią wartością Integer. To techniczny rdzeń przeciążenia i jedyne miejsce, gdzie naturalnie wyglądające sprawdzenie jest dziurą bezpieczeństwa pamięci. Oczywiste sformułowanie jest błędne

// 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');

Prześledź przypadek zawodzący. Weź Index = 2000000000 i Count = 2000000000. Ich prawdziwa suma to cztery miliardy, ale w 32-bitowej arytmetyce ze znakiem wynik zawija się do dokładnie minus 294 967 296. Ta wartość jest wygodnie mniejsza niż Length(Data), więc błędne sprawdzenie przechodzi, @Data[Index] jest brane daleko poza tablicą, a PDFium dostaje dziki wskaźnik plus dwugigabajtową długość. To, co następuje, to naruszenie dostępu w dobry dzień i ciche parsowanie niepowiązanej pamięci procesu w zły

Poprawna kolejność naprawia to, nigdy nie dodając. Wartości ujemne są odrzucane, zanim cokolwiek jest indeksowane, więc @Data[Index] nigdy nie może być wzięte poniżej tablicy. Potem Index jest ograniczany samodzielnie wobec Length(Data), co gwarantuje, że Length(Data) - Index jest nieujemnym Integer. Dopiero wtedy Count jest porównywany z tą resztą. Każda wartość pośrednia pozostaje wewnątrz reprezentowalnego zakresu, więc żadna konfiguracja buildu nie może zmienić wyniku. Nie kuś się też polegać na sprawdzaniu przepełnień {$Q+} jako siatce bezpieczeństwa: buildy release rutynowo są wysyłane z tym wyłączonym, a nawet gdy jest włączone, zamieniłeś błąd bezpieczeństwa pamięci na EIntOverflow uciekający ze środka procedury walidacji. PDFium Component traktuje niezaufaną arytmetykę długości tak samo jak resztę granicy, dyscyplinę omówioną szerzej w utwardzaniu ABI PDFium VCL i bezpieczeństwa pamięci w Delphi

Dlaczego okno o zerowej długości musi przekazać nil?

Ponieważ @Data[Index] nie jest legalnym wyrażeniem dla każdego Index, jaki akceptuje walidacja. Index = Length(Data) z Count = 0 to całkowicie poprawnie sformowane puste okno na końcu bufora, a pusty TBytes daje Index = 0 na tablicy, która w ogóle nie ma elementu zero. Wzięcie adresu w obu przypadkach indeksuje poza koniec albo dereferencjuje pustą tablicę dynamiczną. Przeciążenie więc się rozgałęzia: Count = 0 daje wskaźnik nil, jakikolwiek inny count daje @Data[Index]. Nil płynie wtedy do przeciążenia wskaźnikowego, którego własna strażnica akceptuje wskaźnik nil, gdy rozmiar wynosi zero, a wczytanie kończy się zwykłym błędem „Cannot load PDF document”, a nie naruszeniem dostępu. Wywołujący, który obliczył zerobajtowe okno z zniekształconego kontenera, dostaje czysty, przechwytywalny EPdfError jak każde inne złe dane wejściowe

Pożyczone czy skopiowane: co decyduje Buffered

Buffered wybiera kontrakt własności i jest jedynym parametrem tutaj, którego konsekwencje wykraczają poza samo wywołanie. Przy Buffered = True PDFium Component kopiuje wybrane okno, i tylko okno, do swojego wewnętrznego bufora przed wczytaniem. Kontener 40 MB nie jest kopiowany; PDF 312 KB jest. Gdy LoadDocument zwróci wynik, możesz od razu zwolnić, ponownie użyć albo nadpisać kontener, ponieważ komponent już się do niego nie odwołuje. To domyślne zachowanie i właściwy wybór dla niemal całego kodu

Buffered = False przekazuje @Data[Index] prosto do FPDF_LoadMemDocument64, a PDFium trzyma ten wskaźnik przez całe życie dokumentu zamiast kopiować bajty. To czyni wczytanie bezalokacyjnym i czyni cały bazowy TBytes zasobem pożyczonym. Musi pozostać żywy i niezmodyfikowany, dopóki UnloadDocument nie zostanie uruchomione albo Active nie stanie się False. Nie okno, cała tablica: tablica dynamiczna jest zliczana referencyjnie jako jednostka, a puszczenie ostatniej referencji gdziekolwiek w Twoim kodzie zwalnia pamięć, którą PDFium wciąż czyta. Ustawienie Length na niej jest równie fatalne, ponieważ realokacja może przenieść blok. Zapisz to w swojej własnej dokumentacji API wszędzie tam, gdzie udostępniasz takie wczytanie, w tym samym duchu co każda inna granica pożyczka-kontra-własność w kodzie Pascal; tryb awarii jest identyczny z zagrożeniami aliasingu opisanymi w wycieku FillChar i result string w Delphi, gdzie bufor wygląda na posiadany, a nie jest

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;

Kiedy okno zakresu bajtów jest złym narzędziem

Bądź uczciwy co do granicy. Przeciążenie zakresu bajtów zakłada, że kontener jest już w całości w pamięci, a Count jest Integer, więc pojedyncze okno nie może przekroczyć dwóch gigabajtów. Jeśli kontener to 6 GB archiwum na dysku, albo przychodzi przez gniazdo, którego nie możesz przewinąć, to przeciążenie Ci nie pomoże, a wczytanie całości do TBytes tylko po to, by zaadresować okno w środku, mija się z celem. To dokładnie tam, gdzie należy ścieżka FPDF_FILEACCESS, a artykuł o strumieniowaniu na żądanie pokazuje, jak udostępnić przesunięty widok pliku jako niestandardowe źródło dokumentu. Równie dobrze, jeśli osadzone bajty potrzebują transformacji przed pokazaniem PDFium, dekompresji, odszyfrowania, kroku rozpakowania, to prawdziwa kopia jest nieunikniona i Buffered = True na przetransformowanej tablicy jest uczciwą odpowiedzią. Okno zakresu bajtów opłaca się dokładnie w jednym kształcie: ciągłe, niezmodyfikowane bajty PDF, już rezydentne, pod znanym przesunięciem

Jeśli oceniasz to dla przeglądarki, panelu podglądu albo pipeline'u wsadowego przyjmowania plików, przeciążenie zakresu bajtów i strumieniowy loader to dwie ze strategii wczytywania, jakie PDFium Component dostarcza obok wczytywania z pliku, strumienia i surowego wskaźnika. Pełna powierzchnia API, licencjonowanie i wsparcie wersji Delphi i C++Builder są udokumentowane na stronie produktu PDFium Component