Artigo Técnico

REGEXTEST e REGEXEXTRACT em Delphi com PCRE2 estático

O HotXLS avalia as funções de expressão regular do Microsoft 365 REGEXTEST, REGEXEXTRACT e REGEXREPLACE dentro do seu próprio motor de fórmulas Delphi, sem instalação de Excel e sem DLL de regex do sistema. A calculadora do HotXLS liga um build PCRE2 10.49 de 16 bits fixado estaticamente, faz matching de texto UTF-16 diretamente, faz broadcasting sobre arrays e cobra cada passo de backtracking ao orçamento de recursos da fórmula

Essa última parte é a razão pela qual este artigo existe. Um workbook que chega de um cliente é entrada não confiável, e uma expressão regular dentro de uma fórmula é código que o cliente escreveu. Padrão mais texto é tudo o que é preciso para prender um núcleo de CPU durante minutos com backtracking catastrófico, a forma clássica de ReDoS (a+)+$ contra uma longa sequência de caracteres a com um b no fim. Um servidor que recalcula workbooks carregados não se pode dar a esse luxo, por isso o trabalho interessante no HotXLS 2.384.112 não foi chamar o PCRE2. Foi tornar o PCRE2 parável, responsabilizável e idêntico em todos os alvos

O que devolvem REGEXTEST, REGEXEXTRACT e REGEXREPLACE no HotXLS?

O motor de fórmulas do HotXLS segue as assinaturas publicadas pela Microsoft, e a semântica dos argumentos é a que já conhece do Excel. O matching é sensível a maiúsculas por predefinição. O argumento final case_sensitivity leva 0 para sensível a maiúsculas e 1 para insensível, e qualquer outro valor devolve #VALUE!. Opções PCRE2 inline como (?i) ou (*UCP) também funcionam quando preferir manter o comportamento dentro do padrão

O REGEXTEST(text, pattern, [case_sensitivity]) devolve um Boolean, e uma correspondência falhada é simplesmente FALSE, nunca um erro. O REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) tem três modos: 0 devolve a primeira correspondência completa, 1 devolve todas as correspondências completas como um array vertical, e 2 devolve os grupos de captura da primeira correspondência como um array horizontal. Sem correspondência devolve #N/A, o modo 2 num padrão sem grupos de captura devolve #VALUE!, e um grupo opcional que não participou dá uma string vazia em vez de um erro

O REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) usa a sintaxe de substituição PCRE2, por isso $1 e ${name} referenciam capturas. Um occurrence de 0 substitui todas as correspondências, um valor positivo escolhe a n-ésima correspondência desde o início e um valor negativo conta desde o fim. Quando a ocorrência escolhida não existe, o HotXLS devolve o texto original inalterado. Seletores numéricos são truncados em direção a zero, da mesma forma que o Excel trata argumentos inteiros fracionários

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');
    Sheet.Cells[2, 1].Value := 'PO-2026-00417 shipped to Graz';

    // TRUE quando a célula guarda um número de encomenda
    Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';

    // Primeira correspondência completa: "PO-2026-00417"
    Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';

    // Trocar ano e sequência com capturas numeradas: "PO-00417-2026 ..."
    Sheet.Cells[2, 4].Formula :=
      'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';

    // Teste insensível a maiúsculas com o argumento final
    Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';

    Book.Recalculate;
    Book.SaveAs('orders.xlsx');
  finally
    Book.Free;
  end;
end;

O texto de fórmula em Delphi é um literal de string simples, por isso o \d não precisa de escaping no lado Pascal. Só as aspas duplas do Excel dentro da fórmula importam, e seguem as regras normais do Excel. Na gravação, o escritor XLSX guarda esses nomes com o prefixo de função futura _xlfn. que o Excel espera, e um workbook reaberto recalcula-os outra vez. O motor clássico BIFF8 consegue avaliar as funções numa chamada contextual, mas não as consegue escrever como tokens de fórmula nativos .xls, por isso mantenha as fórmulas regex em output XLSX

Como funciona o broadcasting de arrays com fórmulas regex?

REGEXTEST, REGEXREPLACE e o modo 0 do REGEXEXTRACT fazem broadcasting sobre operandos compatíveis, que é o que os torna úteis em colunas reais. Cada argumento pode ser um escalar, uma linha, uma coluna ou um retângulo, e o HotXLS toma a maior contagem de linhas e colunas como forma do resultado. Um operando cuja dimensão não é 1 nem esse máximo faz a chamada inteira devolver #VALUE!, o habitual desfasamento de arrays do Excel. Assim, REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") dá 499 Booleans, e uma coluna de padrões contra uma única célula de texto funciona da mesma forma na outra direção

Os modos 1 e 2 do REGEXEXTRACT recusam-se a fazer broadcasting. Cada elemento produziria um vetor de comprimento diferente, e um conjunto esfarrapado de vetores não pode ocupar um resultado retangular de fórmula. Em vez de truncar ou encher em silêncio, o HotXLS reporta a chamada como não suportada. Se precisa de todas as correspondências de muitas células, itere as células e extraia uma de cada vez, ou reformule o problema com closures LET e LAMBDA no motor de fórmulas do HotXLS para que cada chamada de closure veja um valor de texto

