mirror of
https://github.com/macintoshplus/mongo-php-driver.git
synced 2026-03-26 09:52:07 +01:00
* Extract bson to json conversion logic * PHPC-326: Add class to represent raw BSON document * PHPC-326: Add class to represent raw BSON array * Typemaps now support raw BSON type * Short-circuit encoding of BSON zvals * PHPC-324: Add BSONIterator class * Polyfill zend_array_is_list on PHP < 8.1 * Add offset accessors to BSON classes * Add more tests around iterators and fetching offsets * Remove compare handler for BSONIterator * Rename BSONArray to ArrayList * Rename BSONDocument to Document * Rename BSONIterator to Iterator * Implement Document::fromBSONString * Test BSON document in bson-corpus tests * Fix ArrayList handling of BSON arrays with wrong keys * Address code review for bson-corpus tests * Address code review in ArrayList * Address code review for Document * Address code review for Iterator * Allow BSON iterators to be rewound * Address code review in tests * Fix comment explaining php_phongo_bson_append_object * Remove obsolete php_phongo_bson_append_zval * More review changes to tests * Return base64 encoded data when debugging BSON classes * Allow serialisation of Document and ArrayList * Add test for iterating past the end of a structure * Update exception message to contain class name * Make BSON pclass handling test more complete * Fix wrong handling of type map in nested structures * Ensure objects are backed by actual memory * Validate UTF-8 BSON data before returning it * Allow ArrayList::toPHP() to accept a different root type map * Ignore null return type for invalid BSON iterators on PHP < 8 * Throw when calling get on non-existing BSON offsets * Remove unnecessary length from serialised output * Refactor names around type map handling * Use short array syntax in new tests * Improve test descriptions * Restructure BSON iterator tests * Expand BSON class serialisation tests * Throw exception when iterator is exhausted The previous method relied on the type system of PHP to throw an exception when accessing the key of an exhausted iterator, but this does not work on non-debug versions of PHP. * Update test description * Rename ArrayList class to PackedArray
342 lines
14 KiB
PHP
342 lines
14 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/../tests/utils/basic.inc';
|
|
|
|
$expectedFailures = [
|
|
'Int64 type: -1' => 'PHP encodes integers as 32-bit if range allows',
|
|
'Int64 type: 0' => 'PHP encodes integers as 32-bit if range allows',
|
|
'Int64 type: 1' => 'PHP encodes integers as 32-bit if range allows',
|
|
'Javascript Code: Embedded nulls' => 'Embedded null in code string is not supported in libbson (CDRIVER-1879)',
|
|
'Javascript Code with Scope: Unicode and embedded null in code string, empty scope' => 'Embedded null in code string is not supported in libbson (CDRIVER-1879)',
|
|
'Multiple types within the same document: All BSON types' => 'PHP encodes integers as 32-bit if range allows',
|
|
'Top-level document validity: Bad $date (number, not string or hash)' => 'Legacy extended JSON $date syntax uses numbers (CDRIVER-2223)',
|
|
];
|
|
|
|
$for64bitOnly = [
|
|
/* Note: Although 64-bit integers be represented by the Int64 class, these
|
|
* tests fail on 32-bit platforms due to json_canonicalize() roundtripping
|
|
* values through PHP, which converts large integers to floats. */
|
|
'Int64 type: MinValue' => "Can't represent 64-bit ints on a 32-bit platform",
|
|
'Int64 type: MaxValue' => "Can't represent 64-bit ints on a 32-bit platform",
|
|
];
|
|
|
|
$outputPath = realpath(__DIR__ . '/../tests') . '/bson-corpus/';
|
|
|
|
if ( ! is_dir($outputPath) && ! mkdir($outputPath, 0755, true)) {
|
|
printf("Error creating output path: %s\n", $outputPath);
|
|
}
|
|
|
|
foreach (array_slice($argv, 1) as $inputFile) {
|
|
if ( ! is_readable($inputFile) || ! is_file($inputFile)) {
|
|
printf("Error reading %s\n", $inputFile);
|
|
continue;
|
|
}
|
|
|
|
$test = json_decode(file_get_contents($inputFile), true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
printf("Error decoding %s: %s\n", $inputFile, json_last_error_msg());
|
|
continue;
|
|
}
|
|
|
|
if ( ! isset($test['description'])) {
|
|
printf("Skipping test file without \"description\" field: %s\n", $inputFile);
|
|
continue;
|
|
}
|
|
|
|
if ( ! empty($test['valid'])) {
|
|
foreach ($test['valid'] as $i => $case) {
|
|
$outputFile = sprintf('%s-valid-%03d.phpt', pathinfo($inputFile, PATHINFO_FILENAME), $i + 1);
|
|
try {
|
|
$output = renderPhpt(getParamsForValid($test, $case), $expectedFailures, $for64bitOnly);
|
|
} catch (Exception $e) {
|
|
printf("Error processing valid[%d] in %s: %s\n", $i, $inputFile, $e->getMessage());
|
|
continue;
|
|
}
|
|
|
|
if (false === file_put_contents($outputPath . '/' . $outputFile, $output)) {
|
|
printf("Error writing valid[%d] in %s\n", $i, $inputFile);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! empty($test['decodeErrors'])) {
|
|
foreach ($test['decodeErrors'] as $i => $case) {
|
|
$outputFile = sprintf('%s-decodeError-%03d.phpt', pathinfo($inputFile, PATHINFO_FILENAME), $i + 1);
|
|
try {
|
|
$output = renderPhpt(getParamsForDecodeError($test, $case), $expectedFailures, $for64bitOnly);
|
|
} catch (Exception $e) {
|
|
printf("Error processing decodeErrors[%d] in %s: %s\n", $i, $inputFile, $e->getMessage());
|
|
continue;
|
|
}
|
|
|
|
if (false === file_put_contents($outputPath . '/' . $outputFile, $output)) {
|
|
printf("Error writing decodeErrors[%d] in %s\n", $i, $inputFile);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! empty($test['parseErrors'])) {
|
|
foreach ($test['parseErrors'] as $i => $case) {
|
|
$outputFile = sprintf('%s-parseError-%03d.phpt', pathinfo($inputFile, PATHINFO_FILENAME), $i + 1);
|
|
try {
|
|
$output = renderPhpt(getParamsForParseError($test, $case), $expectedFailures, $for64bitOnly);
|
|
} catch (Exception $e) {
|
|
printf("Error processing parseErrors[%d] in %s: %s\n", $i, $inputFile, $e->getMessage());
|
|
continue;
|
|
}
|
|
|
|
if (false === file_put_contents($outputPath . '/' . $outputFile, $output)) {
|
|
printf("Error writing parseErrors[%d] in %s\n", $i, $inputFile);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getParamsForValid(array $test, array $case)
|
|
{
|
|
foreach (['description', 'canonical_bson', 'canonical_extjson'] as $field) {
|
|
if (!isset($case[$field])) {
|
|
throw new InvalidArgumentException(sprintf('Missing "%s" field', $field));
|
|
}
|
|
}
|
|
|
|
$code = '';
|
|
$expect = '';
|
|
|
|
$lossy = isset($case['lossy']) ? (boolean) $case['lossy'] : false;
|
|
|
|
$canonicalBson = $case['canonical_bson'];
|
|
$expectedCanonicalBson = strtolower($canonicalBson);
|
|
$code .= sprintf('$canonicalBson = hex2bin(%s);', var_export($canonicalBson, true)) . "\n";
|
|
|
|
if (isset($case['degenerate_bson'])) {
|
|
$degenerateBson = $case['degenerate_bson'];
|
|
$expectedDegenerateBson = strtolower($degenerateBson);
|
|
$code .= sprintf('$degenerateBson = hex2bin(%s);', var_export($degenerateBson, true)) . "\n";
|
|
}
|
|
|
|
if (isset($case['converted_bson'])) {
|
|
$convertedBson = $case['converted_bson'];
|
|
$expectedConvertedBson = strtolower($convertedBson);
|
|
$code .= sprintf('$convertedBson = hex2bin(%s);', var_export($convertedBson, true)) . "\n";
|
|
}
|
|
|
|
$canonicalExtJson = $case['canonical_extjson'];
|
|
$expectedCanonicalExtJson = json_canonicalize($canonicalExtJson);
|
|
$code .= sprintf('$canonicalExtJson = %s;', var_export($canonicalExtJson, true)) . "\n";
|
|
|
|
if (isset($case['relaxed_extjson'])) {
|
|
$relaxedExtJson = $case['relaxed_extjson'];
|
|
$expectedRelaxedExtJson = json_canonicalize($relaxedExtJson);
|
|
$code .= sprintf('$relaxedExtJson = %s;', var_export($relaxedExtJson, true)) . "\n";
|
|
}
|
|
|
|
if (isset($case['degenerate_extjson'])) {
|
|
$degenerateExtJson = $case['degenerate_extjson'];
|
|
$expectedDegenerateExtJson = json_canonicalize($degenerateExtJson);
|
|
$code .= sprintf('$degenerateExtJson = %s;', var_export($degenerateExtJson, true)) . "\n";
|
|
}
|
|
|
|
if (isset($case['converted_extjson'])) {
|
|
$convertedExtJson = $case['converted_extjson'];
|
|
$expectedConvertedExtJson = json_canonicalize($convertedExtJson);
|
|
$code .= sprintf('$convertedExtJson = %s;', var_export($convertedExtJson, true)) . "\n";
|
|
}
|
|
|
|
$code .= "\n// Canonical BSON -> Native -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex(fromPHP(toPHP($canonicalBson))), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
|
|
$code .= "\n// Canonical BSON -> BSON object -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex((string) MongoDB\BSON\Document::fromBSON($canonicalBson)), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
|
|
$code .= "\n// Canonical BSON -> Canonical extJSON\n";
|
|
$code .= 'echo json_canonicalize(toCanonicalExtendedJSON($canonicalBson)), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalExtJson . "\n";
|
|
|
|
$code .= "\n// Canonical BSON -> BSON object -> Canonical extJSON\n";
|
|
$code .= 'echo json_canonicalize(MongoDB\BSON\Document::fromBSON($canonicalBson)->toCanonicalExtendedJSON()), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalExtJson . "\n";
|
|
|
|
if (isset($relaxedExtJson)) {
|
|
$code .= "\n// Canonical BSON -> Relaxed extJSON\n";
|
|
$code .= 'echo json_canonicalize(toRelaxedExtendedJSON($canonicalBson)), "\n";' . "\n";
|
|
$expect .= $expectedRelaxedExtJson . "\n";
|
|
|
|
$code .= "\n// Canonical BSON -> BSON object -> Relaxed extJSON\n";
|
|
$code .= 'echo json_canonicalize(MongoDB\BSON\Document::fromBSON($canonicalBson)->toRelaxedExtendedJSON()), "\n";' . "\n";
|
|
$expect .= $expectedRelaxedExtJson . "\n";
|
|
}
|
|
|
|
if (!$lossy) {
|
|
$code .= "\n// Canonical extJSON -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex(fromJSON($canonicalExtJson)), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
|
|
$code .= "\n// Canonical extJSON -> BSON object -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex((string) MongoDB\BSON\Document::fromJSON($canonicalExtJson)), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
}
|
|
|
|
if (isset($degenerateBson)) {
|
|
$code .= "\n// Degenerate BSON -> Native -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex(fromPHP(toPHP($degenerateBson))), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
|
|
$code .= "\n// Degenerate BSON -> BSON object -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex(fromPHP(MongoDB\BSON\Document::fromBSON($degenerateBson)->toPHP())), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
|
|
$code .= "\n// Degenerate BSON -> Canonical extJSON\n";
|
|
$code .= 'echo json_canonicalize(toCanonicalExtendedJSON($degenerateBson)), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalExtJson . "\n";
|
|
|
|
$code .= "\n// Degenerate BSON -> BSON object -> Canonical extJSON\n";
|
|
$code .= 'echo json_canonicalize(MongoDB\BSON\Document::fromBSON($degenerateBson)->toCanonicalExtendedJSON()), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalExtJson . "\n";
|
|
|
|
if (isset($relaxedExtJson)) {
|
|
$code .= "\n// Degenerate BSON -> Relaxed extJSON\n";
|
|
$code .= 'echo json_canonicalize(toRelaxedExtendedJSON($degenerateBson)), "\n";' . "\n";
|
|
$expect .= $expectedRelaxedExtJson . "\n";
|
|
|
|
$code .= "\n// Degenerate BSON -> BSON object -> Relaxed extJSON\n";
|
|
$code .= 'echo json_canonicalize(MongoDB\BSON\Document::fromBSON($degenerateBson)->toRelaxedExtendedJSON()), "\n";' . "\n";
|
|
$expect .= $expectedRelaxedExtJson . "\n";
|
|
}
|
|
}
|
|
|
|
if (isset($degenerateExtJson) && !$lossy) {
|
|
$code .= "\n// Degenerate extJSON -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex(fromJSON($degenerateExtJson)), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
|
|
$code .= "\n// Degenerate extJSON -> BSON object -> Canonical BSON\n";
|
|
$code .= 'echo bin2hex((string) MongoDB\BSON\Document::fromJSON($degenerateExtJson)), "\n";' . "\n";
|
|
$expect .= $expectedCanonicalBson . "\n";
|
|
}
|
|
|
|
if (isset($relaxedExtJson)) {
|
|
$code .= "\n// Relaxed extJSON -> BSON -> Relaxed extJSON\n";
|
|
$code .= 'echo json_canonicalize(toRelaxedExtendedJSON(fromJSON($relaxedExtJson))), "\n";' . "\n";
|
|
$expect .= $expectedRelaxedExtJson . "\n";
|
|
|
|
$code .= "\n// Relaxed extJSON -> BSON object -> Relaxed extJSON\n";
|
|
$code .= 'echo json_canonicalize(MongoDB\BSON\Document::fromJSON($relaxedExtJson)->toRelaxedExtendedJSON()), "\n";' . "\n";
|
|
$expect .= $expectedRelaxedExtJson . "\n";
|
|
}
|
|
|
|
return [
|
|
'%NAME%' => sprintf('%s: %s', trim($test['description']), trim($case['description'])),
|
|
'%CODE%' => trim($code),
|
|
'%EXPECT%' => trim($expect),
|
|
];
|
|
}
|
|
|
|
function getParamsForDecodeError(array $test, array $case)
|
|
{
|
|
foreach (['description', 'bson'] as $field) {
|
|
if (!isset($case[$field])) {
|
|
throw new InvalidArgumentException(sprintf('Missing "%s" field', $field));
|
|
}
|
|
}
|
|
|
|
$code = sprintf('$bson = hex2bin(%s);', var_export($case['bson'], true)) . "\n\n";
|
|
$code .= "throws(function() use (\$bson) {\n";
|
|
$code .= " var_dump(toPHP(\$bson));\n";
|
|
$code .= "}, 'MongoDB\Driver\Exception\UnexpectedValueException');";
|
|
|
|
/* We do not test for the exception message, since that may differ based on
|
|
* the nature of the decoding error. */
|
|
$expect = "OK: Got MongoDB\Driver\Exception\UnexpectedValueException";
|
|
|
|
return [
|
|
'%NAME%' => sprintf('%s: %s', trim($test['description']), trim($case['description'])),
|
|
'%CODE%' => trim($code),
|
|
'%EXPECT%' => trim($expect),
|
|
];
|
|
}
|
|
|
|
function getParamsForParseError(array $test, array $case)
|
|
{
|
|
foreach (['description', 'string'] as $field) {
|
|
if (!isset($case[$field])) {
|
|
throw new InvalidArgumentException(sprintf('Missing "%s" field', $field));
|
|
}
|
|
}
|
|
|
|
$code = '';
|
|
$expect = '';
|
|
|
|
switch ($test['bson_type']) {
|
|
case '0x00': // Top-level document
|
|
case '0x05': // Binary
|
|
$code = "throws(function() {\n";
|
|
$code .= sprintf(" fromJSON(%s);\n", var_export($case['string'], true));
|
|
$code .= "}, 'MongoDB\Driver\Exception\UnexpectedValueException');";
|
|
|
|
/* We do not test for the exception message, since that may differ
|
|
* based on the nature of the parse error. */
|
|
$expect = "OK: Got MongoDB\Driver\Exception\UnexpectedValueException";
|
|
break;
|
|
|
|
case '0x13': // Decimal128
|
|
$code = "throws(function() {\n";
|
|
$code .= sprintf(" new MongoDB\BSON\Decimal128(%s);\n", var_export($case['string'], true));
|
|
$code .= "}, 'MongoDB\Driver\Exception\InvalidArgumentException');";
|
|
|
|
/* We do not test for the exception message, since that may differ
|
|
* based on the nature of the parse error. */
|
|
$expect = "OK: Got MongoDB\Driver\Exception\InvalidArgumentException";
|
|
break;
|
|
|
|
default:
|
|
throw new UnexpectedValueException(sprintf("Parse errors not supported for BSON type: %s", $test['bson_type']));
|
|
}
|
|
|
|
return [
|
|
'%NAME%' => sprintf('%s: %s', trim($test['description']), trim($case['description'])),
|
|
'%CODE%' => trim($code),
|
|
'%EXPECT%' => trim($expect),
|
|
];
|
|
}
|
|
|
|
function renderPhpt(array $params, array $expectedFailures, array $for64bitOnly)
|
|
{
|
|
$params['%XFAIL%'] = isset($expectedFailures[$params['%NAME%']])
|
|
? "--XFAIL--\n" . $expectedFailures[$params['%NAME%']] . "\n"
|
|
: '';
|
|
$params['%SKIPIF%'] = isset($for64bitOnly[$params['%NAME%']])
|
|
? "--SKIPIF--\n" . "<?php if (PHP_INT_SIZE !== 8) { die(\"skip {$for64bitOnly[$params['%NAME%']]}\"); } ?>" . "\n"
|
|
: '';
|
|
|
|
$template = <<< 'TEMPLATE'
|
|
--TEST--
|
|
%NAME%
|
|
%XFAIL%%SKIPIF%--DESCRIPTION--
|
|
Generated by scripts/convert-bson-corpus-tests.php
|
|
|
|
DO NOT EDIT THIS FILE
|
|
--FILE--
|
|
<?php
|
|
|
|
require_once __DIR__ . '/../utils/basic.inc';
|
|
|
|
%CODE%
|
|
|
|
?>
|
|
===DONE===
|
|
<?php exit(0); ?>
|
|
--EXPECT--
|
|
%EXPECT%
|
|
===DONE===
|
|
TEMPLATE;
|
|
|
|
return str_replace(array_keys($params), array_values($params), $template);
|
|
}
|