Artigo Técnico

Falhas silenciosas de stubs numa biblioteca PDF Pascal

Quando uma biblioteca Delphi ganha uma configuração de compilação sem o framework visual, as classes substitutas são onde os bugs vivem. Não a plataforma, não o compilador: os stand-ins. O PDFlibPas tem uma camada de gráficos que fornece equivalentes de bitmap, canvas, fonte, metaficheiro e impressora para compilações sem a VCL, e portá-lo para Free Pascal trouxe à superfície todos os modos de falha que um stand-in pode ter. Organizam-se de forma nítida por custo de diagnóstico, e a ordenação é o oposto do que a intuição sugere

Um stand-in que lança exceção é barato de encontrar; a exceção nomeia o método. Um stand-in que devolve dados vazios é caro, porque a falha aparece várias camadas longe da sua causa. Um stand-in que devolve sucesso é o pior de todos, porque o código de retorno é válido, o código de erro é zero, nenhuma exceção é lançada, e a única evidência de que algo correu mal está nos bytes que saíram

Três formas de falha silenciosa de stub numa biblioteca PDF Pascal ordenadas por custo de diagnóstico, de um stub que lança exceção a sucesso sobre saída vazia
Um stand-in que lança exceção é barato de diagnosticar, dados vazios são caros, e um retorno de sucesso sobre um artefacto vazio é o pior de encontrar

Forma três: um identificador de imagem válido sobre um XObject vazio

O conversor vetorial de metaficheiros era um corpo de procedimento vazio na configuração sem VCL. Tudo acima dele continuou a funcionar. Os pontos de entrada de importação EMF e o ponto de entrada de captura de canvas correram até ao fim e devolveram um identificador de imagem legal, que o chamador depois colocou numa página. O que aterrrou no ficheiro foi um form XObject com comprimento de conteúdo zero. A página renderizou branca

Nada reportou um problema, e isso inclui o programa de demonstração da própria biblioteca para esta funcionalidade, que desenhou uma página em branco e não deu por isso. Não havia valor de retorno de falha para verificar, porque a sequência de chamadas genuinamente teve todas sucesso; a única coisa errada era o tamanho do stream produzido. Diagnosticar esta classe de defeito significa fazer uma pergunta diferente: não "falhou a chamada" mas "o artefacto é plausível". Um form XObject de comprimento zero, uma imagem de zero píxeis, uma página de zero bytes de conteúdo, essas são as asserções que a apanham

A correção tem duas metades e a segunda metade é fácil de esquecer. Primeiro, fazer a implementação vazia lançar exceção, para que a falha tenha um canal. Segundo, converter essa exceção num resultado nulo na fábrica de imagens e acrescentar verificações de nulo nos dois sítios que consomem um identificador de imagem, porque de outra forma "falha limpa" transforma-se diretamente numa violação de acesso quando a árvore de páginas desreferencia o nada. Um stub que lança exceção só é uma melhoria se os chamadores estavam preparados para uma falha que antes nunca podiam receber

Forma dois: dados vazios, três camadas do crash

O stand-in de canvas de metaficheiro não preenchia as suas dimensões físicas. Esse valor divide num cálculo de geometria de página, pelo que o cálculo produziu zero, e o cálculo da bounding box dividiu por zero. Um tratador de exceções nu engoliu isso, a fábrica de imagens devolveu um resultado nulo, e a violação de acesso finalmente aconteceu na árvore de páginas quando o nulo foi usado. Três camadas entre causa e sintoma, com um tratador de exceções no meio a apagar a evidência

Cadeia de falha de um stand-in vazio de canvas de metaficheiro no PDFlibPas chegando a uma violação de acesso três camadas depois da divisão por zero
A dimensão vazia leva um cálculo de geometria a zero, um tratador nu apaga a exceção, e o identificador nulo faz a árvore de páginas colapsar

A mesma unidade tinha mais duas instâncias do padrão. A classe de fonte tinha corpos vazios de Assign e do construtor, o que importa mais do que parece porque a propriedade de fonte do canvas é só de leitura: atribuir para ela é a única forma de entregar uma fonte, pelo que uma implementação vazia torna a seleção de fonte silenciosamente ineficaz e o texto sai no que quer que fosse a predefinição. E um valor de píxeis por polegada de zero fez com que todos os chamadores que dimensionam um canvas a partir de métricas de fonte produzissem um canvas de zero por zero, o que rende uma página em branco e um retorno de sucesso

// A forma a procurar numa unidade de stand-in: um método que nem
// lança exceção nem faz nada. Ambos compilam e ambos produzem
// "sucesso" sem saída nenhuma
procedure TMetafileCanvasStandIn.Create(...);
begin
  // sem chamada herdada, sem inicialização de campos
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // e o bitmap continua vazio
end;