O posicionamento de erros dentro de um resultado de broadcasting é deliberado. Um erro do Excel para um elemento, digamos #N/A de uma extração que não encontrou nada na linha 37, aterra nessa única célula do resultado enquanto as outras células guardam os seus valores. Um limite de recursos ou um cancelamento é diferente: para a chamada inteira e nunca publica um array parcialmente preenchido, porque metade de um resultado que parece completo é pior do que uma falha explícita

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Opts: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Log');
    Sheet.Cells[1, 1].Value := 'GET /api/v2/orders 200 18ms';

    Opts := XLSDefaultFormulaEvaluationOptions;
    Opts.MaxEvaluationSteps := 200000;   // limitar o trabalho que esta fórmula pode fazer

    // Modo 2: grupos de captura da primeira correspondência como array 1 x 3
    Eval := Sheet.EvaluateFormulaAt(1, 2,
      'REGEXEXTRACT(A1, "(GET|POST) (\S+) (\d{3})", 2)', xlfrsA1, Opts);

    case Eval.Status of
      xlfeOk:
        if VarIsArray(Eval.Value) then
          for I := VarArrayLowBound(Eval.Value, 2) to
                   VarArrayHighBound(Eval.Value, 2) do
            Log(VarToStr(Eval.Value[0, I]));
      xlfeResourceLimit:
        Log('Pattern exceeded its budget: ' + Eval.Issue.Message);
    else
      Log('Evaluation failed: ' + Eval.Issue.Message);
    end;
  finally
    Book.Free;
  end;
end;

O TXLSXWorksheet.EvaluateFormulaAt é o ponto de entrada certo quando o padrão vem de um sítio que não controla. Avalia a fórmula como se vivesse na linha e coluna dadas, devolve o array completo em vez de um único valor de célula em cache, e leva TXLSFormulaEvaluationOptions com controlos explícitos de passos, memória de arrays e cancelamento. O guia do motor de fórmulas e funções personalizadas do HotXLS cobre o resto dessa API de avaliação

Como é que o HotXLS para um padrão de backtracking catastrófico?

O HotXLS para padrões descontrolados fazendo o PCRE2 reportar ao contexto de fórmula a cada passo. O backend compila todos os padrões com PCRE2_AUTO_CALLOUT, que faz o PCRE2 chamar uma callback Pascal a cada item do padrão durante o matching. Essa callback cobra uma unidade de trabalho ao estado de avaliação ativo, verifica a flag de cancelamento e verifica a memória residente. Quando qualquer um desses diz parar, a callback devolve o código de aborto de callout do PCRE2 e o match desenrola-se limpo dentro do PCRE2. A fórmula aparece como xlfeResourceLimit ou um estado de cancelamento, não como uma thread pendurada

A memória é medida da mesma forma. Cada contexto PCRE2 é criado com um contexto geral por instância cujo alocador é uma função Pascal, por isso o padrão compilado, os dados de match e os frames internos de backtracking do PCRE2 são todos alocados através do HotXLS. Cada bloco traz um pequeno cabeçalho e é alinhado a 16 bytes, e o seu tamanho soma-se a um total acumulado que a verificação de orçamento compara com o limite de memória de arrays da fórmula. Buffers de output, vetores de extração e a string de substituição somam-se ao mesmo total, o que significa que um padrão que produza milhões de strings minúsculas atinge o teto com a mesma certeza que um que faça backtracking para sempre

A callback de orçamento também corre sob uma guarda que importa mais do que parece. Uma exceção Delphi levantada dentro de uma callback que o PCRE2 chamou nunca se pode propagar para cima, porque entre a callback e o seu código sentam-se frames de pilha C compilados sem qualquer conhecimento das tabelas de exceção Delphi. Desenrolar-se através deles é indefinido no melhor caso e uma pilha corrompida no pior. A ponte do HotXLS apanha toda a exceção na callback, trava a instância num estado abortado e devolve um código de erro ordinário ao PCRE2. O seu código vê uma fórmula falhada, nunca uma exceção que atravessou um frame estranho

O custo é real e vale a pena dizê-lo. Auto callouts tornam o matching mais lento do que um interpretador PCRE2 nu, e o HotXLS compila o padrão uma vez por elemento avaliado, por isso um broadcasting sobre 50.000 células paga 50.000 compilações. Para trabalho de texto em massa sobre dados confiáveis, uma passagem Delphi de regex dedicada fora do workbook será mais rápida. Dentro de um workbook que estranhos lhe enviam, limitado e previsível ganha a rápido

Porquê um build PCRE2 UTF-16 estático sem JIT?

Strings do Excel são UTF-16, e os valores WideString de Delphi também, o que fez da biblioteca PCRE2 de 16 bits a única escolha sensata. O HotXLS entrega o texto da fórmula ao pcre2_match_16 com PCRE2_UTF ligado, e cada offset que volta é um índice de unidade de código UTF-16. Nenhuma volta por UTF-8 acontece, por isso nada pode desviar-se em surrogate pairs. Onde um match vazio força a varredura a avançar, o HotXLS segue a regra de global match do PCRE2: tentar de novo um match ancorado não vazio na mesma posição, e depois avançar um valor escalar Unicode, o que move duas unidades de código para além de um surrogate pair em vez de o partir

