Artigo Técnico

Erros de Limites (Range Check Errors) nas Bibliotecas PDF do Delphi: Causas Originais

Os erros de verificação de limites (range check errors) nas bibliotecas PDF em Delphi ganharam a fama de serem de difícil diagnóstico por não seguirem um padrão de entrada consistente. O mesmo documento exibe-os numa máquina enquanto não acusa nada noutra; a mesma trajetória de código lança uma exceção num ficheiro de 3 páginas e corre na perfeição noutro com 12 páginas. A dita inconsistência reporta na sua esmagadora maioria a uma e única causa na sua base de raiz: os objetos de página PDF não se encontram armazenados na mesma ordem constante no ficheiro. Se a biblioteca edificar a sua matriz interna de páginas ao varrer sequencialmente os objetos, e não caminhando através da árvore das páginas declarada nos catálogos, irá assim conceber a construção num índice a cuja margem de validação deixará de corresponder com o preceito pretendido pelas chamadas em invocação, recaindo então a verificação de limites a apanhar a incongruência no pior instante possível

Como atua a verificação de limites (range checking) no Delphi

Com a diretiva ao compilador {$R+} ativa (a predefinição na configuração Debug), o Delphi RTL procede a validações, ao longo da execução da rotina, em todo e qualquer índice nos conjuntos array, bem como nas subscrições numéricas numa string de carateres e nas atribuições enumeradas. Ao detetar acessos desenquadrados além dos limites faz levantar um ERangeError ao invés de prosseguir e efetuar caladas leituras à memória envolvente. Essa atitude acarreta o seu valor: acaba a desvendar disfunções (bugs) latentes antes que possibilite o cenário à disrupção sobre as infraestruturas de matriz cujos dados apenas viriam a claudicar mais adiante ao atingir da próxima centena em linhas de extensão no documento. O aspeto frustrante passa pelo deflagrar sobre a ocorrência nas vias em acesso e instantes da transposição de referida exceção e não na zona em que o cálculo sobre o índice pautou ao impreciso pela via do insucesso. Ao deparar-se que a estrutura relativa à disposição nas chamadas aponta rumo ao mergulho aninhado entre linhas na unidade PDF, por força da regra tal constatação reflete-se muitas das ocasiões num erro assente no retrocesso numa dezena a várias passagens das esferas estruturais de acesso e tramas operacionais à retaguarda

Condições com expressões booleanas compostas não ajudam e tendem a exacerbar os males. O Delphi interpreta o percurso das abordagens via formulações com a junção and caminhando num fluxo e leitura a da esquerda em conformidade aos limites para com a direita recorrendo às referências pela métrica (short-circuit) de curto-circuito semântico. A premissa referida contorna porém em saltos as devidas interpretações num único cenário: ao constatar sobre as esquerdas uma valia assente pelo False. Tome por consideração este caso prefigurado à imagem do exposto numa expressão com esta via:

if FDocStarted and (DestIndex < Length(PageArr)) and
   (PageArr[DestIndex].PageObj <> nil) then

Isto parece seguro, mas só protege contra índices fora do limite se FDocStarted for True e DestIndex não for negativo. A verificação DestIndex < Length(PageArr) não faz nada quando DestIndex é negativo. Comparar um número negativo com um comprimento não negativo devolve True em aritmética com sinal. Logo, o acesso à matriz (array) dispara o erro de limite. Mover a verificação de limites para a posição mais externa é a solução correta:

if (DestIndex >= 0) and (DestIndex < Length(PageArr)) then
begin
  if FDocStarted and (PageArr[DestIndex].PageObj <> nil) then
    Result := PageArr[DestIndex].PageObj
  else
    Result := nil;
end
else
  raise ERangeError.CreateFmt(
    'Page index %d is out of range (0..%d)',
    [DestIndex, Length(PageArr) - 1]);

Esta é a solução mecânica. Impede a falha e bloqueio. Porém, não explica o motivo de DestIndex ter recebido um valor fora do limite válido

Diagrama de fluxo de um erro de verificação de intervalo em Delphi numa biblioteca PDF, em que um DestIndex negativo contorna uma verificação de limites unilateral e lança ERangeError no acesso ao array, comparado com a ordem corrigida de proteção que entra em curto-circuito com segurança
Com $R+, a RTL valida cada índice em tempo de execução, pelo que um índice negativo sobrevive a um teste simples de limite superior e dispara no próprio acesso ao array. Testar primeiro o limite inferior com sinal deixa a avaliação de curto-circuito rejeitá-lo antes de qualquer elemento ser tocado

A verdadeira causa: ordem dos objetos contra a ordem das páginas

