GraphQL-Houtou

 view release on metacpan or  search on metacpan

t/53_parser_adversarial.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More 0.98;

# Fixed adversarial parser inputs. Complements the
# randomized util/parser-fuzz.pl with named regression cases: each must
# either parse or raise a normal GraphQL::Houtou::Error, never crash and
# never hang. Parses run in a forked child so a regression that
# reintroduces a crash surfaces as a failed assertion rather than taking
# the test process down with it.

use GraphQL::Houtou qw(parse);

# 'parsed' | 'error' | 'CRASH'
sub parse_outcome {
  my ($input) = @_;
  my $pid = fork();
  die "fork failed: $!" if !defined $pid;
  if ($pid == 0) {
    my $ok = eval { parse($input); 1 };
    exit($ok ? 0 : 1);
  }
  waitpid $pid, 0;
  return 'CRASH' if $? & 127;
  return $? >> 8 == 0 ? 'parsed' : 'error';
}

my %CASES = (
  'invalid unicode escape' => '{ f(a: "\uZZZZ") }',
  'lone high surrogate' => '{ f(a: "\uD83D") }',
  'lone low surrogate' => '{ f(a: "\uDE00") }',
  'unterminated string' => '{ f(a: "no closing quote',
  'unterminated block string' => '{ f(a: """no closing',
  'NUL byte in the middle' => "{ a\x00b }",
  'bare NUL' => "\x00",
  'raw control bytes' => "{ \x01\x02\x03 }",
  'invalid UTF-8 continuation' => "{ f(a: \"\xC3\x28\") }",
  'overlong UTF-8' => "{ \xC0\xAF }",
  'huge integer literal' => '{ f(a: ' . ('9' x 5000) . ') }',
  'huge float exponent' => '{ f(a: 1.0e' . ('9' x 5000) . ') }',
  'many leading minus' => '{ f(a: ' . ('-' x 1000) . '1) }',
  'unbalanced closers' => '{ a } } } }',
  'only openers (bounded)' => '{' x 1000,
  'deep brackets in value' => '{ f(a: ' . ('[' x 1000) . ') }',
  'empty document' => '',
  'whitespace only' => "   \n\t  ",
  'just a comment' => "# nothing here\n",
  'BOM then query' => "\x{feff}{ a }",
  'name with all digits after' => '{ ' . ('a' x 100000) . ' }',
  'giant comment' => '# ' . ('x' x 200000) . "\n{ a }",
  'nested block string quotes' => '{ f(a: """he said """"") }',
  'spread without name' => '{ ... }',
  'directive without name' => '{ a @ }',
  'variable without name' => 'query($) { a }',
);

for my $label (sort keys %CASES) {
  my $outcome = parse_outcome($CASES{$label});
  isnt $outcome, 'CRASH', "$label does not crash the parser"
    or diag "input crashed the parser";
}

# The all-'a' name and giant comment should actually parse (they are valid,
# just large); confirm the parser handles them rather than only not
# crashing.
subtest 'large-but-valid inputs parse' => sub {
  is parse_outcome('{ ' . ('a' x 100000) . ' }'), 'parsed', 'a 100k-char field name';
  is parse_outcome('# ' . ('x' x 200000) . "\n{ a }"), 'parsed', 'a 200k-char comment';
};

done_testing;



( run in 0.932 second using v1.01-cache-2.11-cpan-8dfa8b56332 )