A estrutura wide que guarda só o primeiro carácter

Esta nem sequer é um problema de stand-in, mas pertence ao mesmo catálogo porque o sintoma está igualmente longe da causa. A estrutura de enumeração de impressoras foi declarada com todos os seus doze membros de string tipados como ponteiros para caracteres de um byte, enquanto a função que a preenche é a variante de caracteres wide da API de enumeração

Os tamanhos de ponteiro são idênticos, pelo que o layout da estrutura está correto e nada colapsa. O que acontece em vez disso é que ler uma string UTF-16 como string de um byte para no primeiro byte zero, que para qualquer nome de impressora ASCII é a metade alta do segundo carácter. Todos os nomes de impressoras voltaram com exatamente um carácter. A jusante, a validação de nomes falhou, a criação de impressoras falhou e a impressão falhou para todas as impressoras reais na máquina, e nenhum desses sintomas aponta para uma declaração de estrutura

Nome de impressora UTF-16 truncado para um carácter depois de uma estrutura Win32 wide ser declarada com membros PAnsiChar em vez de PWideChar
Membros de um byte leem um nome UTF-16 só até ao seu primeiro byte zero, pelo que todos os nomes de impressoras voltam com exatamente um carácter
// Errado: tamanho certo, tipo de elemento errado. Sem erro de
// compilação, sem crash, todas as strings truncadas a um carácter
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... mais dez
  end;

// Certo: uma estrutura *W tem membros wide por inteiro
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... mais dez
  end;

A regra que daí sai é mecânica e vale a pena aplicar sem pensar: para qualquer estrutura Win32 cujo nome termina em W, verifique que cada membro de string é a variante wide, campo a campo. Misturar os mundos ANSI e wide não produz nem um diagnóstico de compilador nem um crash, só truncadura silenciosa, e o mesmo se aplica ao inverso para as variantes ANSI

Um tratador de exceções nu é o verdadeiro adversário

Todas estas investigações foram travadas pelo mesmo constructo: um tratador que apanha tudo e converte num valor de retorno falso. É uma coisa razoável de escrever à volta de um descodificador de imagens, já que uma imagem corrompida não deve derrubar um trabalho de documento. É também um dispositivo para apagar a única informação de que precisa

A resposta prática é tornar o tratador temporariamente ruidoso. Descarregar a classe de exceção, mensagem e backtrace de dentro do tratador nu, sob uma condicional de debug, converte um retorno nulo inexplicado numa exceção nomeada com uma localização. Em dois dos três casos acima, esse único passo terminou a investigação, porque a exceção era uma divisão por zero ou uma violação de acesso num método de stand-in cujo nome dizia tudo

Lista de verificação para adotar um caminho de stand-in

Quatro itens, pela ordem em que compensam. Antes de chamar uma classe substituta, leia os métodos que está prestes a usar e confirme que cada um tem um corpo real; um corpo vazio não é um detalhe de implementação, é uma funcionalidade em falta. Prefira stand-ins que lançam exceção a stand-ins que devolvem valores neutros, e combine isso com verificações de nulo nos sítios onde uma fábrica agora pode legitimamente devolver nada. Verifique uma funcionalidade inspecionando o artefacto, não o código de retorno, já que todo o modo de falha aqui é um código de retorno limpo sobre um artefacto vazio; uma decomposição ao nível do byte do que um documento realmente contém é a forma mais rápida de o ver, e o artigo sobre auditoria de tamanho de ficheiro cobre essa ferramenta. E quando uma funcionalidade não tem implementação substituta viável, encaminhe as amostras afetadas para o caminho que funciona e diga porquê num comentário, em vez de deixar uma demonstração que produz silenciosamente saída em branco

O ponto mais amplo aplica-se muito para além de uma biblioteca. Qualquer base de código com uma segunda implementação condicional, uma camada de mock, um modo headless, um shim de plataforma, está exposta à forma três. A razão pela qual se esconde tão bem é que todos os portões de qualidade em que uma equipa normalmente confia, códigos de retorno, códigos de erro, exceções, estados de saída, são um canal de estado, e a forma três mantém todos limpos. Só a saída o trai. Esse é também o raciocínio por trás de verificar artefactos em vez de estados ao tratar entrada não confiável, descrito no artigo sobre análise de PDF não confiável, e por trás de comparar a saída renderizada entre motores em vez de confiar num, descrito em renderização multi-motor

O PDFlibPas é uma biblioteca PDF nativa em Object Pascal para Delphi, C++Builder e Free Pascal, e a sua configuração sem VCL é o que torna possíveis as compilações headless e entre toolchains; a cobertura atual de configurações está listada na página de produto da losLab PDF Developer Library