A norma ISO 32000-1 §7.7.3 define a árvore de páginas como uma árvore de nós Pages. As matrizes Kids listam os objetos das páginas na sua ordem de apresentação. O ficheiro armazena estes objetos em qualquer posição (offset) que o criador tenha escolhido; o objeto 20 pode anteceder o objeto 3 no fluxo de bytes. Uma biblioteca que construa a sua lista iterando a tabela de referência cruzada na ordem numérica dos objetos, em vez de seguir a cadeia de Kids, produzirá uma sequência que diverge do esperado. Nos documentos onde os objetos foram escritos de forma sequencial, tudo funciona. Quando tal não acontece, a discrepância entre a numeração gerada pela biblioteca e a referenciada na chamada causa índices errados em PageArr

A abordagem correta passa por iniciar a leitura do catálogo, resolver a referência indireta /Pages e seguir a matriz Kids de forma recursiva. Para um documento simples sem nós intermediários de Pages, a passagem direta é simples:

Diagrama Delphi contrastando a ordem de armazenamento de objetos PDF descoberta por uma varredura de referências cruzadas com a ordem de exibição recuperada percorrendo o array Kids da árvore de páginas a partir do catálogo
As análises sequenciais numeram páginas pela ordem que o escritor escolheu, enquanto os visualizadores seguem o array Kids da árvore de páginas. Construir o array de páginas a partir da travessia do catálogo mantém os índices do chamador alinhados e dentro do intervalo
procedure BuildPageIndexFromTree(
  const KidsArray: THPDFArray;
  var PageArr: TPageObjArray);
var
  i, Idx: Integer;
  Child: THPDFObject;
  ChildType: string;
begin
  for i := 0 to KidsArray.Count - 1 do
  begin
    Child := KidsArray.GetIndirectObject(i);
    if Child = nil then
      Continue;
    ChildType := Child.GetNameValue('/Type');
    if ChildType = 'Page' then
    begin
      Idx := Length(PageArr);
      SetLength(PageArr, Idx + 1);
      PageArr[Idx].PageObj := Child;
    end
    else if ChildType = 'Pages' then
    begin
      // nó intermédio: recursão pelos seus Kids
      BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
    end;
  end;
end;

Terminada a execução da rotina, PageArr[0] representa a primeira página que o visor exibe, independentemente do local que ocupe no fluxo de bytes. Os índices indicados nos chamamentos ficam assim assentes num mapeamento correto, resolvendo-se as incorreções da verificação de limites (range errors)

Soluções forçadas de contorno agravam o problema

Em bases de código em que a causa primária nunca foi apurada, é frequente encontrarmos arranjos (patches) baseados em heurística: trocar a primeira e a última página num ficheiro com 3 páginas, rodar o índice para documentos criados por um motor de geração específico, ou aplicar desvios quando o primeiro número de objeto ultrapassa um limiar. Estes arranjos encaixam e resultam apenas na amostra de ficheiros utilizada para os testes no momento. Ao adicionar uma fonte de PDF distinta, um dos arranjos falha e ativa-se na altura errada, elaborando um índice duplamente desadequado: está errado por via da matriz desordenada e novamente danificado pela aplicação incorreta e imprópria sobre o mapeamento associado. A verificação falha nalguma posição a jusante e não faculta informações úteis face à falha e à base da origem

A única trajetória produtiva consiste na eliminação dos mapeamentos de base e cunho na natureza da heurística, com a correspondente e inerente adoção pela substituição na estrutura matricial para as matrizes pelas chamadas corretas à travessia da árvore de disposição em cada nó (tree walk). Assim que os índices adquirirem a ordem natural por intermédio e vias da correta formulação e estrutura edificada no vetor na raiz em base pela via do código, os arranjos de correção em (patches) ficam perfeitamente isentos de justificação e o recurso a verificadores (range checker) converte-se num ativo e deixa de constituir um obstáculo

Caso se encontre a efetuar e a assegurar serviços na manutenção de bibliotecas de software cujo desempenho apresente estes exatos sintomas e comportamentos, ative a verificação de limites (range checking) temporariamente numa compilação de modo Release. Submeta o seu código a diversos e múltiplos testes de processamento englobando diferentes PDF de fontes distintas: relatórios feitos pelo Word, pelo LaTeX, pelo software de um digitalizador e ficheiros gerados em utilitários de separação de PDF. Os ficheiros que gerem exceção correspondem àqueles cujas ordens nos objetos divergem das ordens de navegações que o código assume. Cada ocorrência num de constitui um novo ponto de referência e não um bug isolado

Para novos códigos em uso e que chamem bibliotecas Delphi em PDF, a atitude aconselhável é tratar a contagem de páginas da biblioteca como o valor oficial e absoluto. Nunca passe um índice originado a partir de cálculos perante dados externos sem comprovar em primeiro lugar se o mesmo recai no âmbito dos limites de 0..PageCount - 1. O Componente HotPDF exibe a contagem de páginas resolvida no interior de THotPDF.PageCount sempre no momento que se segue ao BeginDoc ou nos carregamentos do documento PDF. Este valor demonstra e reflete a passagem através da árvore das páginas e é sempre confiável para que possa assentar nesse os preceitos de limite máximo para eventuais cálculos de índices