Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/protocol/v6/structures/TypeMarker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Bolt\protocol\v6\structures;

/**
* Type markers for vector data
* @author Michal Stefanak
* @link https://github.com/neo4j-php/Bolt
* @link https://www.neo4j.com/docs/bolt/current/bolt/structure-semantics/#structure-vector
* @package Bolt\protocol\v6\structures
*/
enum TypeMarker: int
{
case INT_8 = 0xC8;
case INT_16 = 0xC9;
case INT_32 = 0xCA;
case INT_64 = 0xCB;
case FLOAT_32 = 0xC6;
case FLOAT_64 = 0xC1;
}
110 changes: 64 additions & 46 deletions src/protocol/v6/structures/Vector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Bolt\packstream\Bytes;
use Bolt\protocol\IStructure;
use Bolt\protocol\v6\structures\TypeMarker;

/**
* Class Vector
Expand All @@ -19,116 +20,133 @@ class Vector implements IStructure
public function __construct(
public readonly Bytes $type_marker,
public readonly Bytes $data
) {
}
) {}

public function __toString(): string
{
return json_encode([(string)$this->type_marker, (string)$this->data]);
}

private static array $formats = ['s', 'l', 'q'];
private static array $endiannessFormats = ['s', 'l', 'q'];

/**
* Encode array as vector structure
* This is a helper method to create Vector structure from array of numbers
* @param int[]|float[] $data
* @param TypeMarker|null $type Optional type to force specific data type .. null = auto decide
* @return self
* @throws \InvalidArgumentException
*/
public static function encode(array $data): self
public static function encode(array $data, ?TypeMarker $type = null): self
{
if (count($data) === 0) {
throw new \InvalidArgumentException('Vector cannot be empty');
$anyFloat = false;
foreach ($data as $entry) {
if (!is_int($entry) && !is_float($entry)) {
throw new \InvalidArgumentException('Vector can only contain numeric values');
}
if (!$anyFloat && is_float($entry)) {
$anyFloat = true;
}
}
if (count($data) > 4096) {
throw new \InvalidArgumentException('Vector cannot have more than 4096 elements');

if ($type === null) {
$type = self::detectTypeMarker($anyFloat, count($data) ? min($data) : 0, count($data) ? max($data) : 0);
}

$anyFloat = in_array(true, array_map('is_float', $data));
$minValue = min($data);
$maxValue = max($data);
$marker = 0;
$packFormat = '';

if ($anyFloat) {
if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32)
$marker = 0xC6;
switch ($type) {
case TypeMarker::FLOAT_32:
$packFormat = 'G';
} else { // Double precision float (FLOAT_64)
$marker = 0xC1;
break;
case TypeMarker::FLOAT_64:
$packFormat = 'E';
}
} else {
if ($minValue >= -128 && $maxValue <= 127) { // INT_8
$marker = 0xC8;
break;
case TypeMarker::INT_8:
$packFormat = 'c';
} elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16
$marker = 0xC9;
break;
case TypeMarker::INT_16:
$packFormat = 's';
} elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32
$marker = 0xCA;
break;
case TypeMarker::INT_32:
$packFormat = 'l';
} else { // INT_64
$marker = 0xCB;
break;
case TypeMarker::INT_64:
$packFormat = 'q';
}
}

if ($marker === 0) {
throw new \InvalidArgumentException('Unsupported data type for vector');
break;
}

// Pack the data
$packed = [];
$littleEndian = unpack('S', "\x01\x00")[1] === 1;
foreach ($data as $entry) {
$value = pack($packFormat, $entry);
$packed[] = in_array($packFormat, self::$formats) && $littleEndian ? strrev($value) : $value;
$value = pack($packFormat, $anyFloat ? (float)$entry : (int)$entry);
$packed[] = in_array($packFormat, self::$endiannessFormats) && $littleEndian ? strrev($value) : $value;
}

return new self(new Bytes([chr($marker)]), new Bytes($packed));
return new self(new Bytes([chr($type->value)]), new Bytes($packed));
}

private static function detectTypeMarker(bool $anyFloat, int|float $minValue, int|float $maxValue): TypeMarker
{
if ($anyFloat) {
if ($minValue >= -3.4028235e+38 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32)
return TypeMarker::FLOAT_32;
} else { // Double precision float (FLOAT_64)
return TypeMarker::FLOAT_64;
}
} else {
if ($minValue >= -128 && $maxValue <= 127) { // INT_8
return TypeMarker::INT_8;
} elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16
return TypeMarker::INT_16;
} elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32
return TypeMarker::INT_32;
} else { // INT_64
return TypeMarker::INT_64;
}
}
}

/**
* Decode vector structure .. returns binary $this->data as array
* Decode vector structure .. returns binary $this->data as array of numbers
* @return int[]|float[]
* @throws \InvalidArgumentException
*/
public function decode(): array
{
switch (ord($this->type_marker[0])) {
case 0xC8: // INT_8
case TypeMarker::INT_8->value: // INT_8
$size = 1;
$unpackFormat = 'c';
break;
case 0xC9: // INT_16
case TypeMarker::INT_16->value: // INT_16
$size = 2;
$unpackFormat = 's';
break;
case 0xCA: // INT_32
case TypeMarker::INT_32->value: // INT_32
$size = 4;
$unpackFormat = 'l';
break;
case 0xCB: // INT_64
case TypeMarker::INT_64->value: // INT_64
$size = 8;
$unpackFormat = 'q';
break;
case 0xC6: // FLOAT_32
case TypeMarker::FLOAT_32->value: // FLOAT_32
$size = 4;
$unpackFormat = 'G';
break;
case 0xC1: // FLOAT_64
case TypeMarker::FLOAT_64->value: // FLOAT_64
$size = 8;
$unpackFormat = 'E';
break;
default:
throw new \InvalidArgumentException('Unknown vector type marker: ' . $this->type_marker[0]);
}

$output = [];
$littleEndian = unpack('S', "\x01\x00")[1] === 1;
foreach(mb_str_split((string)$this->data, $size, '8bit') as $value) {
$output[] = unpack($unpackFormat, in_array($unpackFormat, self::$formats) && $littleEndian ? strrev($value) : $value)[1];
foreach (mb_str_split((string)$this->data, $size, '8bit') as $value) {
$output[] = unpack($unpackFormat, in_array($unpackFormat, self::$endiannessFormats) && $littleEndian ? strrev($value) : $value)[1];
}

return $output;
Expand Down
11 changes: 6 additions & 5 deletions tests/structures/V6/StructuresTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ public function testVector(AProtocol $protocol)
//unpack
$res = iterator_to_array(
$protocol
->run('CYPHER 25 RETURN vector([1.05, 0.123, 5], 3, FLOAT),
->run(
'CYPHER 25 RETURN vector([1.05, 0.123, 5], 3, FLOAT),
vector([1.05, 0.123, 5], 3, FLOAT32),
vector([5, 543, 342765], 3, INTEGER),
vector([5, -60, 120], 3, INTEGER8),
vector([5, -20000, 30000], 3, INTEGER16),
vector([5, -2000000000, 2000000000], 3, INTEGER32)',
[], ['mode' => 'r'])
[],
['mode' => 'r']
)
->pull()
->getResponses(),
false
Expand Down Expand Up @@ -106,8 +109,6 @@ public function testVector(AProtocol $protocol)
public function testVectorExceptions()
{
$this->expectException(\InvalidArgumentException::class);
Vector::encode([]);
$this->expectException(\InvalidArgumentException::class);
Vector::encode(range(1, 5000));
Vector::encode(['abc', 'def']);
}
}