O JIT está desligado de propósito. O compilador JIT precisa de memória executável e da sua própria pilha JIT, o que arrasta código específico de plataforma e alocações fora do alocador por fórmula. O interpretador mantém cada byte sob o orçamento do HotXLS, mantém cada match sob a callout e comporta-se igual nos cinco alvos. Cada instância TXLSRegex, a classe interna atrás destas fórmulas, possui os seus próprios contextos, por isso trabalhadores de cálculo paralelos não partilham estado PCRE2 mutável

A história da ligação é onde o build ficou invulgar. Delphi liga ficheiros objeto C com {$L}, e o seu linker resolve símbolos entre unidades Pascal e objetos C suficientemente bem, mas não resolve referências circulares arbitrárias entre ficheiros objeto C separados. O PCRE2 são duas dúzias de unidades de tradução que se chamam em todas as direções. A correção é um unity build: todo o PCRE2 é compilado como uma unidade de tradução num único objeto pcre2_unity por alvo, por isso cada referência interna resolve dentro de um único objeto e o linker Delphi só vê as funções exportadas _16

O runtime C foi o segundo problema. O PCRE2 chama malloc, memcpy, memcmp e companhia, e outros backends C ligados no mesmo executável podem já trazer as suas próprias cópias desses nomes. O HotXLS pré-processa o PCRE2 para essas chamadas irem para nomes privados como hotxls_regex_memcpy e implementa-os em Pascal. O Clang ainda pode sintetizar chamadas memset e memchr simples depois do pré-processamento, por isso esses dois também existem, dentro da mesma unidade. A decoração de símbolos tem de corresponder a cada formato de objeto: o objeto OMF Win32 espera os nomes C com underscore inicial, enquanto as declarações Pascal para o objeto ELF Win64, o objeto COFF Win64 e os builds FPC ligam os nomes nus

As sondas de pilha foram as mais subteis. Uma função C com frame maior do que uma página chama primeiro uma rotina de sonda, que toca cada página de 4 KB por ordem para que o Windows consigne a pilha através da sua página de guarda. Um stub que simplesmente devolve liga bem e funciona em todos os testes unitários com um padrão pequeno, e depois rebenta na primeira vez que um frame profundo salta a página de guarda. O HotXLS implementa sondas reais em assembly para cada formato de objeto Windows: __chkstk_noalloc para o objeto Win32, __chkstk para o objeto em formato ELF Win64 e ___chkstk_ms para o objeto COFF Win64. Em Linux x64 e macOS ARM64, o FPC liga a biblioteca C do sistema, e todos os outros alvos FPC reportam o backend como indisponível em vez de recuar em silêncio para um dialeto regex diferente

Onde ficam os limites?

As funções regex correm em builds Delphi e C++Builder Windows e em FPC Linux x64 e macOS ARM64. Em qualquer outro alvo a fórmula reporta uma função não suportada através da avaliação contextual em vez de produzir respostas diferentes com outro motor. Essa é a falha certa: um workbook que calcula de forma diferente numa plataforma diferente é um bug que ninguém encontra até a uma auditoria

A equivalência com o Excel tem um limite declarado também. As assinaturas e os exemplos documentados seguem as páginas REGEXTEST, REGEXEXTRACT e REGEXREPLACE da Microsoft, mas o build Excel usado para validação local ainda não expõe estas funções do Microsoft 365. Os casos de limite seguem por isso os contratos explícitos de PCRE2 e de arrays descritos acima em vez de uma alegação de igualar todos os builds Excel bit a bit. Se os seus workbooks dependem de um canto obscuro, como um comportamento específico de backreference ou uma classe de propriedade Unicode, teste diretamente esse canto

FAQ

O HotXLS precisa de Excel ou de uma DLL regex para calcular REGEXTEST?

Não. O HotXLS liga um objeto PCRE2 UTF-16 estático no seu executável e avalia REGEXTEST, REGEXEXTRACT e REGEXREPLACE no seu próprio motor de fórmulas Delphi, sem Excel, sem COM e sem DLL externa

Um padrão malicioso pode pendurar o meu servidor?

Não para além do orçamento configurado. Cada passo de matching passa por uma callout que cobra ao contexto da fórmula, por isso um padrão ReDoS acaba com xlfeResourceLimit ou um estado de cancelamento e sem resultado parcial

Porque é que o modo 1 do REGEXEXTRACT falha num argumento de intervalo?

Cada célula podia devolver um número diferente de correspondências, e esses vetores esfarrapados não conseguem formar um resultado retangular. O HotXLS reporta a chamada como não suportada em vez de encher ou truncar, por isso extraia uma célula de cada vez

O HotXLS traz as funções regex ao lado de LAMBDA, GROUPBY, PIVOTBY e do resto do seu motor de cálculo para Delphi, C++Builder e Lazarus. Detalhes, edições e a transferência de teste estão na página do HotXLS Delphi spreadsheet component