packages/sql-formatter/fuzz/Target/FormatTarget.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Target;
6
7use Error;
8use SqlFormatter\Core\FormatOptions;
9use SqlFormatter\Core\FormattingException;
10use SqlFormatter\Core\Style;
11use SqlFormatter\Facade\Formatter;
12use SqlParser\Lexer\SourceException;
13use SqlParser\MySql\MySqlParser;
14use SqlParser\PostgreSql\PostgreSqlParser;
15use SqlParser\Sqlite\SqliteParser;
16
17/**
18 * Formats arbitrary bytes as SQL text and reports anything but a clean rejection.
19 *
20 * The first input byte selects the layout preset and the indentation width, the rest is
21 * handed to the formatter as the SQL text. Text the parser rejects is not a finding: the
22 * parser's own exceptions are the documented answer to invalid input. Everything else is
23 * one: a verification failure, any other exception, a fatal error, a timeout, or output
24 * that changes when it is formatted again.
25 */
26final class FormatTarget
27{
28    /**
29     * @param MySqlParser|PostgreSqlParser|SqliteParser $parser The parser of the dialect and release under test
30     * @param string $grammarVersion The release tag for findings, e.g. "mysql-8.4.7"
31     */
32    public function __construct(
33        private readonly MySqlParser|PostgreSqlParser|SqliteParser $parser,
34        private readonly string $grammarVersion,
35    ) {
36    }
37
38    /**
39     * Derives the layout from the selector byte: two bits choose the preset, four the indentation.
40     *
41     * @param int $selector The first input byte
42     *
43     * @return FormatOptions One of the 64 layouts
44     */
45    public static function options(int $selector): FormatOptions
46    {
47        $styles = Style::cases();
48        return new FormatOptions($styles[$selector % count($styles)], (($selector >> 2) % 16) + 1);
49    }
50
51    /**
52     * Formats the text and formats the result again.
53     *
54     * @param string $input The fuzzer input: a selector byte followed by the SQL text
55     *
56     * @throws Error When formatting fails verification, or when formatting is not idempotent
57     */
58    public function verify(string $input): void
59    {
60        if ($input === '') {
61            return;
62        }
63        $options = self::options(ord($input[0]));
64        $sql = substr($input, 1);
65        $context = "Grammar: {$this->grammarVersion}\nStyle: {$options->style->value}, indent {$options->indentWidth}\nInput (hex): " . bin2hex($input) . "\nSQL: {$sql}";
66        $formatter = new Formatter($this->parser, $options);
67        try {
68            $formatted = $formatter->format($sql);
69        } catch (SourceException) {
70            return;
71        } catch (FormattingException $failure) {
72            throw new Error("Formatting failed verification\n{$context}\nError: {$failure->getMessage()}", 0, $failure);
73        }
74        try {
75            $again = $formatter->format($formatted);
76        } catch (SourceException|FormattingException $failure) {
77            throw new Error("Formatted SQL cannot be formatted again\n{$context}\nFormatted: {$formatted}\nError: {$failure->getMessage()}", 0, $failure);
78        }
79        if ($again !== $formatted) {
80            throw new Error("Formatting is not idempotent\n{$context}\nFirst: {$formatted}\nSecond: {$again}");
81        }
82    }
83}
84