168 lines
6.3 KiB
PHP
168 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Generator;
|
|
|
|
/** Read the accepted original one feature at a time, without changing its bytes. */
|
|
final class RetainedGeoJsonCollection
|
|
{
|
|
public function __construct(private readonly string $path) {}
|
|
|
|
/** @return array{type:mixed,feature_count:int,first_properties:array<string,mixed>,exceededTransferLimit:mixed} */
|
|
public function manifest(): array
|
|
{
|
|
$features = $this->features();
|
|
$count = 0;
|
|
$first = [];
|
|
foreach ($features as $feature) {
|
|
if ($count === 0) {
|
|
$first = $feature['properties'] ?? [];
|
|
}
|
|
$count++;
|
|
}
|
|
$metadata = $features->getReturn();
|
|
|
|
return ['type' => $metadata['type'] ?? null, 'feature_count' => $count, 'first_properties' => $first,
|
|
'exceededTransferLimit' => $metadata['exceededTransferLimit'] ?? false];
|
|
}
|
|
|
|
/** @return Generator<int,array<string,mixed>,mixed,array<string,mixed>> */
|
|
public function features(): Generator
|
|
{
|
|
$stream = fopen($this->path, 'rb');
|
|
abort_unless(is_resource($stream), 422, 'Exemplar privado indisponível.');
|
|
$buffer = '';
|
|
$position = 0;
|
|
$peek = static function () use ($stream, &$buffer, &$position): ?string {
|
|
if ($position === strlen($buffer)) {
|
|
$chunk = fread($stream, 65536);
|
|
if ($chunk === false) {
|
|
throw new \RuntimeException('Não foi possível ler o exemplar GeoJSON; original preservado.');
|
|
}
|
|
$buffer = $chunk;
|
|
$position = 0;
|
|
}
|
|
|
|
return $buffer !== '' ? $buffer[$position] : null;
|
|
};
|
|
$take = static function () use ($peek, &$position): ?string {
|
|
$char = $peek();
|
|
if ($char !== null) {
|
|
$position++;
|
|
}
|
|
|
|
return $char;
|
|
};
|
|
$white = static function () use ($peek, $take): void {
|
|
while (($c = $peek()) !== null && str_contains(" \t\r\n", $c)) {
|
|
$take();
|
|
}
|
|
};
|
|
$expect = static function (string $expected) use ($white, $take): void {
|
|
$white();
|
|
abort_unless($take() === $expected, 422, 'Estrutura JSON do exemplar inválida.');
|
|
};
|
|
$value = static function () use ($white, $peek, $take, &$buffer, &$position): string {
|
|
$white();
|
|
$first = $peek();
|
|
abort_unless($first !== null, 422, 'Exemplar JSON truncado.');
|
|
$structured = in_array($first, ['{', '[', '"'], true);
|
|
$result = $structured ? $take() : '';
|
|
$depth = $first === '"' ? 0 : 1;
|
|
$quoted = $first === '"';
|
|
$delimiters = $first === '{' ? '{}"' : ($first === '[' ? '[]"' : '"');
|
|
while (true) {
|
|
abort_unless($peek() !== null, 422, 'Exemplar JSON truncado.');
|
|
// Search whole numeric/coordinate spans in C; never loop through every byte in PHP.
|
|
$span = strcspn($buffer, $structured ? ($quoted ? '"\\' : $delimiters) : ",]} \t\r\n", $position);
|
|
$result .= substr($buffer, $position, $span);
|
|
$position += $span;
|
|
abort_if(strlen($result) > 4 * 1024 * 1024, 422, 'Elemento JSON excede o limite seguro de leitura.');
|
|
if ($position === strlen($buffer)) {
|
|
continue;
|
|
}
|
|
if (! $structured) {
|
|
break;
|
|
}
|
|
$c = $take();
|
|
$result .= $c;
|
|
if ($quoted) {
|
|
if ($c === '\\') {
|
|
$escaped = $take();
|
|
abort_unless($escaped !== null, 422, 'Exemplar JSON truncado.');
|
|
$result .= $escaped;
|
|
} elseif ($c === '"') {
|
|
$quoted = false;
|
|
}
|
|
} elseif ($c === '"') {
|
|
$quoted = true;
|
|
} elseif ($c === $first) {
|
|
$depth++;
|
|
} else {
|
|
$depth--;
|
|
}
|
|
if (! $quoted && $depth === 0) {
|
|
break;
|
|
}
|
|
}
|
|
abort_unless($result !== '', 422, 'Valor JSON ausente.');
|
|
|
|
return $result;
|
|
};
|
|
$decode = static function (string $bytes): mixed {
|
|
try {
|
|
return json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
|
|
} catch (\JsonException) {
|
|
abort(422, 'JSON do exemplar inválido.');
|
|
}
|
|
};
|
|
try {
|
|
$expect('{');
|
|
$metadata = [];
|
|
$keys = [];
|
|
$index = 0;
|
|
$hadFeatures = false;
|
|
$white();
|
|
abort_if($peek() === '}', 422, 'Exemplar sem coleção de elementos.');
|
|
while (true) {
|
|
$key = $decode($value());
|
|
abort_unless(is_string($key) && ! isset($keys[$key]), 422, 'Chave JSON inválida ou repetida.');
|
|
$keys[$key] = true;
|
|
$expect(':');
|
|
if ($key === 'features') {
|
|
$hadFeatures = true;
|
|
$expect('[');
|
|
$white();
|
|
if ($peek() !== ']') {
|
|
while (true) {
|
|
$feature = $decode($value());
|
|
abort_unless(is_array($feature) && ($feature['type'] ?? null) === 'Feature', 422, 'Elemento geográfico inválido.');
|
|
yield $index++ => $feature;
|
|
unset($feature);
|
|
$white();
|
|
if ($peek() === ']') {
|
|
break;
|
|
}$expect(',');
|
|
}
|
|
}
|
|
$expect(']');
|
|
} else {
|
|
$metadata[$key] = $decode($value());
|
|
}
|
|
$white();
|
|
if ($peek() === '}') {
|
|
break;
|
|
}$expect(',');
|
|
}
|
|
$expect('}');
|
|
$white();
|
|
abort_unless($peek() === null && $hadFeatures, 422, 'Exemplar incompleto ou com conteúdo adicional.');
|
|
|
|
return $metadata;
|
|
} finally {
|
|
fclose($stream);
|
|
}
|
|
}
|
|
}
|