Files
Investidor-V2/app/Services/RetainedGeoJsonCollection.php
T

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);
}
}
}