PDFium Component는 더 큰 버퍼 안에 들어 있는 PDF를 바이트 범위에서 바로 열 수 있습니다. 오버로드 LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean)는 창(window)을 제자리에서 다룹니다. 사전 Copy가 필요 없습니다. 그 대가로 하나의 규칙을 이해해달라고 요구합니다. Buffered가 False일 때 백업 배열은 복사되는 것이 아니라 빌려지는 것입니다
이는 PDFium VCL로 대용량 PDF를 요청 시 스트리밍하기에서 설명한 콜백 기반 접근과는 다른 메커니즘입니다. 그쪽은 PDFium에 FPDF_FILEACCESS 리더를 넘겨서 필요할 때마다 디스크에서 블록을 끌어오게 합니다. 그것은 RAM에 담기에는 너무 큰 문서를 위한 것입니다. 이것은 이미 RAM에 있고, 다른 무언가 안의 알려진 오프셋에 자리 잡은 문서를 위한 것입니다. 둘은 서로 보완적이며, 마지막 섹션에서 어느 상황이 어디에 속하는지 설명합니다
아무도 요청하지 않은 40MB 복사
이 시나리오는 PDF가 다른 형식 안에서 이동하는 곳이면 어디든 나타납니다. 메일 저장소는 메시지 본문과 첨부 파일을 하나의 레코드에 보관합니다. 아카이브 컨테이너는 매니페스트, 몇 개의 이미지, PDF를 연결합니다. 커스텀 와이어 프로토콜은 문서를 길이 접두사가 붙은 헤더 뒤에 프레임합니다. 어느 경우든 여러분은 커다란 TBytes 하나를 들고 있으며, PDF가 1,182,336번째 바이트에서 시작해서 312킬로바이트를 실행한다는 것을 알고 있습니다
바이트 범위 오버로드가 존재하기 전에는 관용적인 답이 Copy(Data, Index, Count)였고, 이는 두 번째 배열을 할당하고 그 창을 memcpy합니다. 그다음 여러분은 그 슬라이스를 Buffered = True로 LoadDocument에 넘기고, 이는 그것을 컴포넌트의 사설 버퍼로 다시 한번 복사합니다. 같은 바이트의 복사가 두 번이고 그중 하나는 순수한 의식(儀式)에 불과하며, 대형 메일함 스캔에서는 메시지마다 반복됩니다. 바이트 범위 오버로드는 첫 번째 복사를 무조건 없애고 두 번째 복사를 선택적으로 만듭니다
바이트 범위 오버로드가 실제로 하는 일
이 오버로드는 설계상 얇습니다. 검증하고, 포인터 하나를 계산한 뒤, 전체 계열이 이미 모두 통과하는 포인터 형태의 LoadDocument로 위임합니다. Index는 0부터 시작하고, Count는 바이트 길이이며, Buffered는 다른 오버로드에서와 정확히 똑같이 기본값이 True입니다. 단일 인자 LoadDocument(const Data: TBytes; Buffered: Boolean) 자체도 이제는 Index = 0, Count = Length(Data)로 이것을 호출하는 것일 뿐이므로, 검증 경로가 두 개가 아니라 하나입니다
이것을 호출하는 코드는 여러분이 이미 작성하고 있던 코드에서 슬라이스만 뺀 모습입니다
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;
Index와 Count의 합은 왜 경계 검사를 오버플로하는가
Index와 Count가 둘 다 Integer이고, 두 개의 큰 양수 Integer 값의 합이 반드시 큰 양수 Integer는 아니기 때문입니다. 이것이 오버로드의 기술적 핵심이며, 자연스러워 보이는 검사가 메모리 안전 구멍이 되는 유일한 지점입니다. 명백해 보이는 공식은 틀렸습니다
// 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');
실패하는 경우를 따라가 보십시오. Index = 2000000000, Count = 2000000000이라고 합시다. 진짜 합은 40억이지만, 32비트 부호 있는 산술에서 결과는 정확히 마이너스 294,967,296으로 랩어라운드됩니다. 그 값은 Length(Data)보다 편하게 작으므로, 잘못된 검사는 통과하고, @Data[Index]는 배열을 훨씬 벗어난 곳에서 취해지며, PDFium은 제멋대로인 포인터와 2기가바이트 길이를 넘겨받습니다. 이어지는 것은 운이 좋으면 액세스 위반이고, 운이 나쁘면 무관한 프로세스 메모리를 조용히 파싱하는 것입니다
올바른 순서는 절대 더하지 않음으로써 이를 고칩니다. 음수는 어떤 것도 인덱싱되기 전에 거부되므로, @Data[Index]는 배열 아래로는 절대 취해질 수 없습니다. 그다음 Index는 Length(Data)에 대해 단독으로 경계 지어지며, 이는 Length(Data) - Index가 음수가 아닌 Integer임을 보장합니다. 그런 다음에야 Count가 그 나머지와 비교됩니다. 모든 중간값은 표현 가능한 범위 안에 머물러 있으므로, 어떤 빌드 구성도 결과를 바꿀 수 없습니다. {$Q+} 오버플로 검사를 안전망으로 삼고 싶은 유혹도 뿌리치십시오. 릴리스 빌드는 흔히 이를 끄고 출하되며, 켜져 있더라도 여러분은 메모리 안전 버그를 검증 루틴 중간에서 빠져나오는 EIntOverflow로 바꾼 것에 불과합니다. PDFium Component는 신뢰할 수 없는 길이 산술을 경계의 나머지 부분과 똑같이 취급하며, 이 원칙은 PDFium VCL ABI와 메모리 안전 강화하기에서 더 폭넓게 다룹니다
왜 길이 0짜리 창은 nil을 넘겨야 하는가
@Data[Index]는 검증이 받아들이는 모든 Index에 대해 합법적인 표현식이 아니기 때문입니다. Count = 0인 Index = Length(Data)는 버퍼 꼬리에 있는 완벽하게 형식이 맞는 빈 창이고, 빈 TBytes는 원소 0이 아예 없는 배열에서 Index = 0을 냅니다. 둘 중 어느 쪽에서든 주소를 취하면 끝을 넘어서 인덱싱하거나 nil 동적 배열을 역참조하게 됩니다. 그래서 오버로드는 분기합니다. Count = 0은 nil 포인터를 내고, 그 밖의 개수는 @Data[Index]를 냅니다. nil은 그다음 포인터 오버로드로 흘러들어가는데, 그 오버로드 자체의 가드는 크기가 0일 때 nil 포인터를 받아들이고, 로드는 액세스 위반이 아니라 여느 잘못된 입력과 마찬가지로 평범한 "Cannot load PDF document" 오류로 끝납니다. 손상된 컨테이너에서 0바이트 창을 계산해낸 호출자는 깔끔하고 잡을 수 있는 EPdfError를 얻습니다
빌리거나 복사하거나: Buffered가 결정하는 것
Buffered는 소유권 계약을 선택하며, 이 호출에서 호출을 넘어서는 결과를 가진 유일한 매개변수입니다. Buffered = True이면 PDFium Component는 선택된 창만을, 그리고 오직 그것만을 로드하기 전에 자신의 내부 버퍼로 복사합니다. 40MB 컨테이너는 복사되지 않습니다. 312KB PDF만 복사됩니다. LoadDocument가 반환된 뒤에는 컨테이너를 즉시 해제하거나 재사용하거나 덮어써도 되는데, 컴포넌트가 더 이상 그것을 참조하지 않기 때문입니다. 이것이 기본값이며 거의 모든 코드에 맞는 선택입니다
Buffered = False는 @Data[Index]를 FPDF_LoadMemDocument64에 곧바로 넘기며, PDFium은 바이트를 복사하는 대신 문서의 수명 동안 그 포인터를 유지합니다. 이는 로드를 할당 없는 작업으로 만들지만, 백업 TBytes 전체를 빌린 자원으로 만듭니다. 이것은 UnloadDocument가 실행되거나 Active가 False가 될 때까지 살아 있고 변경되지 않아야 합니다. 창만이 아니라 배열 전체입니다. 동적 배열은 단위로 참조 카운트되므로, 여러분 코드 어디에서든 마지막 참조를 놓아버리면 PDFium이 여전히 읽고 있는 메모리가 해제됩니다. 그 위에서 Length를 설정하는 것도 재할당이 블록을 이동시킬 수 있으므로 똑같이 치명적입니다. 이런 로드를 노출하는 여러분 자신의 API 문서에도 이를 명시하십시오. Pascal 코드의 다른 어떤 빌림 대 소유 경계에서와 같은 정신입니다. 실패 형태는 Delphi의 FillChar와 결과 문자열 누수에서 설명하는 앨리어싱 위험, 즉 버퍼가 소유된 것처럼 보이지만 실제로는 그렇지 않은 경우와 동일합니다
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;
바이트 범위 창이 맞지 않는 도구일 때
경계에 대해 솔직해집시다. 바이트 범위 오버로드는 컨테이너가 이미 완전히 메모리에 있다고 가정하며, Count는 Integer이므로 단일 창은 2기가바이트를 넘을 수 없습니다. 컨테이너가 디스크상의 6GB 아카이브이거나 되감을 수 없는 소켓을 통해 도착한다면, 이 오버로드는 도움이 되지 않으며, 그 안의 창을 다루기 위해서만 전체를 TBytes로 읽어들이는 것은 요점을 무너뜨립니다. 이것이 바로 FPDF_FILEACCESS 경로가 속하는 지점이며, 온디맨드 스트리밍 글은 파일에서 오프셋이 이동된 뷰를 커스텀 문서 소스로 노출하는 방법을 보여줍니다. 마찬가지로, 임베딩된 바이트가 PDFium에 도달하기 전에 압축 해제, 복호화, 언래핑 단계와 같은 변환이 필요하다면, 진짜 복사는 피할 수 없으며 변환된 배열에 대한 Buffered = True가 정직한 답입니다. 바이트 범위 창은 정확히 하나의 형태에서 값어치를 합니다. 연속적이고, 변경되지 않은 PDF 바이트가, 이미 상주하고 있으며, 알려진 오프셋에 있는 경우입니다
뷰어, 미리보기 창, 또는 배치 인테이크 파이프라인을 위해 이를 검토 중이라면, 바이트 범위 오버로드와 스트리밍 로더는 PDFium Component가 파일, 스트림, 원시 포인터 로드와 함께 제공하는 로딩 전략들 중 두 가지입니다. 전체 API 표면, 라이선스, Delphi와 C++Builder 버전 지원은 PDFium Component 제품 페이지에 문서화되어 있습니다