@@ -0,0 +1,163 @@
|
||||
<?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)) {
|
||||
$buffer = fread($stream, 65536);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user