├── classes ├── Types.h ├── DataDecodeException.h ├── ByteBufferReader.h ├── ByteBufferWriter.h ├── ByteBufferReader.cpp ├── ByteBufferWriter.cpp └── Types.cpp ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitattributes ├── stubs ├── DataDecodeException.stub.php ├── DataDecodeException_arginfo.h ├── ByteBufferReader.stub.php ├── ByteBufferWriter.stub.php ├── ByteBufferReader_arginfo.h └── ByteBufferWriter_arginfo.h ├── config.w32 ├── php_encoding.h ├── tests ├── reader │ ├── get-data.phpt │ ├── new.phpt │ ├── serialize-unserialize.phpt │ ├── var-dump.phpt │ ├── unread-length.phpt │ ├── clone.phpt │ └── read-byte-array.phpt ├── writer │ ├── trim.phpt │ ├── serialize-unserialize.phpt │ ├── write-byte-array.phpt │ ├── get-data-no-reserved.phpt │ ├── clone.phpt │ ├── var-dump.phpt │ ├── new.phpt │ ├── reserve.phpt │ └── set-offset.phpt ├── fixed-complex │ ├── read-triad.phpt │ └── write-triad.phpt ├── varint │ ├── read-update-offset.phpt │ ├── varint-binary-samples.inc │ ├── read-large-long.phpt │ ├── read-not-enough-bytes.phpt │ ├── read-too-many-bytes.phpt │ ├── write-unsigned-negative-input.phpt │ ├── varlong-binary-samples.inc │ └── varint-parity.phpt ├── fixed-simple │ ├── read-update-offset.phpt │ ├── read-samples.inc │ ├── pack-unpack-parity.phpt │ └── read-not-enough-bytes.phpt ├── symmetry-tests.inc ├── type-samples.inc └── symmetry.phpt ├── config.m4 ├── .gitignore ├── ZendUtil.h ├── LICENSE ├── encoding.cpp ├── README.md └── Serializers.h /classes/Types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | void init_class_Types(void); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.php text eol=lf 3 | *.c text eol=lf 4 | *.cpp text eol=lf 5 | *.h text eol=lf 6 | *.m4 text eol=lf 7 | /config.w32 text eol=crlf -------------------------------------------------------------------------------- /classes/DataDecodeException.h: -------------------------------------------------------------------------------- 1 | #ifndef DATA_DECODE_EXCEPTION_H 2 | #define DATA_DECODE_EXCEPTION_H 3 | extern "C" { 4 | #include "php.h" 5 | } 6 | 7 | extern zend_class_entry* data_decode_exception_ce; 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /stubs/DataDecodeException.stub.php: -------------------------------------------------------------------------------- 1 | getData()); 10 | debug_zval_dump($reader->getData()); 11 | debug_zval_dump($reader->getData()); 12 | ?> 13 | --EXPECT-- 14 | string(10) "aaaaaaaaaa" refcount(2) 15 | string(10) "aaaaaaaaaa" refcount(2) 16 | string(10) "aaaaaaaaaa" refcount(2) 17 | -------------------------------------------------------------------------------- /tests/reader/new.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that new ByteBufferReader() works correctly 3 | --FILE-- 4 | getData()); 10 | 11 | $buffer = new ByteBufferReader(""); 12 | var_dump($buffer->getData()); 13 | 14 | $buffer = new ByteBufferReader("hello world"); 15 | $buffer->setOffset(6); 16 | var_dump($buffer->readByteArray(5)); 17 | ?> 18 | --EXPECT-- 19 | string(3) "abc" 20 | string(0) "" 21 | string(5) "world" 22 | -------------------------------------------------------------------------------- /config.m4: -------------------------------------------------------------------------------- 1 | PHP_ARG_ENABLE([encoding], 2 | [whether to enable encoding support], 3 | [AS_HELP_STRING([--enable-encoding], 4 | [Enable encoding support])], 5 | [no]) 6 | 7 | if test "$PHP_ENCODING" != "no"; then 8 | PHP_REQUIRE_CXX() 9 | 10 | dnl the 6th parameter here is required for C++ shared extensions 11 | PHP_NEW_EXTENSION(encoding, encoding.cpp classes/ByteBufferReader.cpp classes/ByteBufferWriter.cpp classes/Types.cpp, $ext_shared,,-std=c++20 -Wall -Werror, yes) 12 | PHP_ADD_BUILD_DIR($ext_builddir/classes, 1) 13 | fi 14 | -------------------------------------------------------------------------------- /tests/writer/trim.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferWriter::trim() works correctly 3 | --FILE-- 4 | reserve(100); 10 | $buffer->writeByteArray("aaaaa"); 11 | var_dump($buffer->getReservedLength()); 12 | $buffer->trim(); 13 | var_dump($buffer->getReservedLength()); 14 | 15 | $buffer = new ByteBufferWriter("aaaaaaaaaa"); 16 | $buffer->trim(); 17 | var_dump($buffer->getReservedLength()); 18 | ?> 19 | --EXPECT-- 20 | int(100) 21 | int(5) 22 | int(10) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lo 2 | *.la 3 | .libs 4 | acinclude.m4 5 | aclocal.m4 6 | autom4te.cache 7 | build 8 | config.guess 9 | config.h 10 | config.h.in 11 | config.log 12 | config.nice 13 | config.status 14 | config.sub 15 | configure 16 | configure.ac 17 | configure.in 18 | include 19 | install-sh 20 | libtool 21 | ltmain.sh 22 | Makefile 23 | Makefile.fragments 24 | Makefile.global 25 | Makefile.objects 26 | missing 27 | mkinstalldirs 28 | modules 29 | run-tests.php 30 | tests/**/*.diff 31 | tests/**/*.out 32 | tests/**/*.php 33 | tests/**/*.exp 34 | tests/**/*.log 35 | tests/**/*.sh 36 | -------------------------------------------------------------------------------- /tests/writer/serialize-unserialize.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer serializes and unserializes correctly 3 | --FILE-- 4 | 14 | --EXPECTF-- 15 | object(pmmp\encoding\ByteBufferWriter)#%d (2) { 16 | ["buffer"]=> 17 | string(11) "hello world" 18 | ["offset"]=> 19 | int(11) 20 | } 21 | object(pmmp\encoding\ByteBufferWriter)#%d (2) { 22 | ["buffer"]=> 23 | string(11) "hello world" 24 | ["offset"]=> 25 | int(11) 26 | } 27 | -------------------------------------------------------------------------------- /tests/reader/serialize-unserialize.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferReader serializes and unserializes correctly 3 | --FILE-- 4 | 14 | --EXPECTF-- 15 | object(pmmp\encoding\ByteBufferReader)#%d (2) { 16 | ["buffer"]=> 17 | string(11) "hello world" 18 | ["offset"]=> 19 | int(0) 20 | } 21 | object(pmmp\encoding\ByteBufferReader)#%d (2) { 22 | ["buffer"]=> 23 | string(11) "hello world" 24 | ["offset"]=> 25 | int(0) 26 | } 27 | -------------------------------------------------------------------------------- /tests/writer/write-byte-array.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferWriter::writeByteArray() works as expected 3 | --FILE-- 4 | writeByteArray("12345"); 10 | 11 | var_dump($buffer->getOffset()); 12 | var_dump($buffer->getData()); 13 | 14 | $buffer->writeByteArray("67"); 15 | 16 | var_dump($buffer->getOffset()); 17 | var_dump($buffer->getData()); 18 | 19 | $buffer->setOffset(2); 20 | $buffer->writeByteArray("890"); 21 | var_dump($buffer->getOffset()); 22 | var_dump($buffer->getData()); 23 | ?> 24 | --EXPECT-- 25 | int(5) 26 | string(5) "12345" 27 | int(7) 28 | string(7) "1234567" 29 | int(5) 30 | string(7) "1289067" 31 | -------------------------------------------------------------------------------- /classes/ByteBufferReader.h: -------------------------------------------------------------------------------- 1 | #ifndef BYTE_BUFFER_READER_H 2 | #define BYTE_BUFFER_READER_H 3 | 4 | extern "C" { 5 | #include "php.h" 6 | } 7 | #include "../ZendUtil.h" 8 | 9 | typedef struct _byte_buffer_reader_t { 10 | zend_string* buffer; 11 | size_t offset; 12 | } byte_buffer_reader_t; 13 | 14 | typedef struct _byte_buffer_reader_zend_object { 15 | byte_buffer_reader_t reader; 16 | zend_object std; 17 | } byte_buffer_reader_zend_object; 18 | 19 | #define READER_FROM_ZVAL(zv) fetch_from_zend_object(Z_OBJ_P(zv)) 20 | #define READER_THIS() READER_FROM_ZVAL(ZEND_THIS) 21 | 22 | extern zend_class_entry* byte_buffer_reader_ce; 23 | 24 | zend_class_entry* init_class_ByteBufferReader(void); 25 | #endif 26 | -------------------------------------------------------------------------------- /stubs/DataDecodeException_arginfo.h: -------------------------------------------------------------------------------- 1 | /* This is a generated file, edit the .stub.php file instead. 2 | * Stub hash: d9c3dfd4ba78bcdcd5e4af033c6f978a5b1265fc */ 3 | 4 | static zend_class_entry *register_class_pmmp_encoding_DataDecodeException(zend_class_entry *class_entry_RuntimeException) 5 | { 6 | zend_class_entry ce, *class_entry; 7 | 8 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "DataDecodeException", NULL); 9 | #if (PHP_VERSION_ID >= 80400) 10 | class_entry = zend_register_internal_class_with_flags(&ce, class_entry_RuntimeException, ZEND_ACC_FINAL); 11 | #else 12 | class_entry = zend_register_internal_class_ex(&ce, class_entry_RuntimeException); 13 | class_entry->ce_flags |= ZEND_ACC_FINAL; 14 | #endif 15 | 16 | return class_entry; 17 | } 18 | -------------------------------------------------------------------------------- /tests/reader/var-dump.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferReader::__debugInfo() works as expected 3 | --FILE-- 4 | 13 | --EXPECTF-- 14 | object(pmmp\encoding\ByteBufferReader)#%d (2) { 15 | ["buffer"]=> 16 | string(22) "looooooooooooong short" 17 | ["offset"]=> 18 | int(0) 19 | } 20 | object(pmmp\encoding\ByteBufferReader)#%d (2) { 21 | ["buffer"]=> 22 | string(22) "looooooooooooong short" 23 | ["offset"]=> 24 | int(0) 25 | } 26 | -------------------------------------------------------------------------------- /ZendUtil.h: -------------------------------------------------------------------------------- 1 | #ifndef HAVE_BITARRAY_ZEND_UTIL_CPP_H 2 | #define HAVE_BITARRAY_ZEND_UTIL_CPP_H 3 | 4 | extern "C" { 5 | #include "php.h" 6 | } 7 | 8 | template 9 | static inline class_name * fetch_from_zend_object(zend_object *obj) { 10 | return (class_name *)((char *)obj - XtOffsetOf(class_name, std)); 11 | } 12 | 13 | template 14 | static class_name* alloc_custom_zend_object(zend_class_entry* ce, zend_object_handlers *handlers) { 15 | class_name* object = (class_name*)emalloc(sizeof(class_name) + zend_object_properties_size(ce)); 16 | 17 | zend_object_std_init(&object->std, ce); 18 | object_properties_init(&object->std, ce); 19 | 20 | object->std.handlers = handlers; 21 | 22 | return object; 23 | } 24 | 25 | #endif 26 | 27 | -------------------------------------------------------------------------------- /tests/reader/unread-length.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test ByteBuffer::getUnreadLength() 3 | --FILE-- 4 | getUnreadLength()); //11 10 | 11 | $buffer->readByteArray(1); 12 | var_dump($buffer->getUnreadLength()); //10 13 | 14 | $buffer->setOffset(5); 15 | var_dump($buffer->getUnreadLength()); //6 16 | 17 | $buffer->readByteArray(6); 18 | var_dump($buffer->getUnreadLength()); //0 19 | 20 | $buffer->setOffset(11); //length of buffer 21 | var_dump($buffer->getUnreadLength()); //0 22 | 23 | $buffer->setOffset(0); 24 | var_dump($buffer->getUnreadLength()); //11 25 | ?> 26 | --EXPECT-- 27 | int(11) 28 | int(10) 29 | int(6) 30 | int(0) 31 | int(0) 32 | int(11) 33 | -------------------------------------------------------------------------------- /tests/writer/get-data-no-reserved.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferWriter::getData() doesn't show unused bytes in reserved space 3 | --DESCRIPTION-- 4 | ByteBufferWriter may allocate more bytes than it needs in order to minimize allocations. 5 | --FILE-- 6 | getData())); 17 | 18 | $buffer = new ByteBufferWriter("aaaaaaaaaa"); 19 | var_dump($buffer->getData()); 20 | ?> 21 | --EXPECT-- 22 | string(10) "0000000000" 23 | string(10) "aaaaaaaaaa" 24 | -------------------------------------------------------------------------------- /classes/ByteBufferWriter.h: -------------------------------------------------------------------------------- 1 | #ifndef BYTE_BUFFER_WRITER_H 2 | #define BYTE_BUFFER_WRITER_H 3 | 4 | extern "C" { 5 | #include "php.h" 6 | } 7 | #include "../ZendUtil.h" 8 | 9 | typedef struct _byte_buffer_writer_t { 10 | unsigned char* buffer; 11 | size_t length; 12 | size_t offset; 13 | size_t used; 14 | } byte_buffer_writer_t; 15 | 16 | typedef struct _byte_buffer_writer_zend_object { 17 | byte_buffer_writer_t writer; 18 | zend_object std; 19 | } byte_buffer_writer_zend_object; 20 | 21 | #define WRITER_FROM_ZVAL(zv) fetch_from_zend_object(Z_OBJ_P(zv)) 22 | #define WRITER_THIS() WRITER_FROM_ZVAL(ZEND_THIS) 23 | 24 | extern zend_class_entry* byte_buffer_writer_ce; 25 | 26 | zend_class_entry* init_class_ByteBufferWriter(void); 27 | #endif 28 | -------------------------------------------------------------------------------- /tests/fixed-complex/read-triad.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that reading triads works correctly 3 | --DESCRIPTION-- 4 | Triads require special implementation due to not being a power-of-two size. This opens avenues for extra bugs that must be tested for. 5 | --FILE-- 6 | setOffset(0); 17 | var_dump(LE::readSignedTriad($buffer)); 18 | 19 | $buffer->setOffset(0); 20 | var_dump(BE::readUnsignedTriad($buffer)); 21 | 22 | $buffer->setOffset(0); 23 | var_dump(LE::readUnsignedTriad($buffer)); 24 | 25 | ?> 26 | --EXPECT-- 27 | int(-65536) 28 | int(255) 29 | int(16711680) 30 | int(255) 31 | 32 | -------------------------------------------------------------------------------- /tests/reader/clone.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that cloning ByteBufferReader works correctly 3 | --DESCRIPTION-- 4 | byte_buffer->used wasn't being copied during clones, leading to the cloned object appearing to have an empty buffer 5 | --FILE-- 6 | getData()); 16 | var_dump($buffer2->getData()); 17 | 18 | ?> 19 | --EXPECTF-- 20 | object(pmmp\encoding\ByteBufferReader)#%d (2) { 21 | ["buffer"]=> 22 | string(9) "Some Data" 23 | ["offset"]=> 24 | int(0) 25 | } 26 | object(pmmp\encoding\ByteBufferReader)#%d (2) { 27 | ["buffer"]=> 28 | string(9) "Some Data" 29 | ["offset"]=> 30 | int(0) 31 | } 32 | string(9) "Some Data" 33 | string(9) "Some Data" 34 | -------------------------------------------------------------------------------- /tests/writer/clone.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that cloning ByteBufferWriter works correctly 3 | --DESCRIPTION-- 4 | byte_buffer->used wasn't being copied during clones, leading to the cloned object appearing to have an empty buffer 5 | --FILE-- 6 | getData()); 16 | var_dump($buffer2->getData()); 17 | 18 | ?> 19 | --EXPECTF-- 20 | object(pmmp\encoding\ByteBufferWriter)#%d (2) { 21 | ["buffer"]=> 22 | string(9) "Some Data" 23 | ["offset"]=> 24 | int(9) 25 | } 26 | object(pmmp\encoding\ByteBufferWriter)#%d (2) { 27 | ["buffer"]=> 28 | string(9) "Some Data" 29 | ["offset"]=> 30 | int(9) 31 | } 32 | string(9) "Some Data" 33 | string(9) "Some Data" 34 | -------------------------------------------------------------------------------- /tests/varint/read-update-offset.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | VarInt::read(Un)Signed(Int|Long)() must correctly update the internal offset 3 | --EXTENSIONS-- 4 | encoding 5 | --FILE-- 6 | setOffset($originalOffset); 16 | 17 | $function($buffer); 18 | var_dump($buffer->getOffset() === $size + $originalOffset); 19 | } 20 | 21 | test(VarInt::readUnsignedInt(...), 5); 22 | test(VarInt::readSignedInt(...), 5); 23 | test(VarInt::readUnsignedLong(...), 10); 24 | test(VarInt::readSignedLong(...), 10); 25 | 26 | ?> 27 | --EXPECT-- 28 | bool(true) 29 | bool(true) 30 | bool(true) 31 | bool(true) 32 | -------------------------------------------------------------------------------- /tests/writer/var-dump.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferWriterWriter::__debugInfo() doesn't read out-of-bounds when dumping internal buffer 3 | --DESCRIPTION-- 4 | The debuginfo handler wasn't accounting for the possibility that the used portion of the internal buffer may be smaller than the allocated capacity. 5 | In some cases, this could lead to segfaults due to reading out-of-bounds. 6 | --FILE-- 7 | writeByteArray("looooooooooooong"); 13 | $buffer->writeByteArray(" short"); //this will result in a buffer larger than the contents as the previous size will be doubled 14 | var_dump($buffer); 15 | ?> 16 | --EXPECTF-- 17 | object(pmmp\encoding\ByteBufferWriter)#%d (2) { 18 | ["buffer"]=> 19 | string(22) "looooooooooooong short" 20 | ["offset"]=> 21 | int(22) 22 | } 23 | -------------------------------------------------------------------------------- /tests/varint/varint-binary-samples.inc: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 0 => 127, 7 | 1 => 127, 8 | 2 => -127, 9 | 3 => '7f', 10 | 4 => 'fe01', 11 | 5 => 'fd01', 12 | ), 13 | 1 => 14 | array ( 15 | 0 => 16256, 16 | 1 => 16256, 17 | 2 => -16256, 18 | 3 => '807f', 19 | 4 => '80fe01', 20 | 5 => 'fffd01', 21 | ), 22 | 2 => 23 | array ( 24 | 0 => 2080768, 25 | 1 => 2080768, 26 | 2 => -2080768, 27 | 3 => '80807f', 28 | 4 => '8080fe01', 29 | 5 => 'fffffd01', 30 | ), 31 | 3 => 32 | array ( 33 | 0 => 266338304, 34 | 1 => 266338304, 35 | 2 => -266338304, 36 | 3 => '8080807f', 37 | 4 => '808080fe01', 38 | 5 => 'fffffffd01', 39 | ), 40 | 4 => 41 | array ( 42 | 0 => 34091302912, 43 | 1 => 34091302912, 44 | 2 => -34091302912, 45 | 3 => '808080800f', 46 | 4 => 'ffffffff01', 47 | 5 => '8080808002', 48 | ), 49 | ); 50 | -------------------------------------------------------------------------------- /tests/writer/new.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that new ByteBufferWriter() works correctly 3 | --DESCRIPTION-- 4 | Currently, giving a string to the constructor of a writer behaves the same way as doing 5 | --FILE-- 6 | getData()); 12 | $buffer->writeByteArray("hello world"); 13 | var_dump($buffer->getData()); 14 | 15 | $buffer = new ByteBufferWriter("hello world"); 16 | $buffer2 = new ByteBufferWriter(); 17 | $buffer2->writeByteArray("hello world"); 18 | 19 | //these should have the same buffer and offset 20 | var_dump($buffer, $buffer2); 21 | 22 | ?> 23 | --EXPECTF-- 24 | string(0) "" 25 | string(11) "hello world" 26 | object(pmmp\encoding\ByteBufferWriter)#%d (2) { 27 | ["buffer"]=> 28 | string(11) "hello world" 29 | ["offset"]=> 30 | int(11) 31 | } 32 | object(pmmp\encoding\ByteBufferWriter)#%d (2) { 33 | ["buffer"]=> 34 | string(11) "hello world" 35 | ["offset"]=> 36 | int(11) 37 | } 38 | -------------------------------------------------------------------------------- /tests/writer/reserve.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that reserving works correctly 3 | --FILE-- 4 | reserve(40); 12 | var_dump($buffer->getReservedLength()); //40 13 | var_dump($buffer->getData()); //still empty, we haven't used any space 14 | 15 | Byte::writeSigned($buffer, ord("a")); 16 | var_dump($buffer->getReservedLength()); //40 17 | var_dump($buffer->getData()); 18 | 19 | $buffer->writeByteArray(str_repeat("a", 40)); //cause new allocation, this should double the buffer size to 80 20 | var_dump($buffer->getReservedLength()); //80 21 | var_dump($buffer->getData()); 22 | 23 | try{ 24 | $buffer->reserve(-1); 25 | }catch(\ValueError $e){ 26 | echo $e->getMessage() . PHP_EOL; 27 | } 28 | ?> 29 | --EXPECT-- 30 | int(40) 31 | string(0) "" 32 | int(40) 33 | string(1) "a" 34 | int(80) 35 | string(41) "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 36 | Length must be greater than zero 37 | -------------------------------------------------------------------------------- /tests/fixed-complex/write-triad.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that writing triads works as expected 3 | --DESCRIPTION-- 4 | Triads require special implementation due to not being a power-of-two size. This opens avenues for extra bugs that must be tested for. 5 | --FILE-- 6 | getData() === "\xff\x00\x00"); 15 | 16 | $buffer = new ByteBufferWriter(); 17 | LE::writeSignedTriad($buffer, -65536); 18 | var_dump($buffer->getData() === "\x00\x00\xff"); 19 | 20 | $buffer = new ByteBufferWriter(); 21 | BE::writeUnsignedTriad($buffer, -65536); 22 | var_dump($buffer->getData() === "\xff\x00\x00"); 23 | 24 | $buffer = new ByteBufferWriter(); 25 | LE::writeUnsignedTriad($buffer, -65536); 26 | var_dump($buffer->getData() === "\x00\x00\xff"); 27 | 28 | ?> 29 | --EXPECT-- 30 | bool(true) 31 | bool(true) 32 | bool(true) 33 | bool(true) 34 | -------------------------------------------------------------------------------- /tests/fixed-simple/read-update-offset.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | (BE|LE)::read*() for fixed-size type must correctly update the reference $offset parameter if given 3 | --EXTENSIONS-- 4 | encoding 5 | --FILE-- 6 | setOffset($originalOffset); 16 | 17 | $function($buffer); 18 | var_dump($buffer->getOffset() === $originalOffset + $size); 19 | } 20 | 21 | $functions = require __DIR__ . '/read-samples.inc'; 22 | 23 | foreach($functions as [$function, $buf]){ 24 | test($function, strlen($buf)); 25 | } 26 | 27 | ?> 28 | --EXPECT-- 29 | bool(true) 30 | bool(true) 31 | bool(true) 32 | bool(true) 33 | bool(true) 34 | bool(true) 35 | bool(true) 36 | bool(true) 37 | bool(true) 38 | bool(true) 39 | bool(true) 40 | bool(true) 41 | bool(true) 42 | bool(true) 43 | bool(true) 44 | bool(true) 45 | bool(true) 46 | bool(true) 47 | -------------------------------------------------------------------------------- /tests/writer/set-offset.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferWriter::setOffset() works as expected 3 | --FILE-- 4 | setOffset(0); 10 | 11 | $buffer->writeByteArray("aaaa"); 12 | //setting offset at the end of the buffer is allowed and results in buffer extension on the next write 13 | $buffer->setOffset(4); 14 | $buffer->writeByteArray("bbbb"); 15 | 16 | var_dump($buffer->getData()); 17 | 18 | $buffer->setOffset(6); 19 | $buffer->writeByteArray("cccc"); 20 | 21 | var_dump($buffer->getData()); 22 | 23 | try{ 24 | $buffer->setOffset(-1); 25 | }catch(\ValueError $e){ 26 | echo $e->getMessage() . PHP_EOL; 27 | } 28 | 29 | try{ 30 | $buffer->setOffset(11); 31 | }catch(\ValueError $e){ 32 | echo $e->getMessage() . PHP_EOL; 33 | } 34 | 35 | ?> 36 | --EXPECT-- 37 | string(8) "aaaabbbb" 38 | string(10) "aaaabbcccc" 39 | Offset must not be less than zero or greater than the buffer size 40 | Offset must not be less than zero or greater than the buffer size 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 PMMP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/fixed-simple/read-samples.inc: -------------------------------------------------------------------------------- 1 | 5 bytes) works correctly 3 | --DESCRIPTION-- 4 | The result of the bitwise AND operation has a result of int regardless of the input. 5 | When shifting this to the left by more than 31, the result will be 0 if not explicitly 6 | casted to a larger type before shifting. For varlongs this led to all bits above 32 7 | to be discarded. This test verifies the result of the fix. 8 | --FILE-- 9 | 30 | --EXPECT-- 31 | int(72624976668147841) 32 | int(-36312488334073921) 33 | int(36312488334073920) 34 | -------------------------------------------------------------------------------- /tests/varint/read-not-enough-bytes.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | read(Un)SignedVar(Int|Long)() must correctly handle not being given enough bytes 3 | --EXTENSIONS-- 4 | encoding 5 | --FILE-- 6 | getMessage() . "\n"; 18 | } 19 | 20 | $buffer = new ByteBufferReader("\x00\x00\x00\x00\x80"); 21 | try{ 22 | $buffer->setOffset(4); 23 | $function($buffer); 24 | }catch(DataDecodeException $e){ 25 | echo "offset valid, not enough bytes: " . $e->getMessage() . "\n"; 26 | } 27 | 28 | echo "\n"; 29 | } 30 | 31 | test(VarInt::readUnsignedInt(...)); 32 | test(VarInt::readSignedInt(...)); 33 | test(VarInt::readUnsignedLong(...)); 34 | test(VarInt::readSignedLong(...)); 35 | 36 | ?> 37 | --EXPECT-- 38 | no offset, not enough bytes: No bytes left in buffer 39 | offset valid, not enough bytes: No bytes left in buffer 40 | 41 | no offset, not enough bytes: No bytes left in buffer 42 | offset valid, not enough bytes: No bytes left in buffer 43 | 44 | no offset, not enough bytes: No bytes left in buffer 45 | offset valid, not enough bytes: No bytes left in buffer 46 | 47 | no offset, not enough bytes: No bytes left in buffer 48 | offset valid, not enough bytes: No bytes left in buffer 49 | -------------------------------------------------------------------------------- /tests/reader/read-byte-array.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBufferReader::readByteArray() works as expected 3 | --FILE-- 4 | readByteArray(1); 14 | }catch(DataDecodeException $e){ 15 | echo $e->getMessage() . PHP_EOL; 16 | } 17 | 18 | $buffer = new ByteBufferReader("abcde"); 19 | var_dump($buffer->readByteArray(3)); 20 | var_dump($buffer->readByteArray(1)); 21 | try{ 22 | $buffer->readByteArray(2); 23 | }catch(DataDecodeException $e){ 24 | echo $e->getMessage() . PHP_EOL; 25 | } 26 | 27 | $buffer = new ByteBufferReader("abcde"); 28 | try{ 29 | $buffer->readByteArray(-1); 30 | }catch(\ValueError $e){ 31 | echo $e->getMessage() . PHP_EOL; 32 | } 33 | 34 | //ensure offset is updated properly 35 | $buffer->setOffset(1); 36 | var_dump($buffer->readByteArray(2)); 37 | var_dump($buffer->getOffset()); 38 | 39 | //read with bytes, but all before the buffer start 40 | $buffer->setOffset(5); 41 | try{ 42 | $buffer->readByteArray(2); 43 | }catch(DataDecodeException $e){ 44 | echo $e->getMessage() . PHP_EOL; 45 | } 46 | 47 | $buffer->setOffset(1); 48 | var_dump($buffer->readByteArray($buffer->getUnreadLength())); 49 | ?> 50 | --EXPECT-- 51 | Need at least 1 bytes, but only have 0 bytes 52 | string(3) "abc" 53 | string(1) "d" 54 | Need at least 2 bytes, but only have 1 bytes 55 | Length cannot be negative 56 | string(2) "bc" 57 | int(3) 58 | Need at least 2 bytes, but only have 0 bytes 59 | string(4) "bcde" 60 | -------------------------------------------------------------------------------- /tests/varint/read-too-many-bytes.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | read(Un)SignedVar(Int|Long)() must terminate when too many bytes are given 3 | --DESCRIPTION-- 4 | VarInt reading works by checking if the MSB (most significant bit) is set on the current byte. 5 | If it is, it reads another byte. 6 | 7 | However, this means that it's possible for a string of bytes longer than the max size of a varint/varlong to appear, 8 | potentially locking up the read for a long time if the max number of bytes isn't capped. 9 | 10 | This test verifies that the varint reader functions bail out if there are too many consecutive bytes with MSB set. 11 | --EXTENSIONS-- 12 | encoding 13 | --FILE-- 14 | getMessage() . PHP_EOL; 27 | } 28 | 29 | try{ 30 | VarInt::readSignedInt(new ByteBufferReader($shortBuf)); 31 | }catch(DataDecodeException $e){ 32 | echo "sv32: " . $e->getMessage() . PHP_EOL; 33 | } 34 | 35 | try{ 36 | VarInt::readUnsignedLong(new ByteBufferReader($longBuf)); 37 | }catch(DataDecodeException $e){ 38 | echo "uv64: " . $e->getMessage() . PHP_EOL; 39 | } 40 | 41 | try{ 42 | VarInt::readSignedLong(new ByteBufferReader($longBuf)); 43 | }catch(DataDecodeException $e){ 44 | echo "sv64: " . $e->getMessage() . PHP_EOL; 45 | } 46 | 47 | ?> 48 | --EXPECT-- 49 | uv32: VarInt did not terminate after 5 bytes! 50 | sv32: VarInt did not terminate after 5 bytes! 51 | uv64: VarInt did not terminate after 10 bytes! 52 | sv64: VarInt did not terminate after 10 bytes! 53 | -------------------------------------------------------------------------------- /stubs/ByteBufferReader.stub.php: -------------------------------------------------------------------------------- 1 | > anything = -1. 7 | 8 | This should not be an issue in ext-encoding because of the exclusive use of unsigned integer types for the actual 9 | encoding (meaning that the compiler always generates logical right-shift instructions), but this needs to be tested 10 | anyway. 11 | 12 | In such cases, we expect that the type will be truncated to the appropriate size, and the remainder of the bits 13 | treated as unsigned. This means that -1 written as a varint32 will be interpreted as 4,294,967,295 and so on. 14 | 15 | In the varint64 case we would not truncate, but still interpret the number as its unsigned counterpart during writing. 16 | However, during decoding this would lead to the MSB being interpreted as a sign bit, which also happens for actual 17 | unsigned numbers. Due to the lack of unsigned types in PHP, there isn't much that can be done about this. 18 | --EXTENSIONS-- 19 | encoding 20 | --FILE-- 21 | getData()); 30 | var_dump(VarInt::readUnsignedInt($reader)); 31 | 32 | $buffer->setOffset(0); 33 | VarInt::writeUnsignedLong($buffer, -1); 34 | $reader = new ByteBufferReader($buffer->getData()); 35 | var_dump(VarInt::readUnsignedLong($reader)); 36 | 37 | ?> 38 | --EXPECT-- 39 | int(4294967295) 40 | int(-1) 41 | 42 | -------------------------------------------------------------------------------- /tests/varint/varlong-binary-samples.inc: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 0 => 127, 7 | 1 => 127, 8 | 2 => -127, 9 | 3 => '7f', 10 | 4 => 'fe01', 11 | 5 => 'fd01', 12 | ), 13 | 1 => 14 | array ( 15 | 0 => 16256, 16 | 1 => 16256, 17 | 2 => -16256, 18 | 3 => '807f', 19 | 4 => '80fe01', 20 | 5 => 'fffd01', 21 | ), 22 | 2 => 23 | array ( 24 | 0 => 2080768, 25 | 1 => 2080768, 26 | 2 => -2080768, 27 | 3 => '80807f', 28 | 4 => '8080fe01', 29 | 5 => 'fffffd01', 30 | ), 31 | 3 => 32 | array ( 33 | 0 => 266338304, 34 | 1 => 266338304, 35 | 2 => -266338304, 36 | 3 => '8080807f', 37 | 4 => '808080fe01', 38 | 5 => 'fffffffd01', 39 | ), 40 | 4 => 41 | array ( 42 | 0 => 34091302912, 43 | 1 => 34091302912, 44 | 2 => -34091302912, 45 | 3 => '808080807f', 46 | 4 => '80808080fe01', 47 | 5 => 'fffffffffd01', 48 | ), 49 | 5 => 50 | array ( 51 | 0 => 4363686772736, 52 | 1 => 4363686772736, 53 | 2 => -4363686772736, 54 | 3 => '80808080807f', 55 | 4 => '8080808080fe01', 56 | 5 => 'fffffffffffd01', 57 | ), 58 | 6 => 59 | array ( 60 | 0 => 558551906910208, 61 | 1 => 558551906910208, 62 | 2 => -558551906910208, 63 | 3 => '8080808080807f', 64 | 4 => '808080808080fe01', 65 | 5 => 'fffffffffffffd01', 66 | ), 67 | 7 => 68 | array ( 69 | 0 => 71494644084506624, 70 | 1 => 71494644084506624, 71 | 2 => -71494644084506624, 72 | 3 => '808080808080807f', 73 | 4 => '80808080808080fe01', 74 | 5 => 'fffffffffffffffd01', 75 | ), 76 | 8 => 77 | array ( 78 | 0 => 9151314442816847872, 79 | 1 => 9151314442816847872, 80 | 2 => -9151314442816847872, 81 | 3 => '80808080808080807f', 82 | 4 => '8080808080808080fe01', 83 | 5 => 'fffffffffffffffffd01', 84 | ), 85 | 9 => 86 | array ( 87 | 0 => -9223372036854775807-1, 88 | 1 => -9223372036854775807-1, 89 | 2 => -9223372036854775807-1, 90 | 3 => '80808080808080808001', 91 | 4 => 'ffffffffffffffffff01', 92 | 5 => 'ffffffffffffffffff01', 93 | ), 94 | ); 95 | -------------------------------------------------------------------------------- /encoding.cpp: -------------------------------------------------------------------------------- 1 | /* encoding extension for PHP */ 2 | 3 | #ifdef HAVE_CONFIG_H 4 | # include "config.h" 5 | #endif 6 | 7 | extern "C" { 8 | #include "php.h" 9 | #include "ext/standard/info.h" 10 | #include "php_encoding.h" 11 | #include "ext/spl/spl_exceptions.h" 12 | #include "stubs/DataDecodeException_arginfo.h" 13 | } 14 | #include "classes/ByteBufferReader.h" 15 | #include "classes/ByteBufferWriter.h" 16 | #include "classes/Types.h" 17 | #include "classes/DataDecodeException.h" 18 | 19 | /* {{{ PHP_MINFO_FUNCTION */ 20 | PHP_MINFO_FUNCTION(encoding) 21 | { 22 | php_info_print_table_start(); 23 | php_info_print_table_header(2, "Version", PHP_ENCODING_VERSION); 24 | php_info_print_table_header(2, "Experimental", "YES"); 25 | php_info_print_table_end(); 26 | } 27 | /* }}} */ 28 | 29 | /* {{{ PHP_RINIT_FUNCTION 30 | */ 31 | PHP_RINIT_FUNCTION(encoding) 32 | { 33 | #if defined(ZTS) && defined(COMPILE_DL_ENCODING) 34 | ZEND_TSRMLS_CACHE_UPDATE(); 35 | #endif 36 | 37 | return SUCCESS; 38 | } 39 | /* }}} */ 40 | 41 | zend_class_entry* data_decode_exception_ce; 42 | 43 | PHP_MINIT_FUNCTION(encoding) { 44 | data_decode_exception_ce = register_class_pmmp_encoding_DataDecodeException(spl_ce_RuntimeException); 45 | init_class_ByteBufferReader(); 46 | init_class_ByteBufferWriter(); 47 | init_class_Types(); 48 | 49 | return SUCCESS; 50 | } 51 | 52 | static const zend_module_dep module_dependencies[] = { 53 | ZEND_MOD_REQUIRED("spl") 54 | ZEND_MOD_END 55 | }; 56 | 57 | /* {{{ encoding_module_entry */ 58 | zend_module_entry encoding_module_entry = { 59 | STANDARD_MODULE_HEADER_EX, 60 | NULL, /* ini_entries */ 61 | module_dependencies, 62 | "encoding", /* Extension name */ 63 | NULL, /* zend_function_entry */ 64 | PHP_MINIT(encoding), /* PHP_MINIT - Module initialization */ 65 | NULL, /* PHP_MSHUTDOWN - Module shutdown */ 66 | PHP_RINIT(encoding), /* PHP_RINIT - Request initialization */ 67 | NULL, /* PHP_RSHUTDOWN - Request shutdown */ 68 | PHP_MINFO(encoding), /* PHP_MINFO - Module info */ 69 | PHP_ENCODING_VERSION, /* Version */ 70 | STANDARD_MODULE_PROPERTIES 71 | }; 72 | /* }}} */ 73 | 74 | #ifdef COMPILE_DL_ENCODING 75 | # ifdef ZTS 76 | ZEND_TSRMLS_CACHE_DEFINE() 77 | # endif 78 | ZEND_GET_MODULE(encoding) 79 | #endif 80 | -------------------------------------------------------------------------------- /tests/symmetry-tests.inc: -------------------------------------------------------------------------------- 1 | getData()); 19 | $decoded = $readMethod($reader); 20 | 21 | echo "($sample): match = " . ($sample === $decoded ? "YES" : "NO") . "\n"; 22 | } 23 | } 24 | 25 | function testPackVsNormalSymmetry(array $samples, \Closure $readMethod, \Closure $writeMethod, \Closure $unpackMethod, \Closure $packMethod) : void{ 26 | echo "--- single pack vs buffer read symmetry ---\n"; 27 | foreach($samples as $sample){ 28 | $packed = $packMethod($sample); 29 | 30 | $reader = new ByteBufferReader($packed); 31 | $decoded = $readMethod($reader); 32 | 33 | echo "($sample): match = " . ($sample === $decoded ? "YES" : "NO") . "\n"; 34 | } 35 | echo "--- single buffer write vs unpack symmetry ---\n"; 36 | foreach($samples as $sample){ 37 | $writer = new ByteBufferWriter(); 38 | $writeMethod($writer, $sample); 39 | 40 | $data = $writer->getData(); 41 | $decoded = $unpackMethod($data); 42 | echo "($sample): match = " . ($sample === $decoded ? "YES" : "NO") . "\n"; 43 | } 44 | } 45 | 46 | /** 47 | * @phpstan-template TValue 48 | * @phpstan-param TValue[] $samples 49 | * @phpstan-param \Closure(ByteBufferReader):TValue $readMethod 50 | * @phpstan-param \Closure(ByteBufferWriter,TValue):void $writeMethod 51 | * @phpstan-param \Closure(string):TValue $unpackMethod 52 | * @phpstan-param \Closure(TValue):string $packMethod 53 | */ 54 | function testFullSymmetry(string $label, array $samples, \Closure $readMethod, \Closure $writeMethod, \Closure $unpackMethod, \Closure $packMethod) : void{ 55 | echo "########## TEST " . $label . " ##########\n"; 56 | testSingleSymmetry($samples, $readMethod, $writeMethod); 57 | testPackVsNormalSymmetry($samples, $readMethod, $writeMethod, $unpackMethod, $packMethod); 58 | echo "########## END TEST " . $label . " ##########\n\n"; 59 | } 60 | -------------------------------------------------------------------------------- /stubs/ByteBufferWriter.stub.php: -------------------------------------------------------------------------------- 1 | [Byte::readUnsigned(...), Byte::writeUnsigned(...), [0, 127, 128, 255]], 14 | "c" => [Byte::readSigned(...), Byte::writeSigned(...), [-128, -1, 0, 1, 127]], 15 | 16 | //signed short doesn't have any pack() equivalent 17 | "n" => [BE::readUnsignedShort(...), BE::writeUnsignedShort(...), [0, 1, 32767, 32768, 65535]], 18 | "v" => [LE::readUnsignedShort(...), LE::writeUnsignedShort(...), [0, 1, 32767, 32768, 65535]], 19 | 20 | //signed long doesn't have any pack() equivalent 21 | "N" => [BE::readUnsignedInt(...), BE::writeUnsignedInt(...), [0, 1, 2147483647, 2147483648, 4294967295]], 22 | "V" => [LE::readUnsignedInt(...), LE::writeUnsignedInt(...), [0, 1, 2147483647, 2147483648, 4294967295]], 23 | 24 | //these codes are supposed to be unsigned int64, but there's no such thing in PHP 25 | //the negative bounds must be written weirdly here due to weirdness in PHP parser 26 | "J" => [BE::readSignedLong(...), BE::writeSignedLong(...), [-9223372036854775807-1, -1, 0, 1, 9223372036854775807]], 27 | "P" => [LE::readSignedLong(...), LE::writeSignedLong(...), [-9223372036854775807-1, -1, 0, 1, 9223372036854775807]], 28 | 29 | "G" => [BE::readFloat(...), BE::writeFloat(...), [-1.0, 0.0, 1.0]], 30 | "g" => [LE::readFloat(...), LE::writeFloat(...), [-1.0, 0.0, 1.0]], 31 | 32 | "E" => [BE::readDouble(...), BE::writeDouble(...), [-1.0, 0.0, 1.0]], 33 | "e" => [LE::readDouble(...), LE::writeDouble(...), [-1.0, 0.0, 1.0]], 34 | ]; 35 | 36 | foreach($map as $packCode => [$readFunc, $writeFunc, $testValues]){ 37 | echo "--- Testing equivalents for pack code \"$packCode\" with " . count($testValues) . " samples ---\n"; 38 | foreach($testValues as $value){ 39 | $expectedBytes = pack($packCode, $value); 40 | 41 | $buffer = new ByteBufferWriter(); 42 | $writeFunc($buffer, $value); 43 | 44 | if($expectedBytes !== $buffer->getData()){ 45 | echo "Mismatch \"$packCode\" write: " . bin2hex($expectedBytes) . " " . bin2hex($buffer->getData()) . "\n"; 46 | } 47 | 48 | $buffer = new ByteBufferReader($expectedBytes); 49 | $decodedValue = $readFunc($buffer); 50 | 51 | if($value !== $decodedValue){ 52 | echo "Mismatch \"$packCode\" read: " . $value . " " . $decodedValue . "\n"; 53 | } 54 | } 55 | } 56 | ?> 57 | --EXPECT-- 58 | --- Testing equivalents for pack code "C" with 4 samples --- 59 | --- Testing equivalents for pack code "c" with 5 samples --- 60 | --- Testing equivalents for pack code "n" with 5 samples --- 61 | --- Testing equivalents for pack code "v" with 5 samples --- 62 | --- Testing equivalents for pack code "N" with 5 samples --- 63 | --- Testing equivalents for pack code "V" with 5 samples --- 64 | --- Testing equivalents for pack code "J" with 5 samples --- 65 | --- Testing equivalents for pack code "P" with 5 samples --- 66 | --- Testing equivalents for pack code "G" with 3 samples --- 67 | --- Testing equivalents for pack code "g" with 3 samples --- 68 | --- Testing equivalents for pack code "E" with 3 samples --- 69 | --- Testing equivalents for pack code "e" with 3 samples --- 70 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-22.04 11 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 12 | name: Tests (PHP ${{ matrix.php }}, Valgrind ${{ matrix.valgrind }}, Debug=${{ matrix.debug }}, ZTS=${{ matrix.zts }}) 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php: 17 | - 8.1.31 18 | - 8.2.27 19 | - 8.3.15 20 | - 8.4.2 21 | valgrind: [0, 1] 22 | debug: [enable, disable] 23 | zts: [enable, disable] 24 | 25 | env: 26 | CFLAGS: "-march=x86-64" 27 | CXXFLAGS: "-march=x86-64" 28 | 29 | steps: 30 | - uses: actions/checkout@v5 31 | 32 | - name: Install Valgrind 33 | if: matrix.valgrind == '1' 34 | run: | 35 | sudo apt-get update && sudo apt-get install valgrind 36 | echo "TEST_PHP_ARGS=-m" >> $GITHUB_ENV 37 | echo "PHP_BUILD_CONFIGURE_OPTS=--with-valgrind" >> $GITHUB_ENV 38 | 39 | - name: Restore PHP build cache 40 | uses: actions/cache@v4 41 | id: php-build-cache 42 | with: 43 | path: ${{ github.workspace }}/php 44 | key: php-${{ matrix.php }}-debug-${{ matrix.debug }}-valgrind-${{ matrix.valgrind }}-zts-${{ matrix.zts }}-ubuntu-22.04 45 | 46 | - name: Install PHP build dependencies 47 | if: steps.php-build-cache.outputs.cache-hit != 'true' 48 | run: | 49 | sudo apt-get update && sudo apt-get install \ 50 | re2c 51 | 52 | - name: Get number of CPU cores 53 | if: steps.php-build-cache.outputs.cache-hit != 'true' 54 | uses: SimenB/github-actions-cpu-cores@v2 55 | id: cpu-cores 56 | 57 | - name: Download PHP 58 | if: steps.php-build-cache.outputs.cache-hit != 'true' 59 | working-directory: /tmp 60 | run: curl -L https://github.com/php/php-src/archive/refs/tags/php-${{ matrix.php }}.tar.gz | tar -xz 61 | 62 | - name: Compile PHP 63 | if: steps.php-build-cache.outputs.cache-hit != 'true' 64 | working-directory: /tmp/php-src-php-${{ matrix.php }} 65 | run: | 66 | ./buildconf --force 67 | ./configure \ 68 | --disable-all \ 69 | --enable-cli \ 70 | --${{ matrix.zts }}-zts \ 71 | --${{ matrix.debug}}-debug \ 72 | "$PHP_BUILD_CONFIGURE_OPTS" \ 73 | --prefix="${{ github.workspace }}/php" 74 | make -j ${{ steps.cpu-cores.outputs.count }} install 75 | 76 | - name: Compile extension 77 | run: | 78 | $GITHUB_WORKSPACE/php/bin/phpize 79 | ./configure --with-php-config=$GITHUB_WORKSPACE/php/bin/php-config 80 | make install 81 | 82 | - name: Generate php.ini 83 | run: | 84 | echo "extension=encoding.so" > $GITHUB_WORKSPACE/php.ini 85 | 86 | - name: Run PHPT tests 87 | run: | 88 | $GITHUB_WORKSPACE/php/bin/php ./run-tests.php $TEST_PHP_ARGS -P -q --show-diff --show-slow 30000 -n -c $GITHUB_WORKSPACE/php.ini 89 | 90 | - name: Upload test results 91 | if: failure() 92 | uses: actions/upload-artifact@v5 93 | with: 94 | name: test-results-${{ matrix.php }}-valgrind-${{ matrix.valgrind }} 95 | path: | 96 | ${{ github.workspace }}/tests/* 97 | !${{ github.workspace }}/tests/*.phpt 98 | -------------------------------------------------------------------------------- /tests/type-samples.inc: -------------------------------------------------------------------------------- 1 | 0, 23 | self::SIGNED_SHORT => -32768, 24 | self::SIGNED_TRIAD => -8388608, 25 | self::SIGNED_INT => -2147483648, 26 | 27 | self::UNSIGNED_LONG, //PHP doesn't have an unsigned long type 28 | self::SIGNED_LONG => -9223372036854775807 - 1, //PHP parser weirdness 29 | }; 30 | } 31 | 32 | public function getMax() : int{ 33 | return match ($this) { 34 | self::UNSIGNED_SHORT => 65535, 35 | self::UNSIGNED_TRIAD => 16777215, 36 | self::UNSIGNED_INT => 4294967295, 37 | self::SIGNED_SHORT => 32767, 38 | self::SIGNED_TRIAD => 8388607, 39 | self::SIGNED_INT => 2147483647, 40 | 41 | self::UNSIGNED_LONG, //PHP doesn't have an unsigned long type 42 | self::SIGNED_LONG => 9223372036854775807, 43 | }; 44 | } 45 | 46 | /** 47 | * @return int[] 48 | */ 49 | public function getSamples() : array{ 50 | $min = $this->getMin(); 51 | $signed = $min < 0; 52 | $max = $this->getMax(); 53 | return $signed ? [$min, $min + 1, -1, 0, 1, $max - 1, $max] : [0, 1, $max - 1, $max]; 54 | } 55 | 56 | /** 57 | * @phpstan-return array{\Closure(ByteBufferReader):int, \Closure(ByteBufferWriter,int):void, Closure(int):string, Closure(string):int} 58 | */ 59 | function getMethods(bool $bigEndian) : array{ 60 | $class = $bigEndian ? BE::class : LE::class; 61 | return match($this){ 62 | self::UNSIGNED_SHORT => [$class::readUnsignedShort(...), $class::writeUnsignedShort(...), $class::unpackUnsignedShort(...), $class::packUnsignedShort(...)], 63 | self::SIGNED_SHORT => [$class::readSignedShort(...), $class::writeSignedShort(...), $class::unpackSignedShort(...), $class::packSignedShort(...)], 64 | self::UNSIGNED_TRIAD => [$class::readUnsignedTriad(...), $class::writeUnsignedTriad(...), $class::unpackUnsignedTriad(...), $class::packUnsignedTriad(...)], 65 | self::SIGNED_TRIAD => [$class::readSignedTriad(...), $class::writeSignedTriad(...), $class::unpackSignedTriad(...), $class::packSignedTriad(...)], 66 | self::UNSIGNED_INT => [$class::readUnsignedInt(...), $class::writeUnsignedInt(...), $class::unpackUnsignedInt(...), $class::packUnsignedInt(...)], 67 | self::SIGNED_INT => [$class::readSignedInt(...), $class::writeSignedInt(...), $class::unpackSignedInt(...), $class::packSignedInt(...)], 68 | self::UNSIGNED_LONG => [$class::readUnsignedLong(...), $class::writeUnsignedLong(...), $class::unpackUnsignedLong(...), $class::packUnsignedLong(...)], 69 | self::SIGNED_LONG => [$class::readSignedLong(...), $class::writeSignedLong(...), $class::unpackSignedLong(...), $class::packSignedLong(...)], 70 | }; 71 | } 72 | } 73 | 74 | enum FloatSamples{ 75 | case FLOAT; 76 | case DOUBLE; 77 | 78 | /** 79 | * @phpstan-return array{\Closure(ByteBufferReader):float, \Closure(ByteBufferWriter,float):void, Closure(float):string, Closure(string):float} 80 | */ 81 | public function getMethods(bool $bigEndian) : array{ 82 | $class = $bigEndian ? BE::class : LE::class; 83 | return match($this){ 84 | self::FLOAT => [$class::readFloat(...), $class::writeFloat(...), $class::unpackFloat(...), $class::packFloat(...)], 85 | self::DOUBLE => [$class::readDouble(...), $class::writeDouble(...), $class::unpackDouble(...), $class::packDouble(...)], 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/fixed-simple/read-not-enough-bytes.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | read*() for fixed-size type must throw DataDecodeException when not enough bytes are available 3 | --EXTENSIONS-- 4 | encoding 5 | --FILE-- 6 | getClosureScopeClass()->getShortName() . "::" . $reflect->getName() . " no offset: " . $e->getMessage() . "\n"; 22 | } 23 | 24 | try{ 25 | $buffer = new ByteBufferReader($test); 26 | $buffer->setOffset(15); 27 | $function($buffer); 28 | }catch(DataDecodeException $e){ 29 | $reflect = new \ReflectionFunction($function); 30 | 31 | echo $reflect->getClosureScopeClass()->getShortName() . "::" . $reflect->getName() . " with offset: " . $e->getMessage() . "\n"; 32 | } 33 | 34 | echo "\n"; 35 | } 36 | 37 | ?> 38 | --EXPECT-- 39 | LE::readUnsignedShort no offset: Need at least 2 bytes, but only have 1 bytes 40 | LE::readUnsignedShort with offset: Need at least 2 bytes, but only have 1 bytes 41 | 42 | LE::readSignedShort no offset: Need at least 2 bytes, but only have 1 bytes 43 | LE::readSignedShort with offset: Need at least 2 bytes, but only have 1 bytes 44 | 45 | BE::readUnsignedShort no offset: Need at least 2 bytes, but only have 1 bytes 46 | BE::readUnsignedShort with offset: Need at least 2 bytes, but only have 1 bytes 47 | 48 | BE::readSignedShort no offset: Need at least 2 bytes, but only have 1 bytes 49 | BE::readSignedShort with offset: Need at least 2 bytes, but only have 1 bytes 50 | 51 | LE::readUnsignedInt no offset: Need at least 4 bytes, but only have 1 bytes 52 | LE::readUnsignedInt with offset: Need at least 4 bytes, but only have 1 bytes 53 | 54 | LE::readSignedInt no offset: Need at least 4 bytes, but only have 1 bytes 55 | LE::readSignedInt with offset: Need at least 4 bytes, but only have 1 bytes 56 | 57 | LE::readFloat no offset: Need at least 4 bytes, but only have 1 bytes 58 | LE::readFloat with offset: Need at least 4 bytes, but only have 1 bytes 59 | 60 | BE::readUnsignedInt no offset: Need at least 4 bytes, but only have 1 bytes 61 | BE::readUnsignedInt with offset: Need at least 4 bytes, but only have 1 bytes 62 | 63 | BE::readSignedInt no offset: Need at least 4 bytes, but only have 1 bytes 64 | BE::readSignedInt with offset: Need at least 4 bytes, but only have 1 bytes 65 | 66 | BE::readFloat no offset: Need at least 4 bytes, but only have 1 bytes 67 | BE::readFloat with offset: Need at least 4 bytes, but only have 1 bytes 68 | 69 | LE::readSignedLong no offset: Need at least 8 bytes, but only have 1 bytes 70 | LE::readSignedLong with offset: Need at least 8 bytes, but only have 1 bytes 71 | 72 | BE::readSignedLong no offset: Need at least 8 bytes, but only have 1 bytes 73 | BE::readSignedLong with offset: Need at least 8 bytes, but only have 1 bytes 74 | 75 | LE::readDouble no offset: Need at least 8 bytes, but only have 1 bytes 76 | LE::readDouble with offset: Need at least 8 bytes, but only have 1 bytes 77 | 78 | BE::readDouble no offset: Need at least 8 bytes, but only have 1 bytes 79 | BE::readDouble with offset: Need at least 8 bytes, but only have 1 bytes 80 | 81 | BE::readUnsignedTriad no offset: Need at least 3 bytes, but only have 1 bytes 82 | BE::readUnsignedTriad with offset: Need at least 3 bytes, but only have 1 bytes 83 | 84 | LE::readUnsignedTriad no offset: Need at least 3 bytes, but only have 1 bytes 85 | LE::readUnsignedTriad with offset: Need at least 3 bytes, but only have 1 bytes 86 | 87 | BE::readSignedTriad no offset: Need at least 3 bytes, but only have 1 bytes 88 | BE::readSignedTriad with offset: Need at least 3 bytes, but only have 1 bytes 89 | 90 | LE::readSignedTriad no offset: Need at least 3 bytes, but only have 1 bytes 91 | LE::readSignedTriad with offset: Need at least 3 bytes, but only have 1 bytes 92 | -------------------------------------------------------------------------------- /tests/varint/varint-parity.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that varint encoding matches some known binary values 3 | --DESCRIPTION-- 4 | We already verify that all the encoders and decoders are symmetric in another test. 5 | This test verifies parity with BinaryUtils using some known values. 6 | --FILE-- 7 | [$unsignedValue, $signedValue, $negativeSignedValue, $unsignedBinary, $signedBinary, $negativeSignedBinary]){ 20 | $writer = new ByteBufferWriter(); 21 | VarInt::writeUnsignedInt($writer, $unsignedValue); 22 | check("$case (unsigned)", $writer->getData(), $unsignedBinary); 23 | 24 | $writer = new ByteBufferWriter(); 25 | VarInt::writeSignedInt($writer, $signedValue); 26 | check("$case (signed, positive)", $writer->getData(), $signedBinary); 27 | 28 | $writer = new ByteBufferWriter(); 29 | VarInt::writeSignedInt($writer, $negativeSignedValue); 30 | check("$case (signed, negative)", $writer->getData(), $negativeSignedBinary); 31 | } 32 | 33 | $sample64 = require __DIR__ . '/varlong-binary-samples.inc'; 34 | 35 | echo "--- varlong ---\n"; 36 | foreach($sample64 as $case => [$unsignedValue, $signedValue, $negativeSignedValue, $unsignedBinary, $signedBinary, $negativeSignedBinary]){ 37 | $writer = new ByteBufferWriter(); 38 | VarInt::writeUnsignedLong($writer, $unsignedValue); 39 | check("$case (unsigned)", $writer->getData(), $unsignedBinary); 40 | 41 | $writer = new ByteBufferWriter(); 42 | VarInt::writeSignedLong($writer, $signedValue); 43 | check("$case (signed, positive)", $writer->getData(), $signedBinary); 44 | 45 | $writer = new ByteBufferWriter(); 46 | VarInt::writeSignedLong($writer, $negativeSignedValue); 47 | check("$case (signed, negative)", $writer->getData(), $negativeSignedBinary); 48 | } 49 | echo "--- end ---\n"; 50 | ?> 51 | --EXPECT-- 52 | --- varint --- 53 | case 0 (unsigned): match = YES 54 | case 0 (signed, positive): match = YES 55 | case 0 (signed, negative): match = YES 56 | case 1 (unsigned): match = YES 57 | case 1 (signed, positive): match = YES 58 | case 1 (signed, negative): match = YES 59 | case 2 (unsigned): match = YES 60 | case 2 (signed, positive): match = YES 61 | case 2 (signed, negative): match = YES 62 | case 3 (unsigned): match = YES 63 | case 3 (signed, positive): match = YES 64 | case 3 (signed, negative): match = YES 65 | case 4 (unsigned): match = YES 66 | case 4 (signed, positive): match = YES 67 | case 4 (signed, negative): match = YES 68 | --- varlong --- 69 | case 0 (unsigned): match = YES 70 | case 0 (signed, positive): match = YES 71 | case 0 (signed, negative): match = YES 72 | case 1 (unsigned): match = YES 73 | case 1 (signed, positive): match = YES 74 | case 1 (signed, negative): match = YES 75 | case 2 (unsigned): match = YES 76 | case 2 (signed, positive): match = YES 77 | case 2 (signed, negative): match = YES 78 | case 3 (unsigned): match = YES 79 | case 3 (signed, positive): match = YES 80 | case 3 (signed, negative): match = YES 81 | case 4 (unsigned): match = YES 82 | case 4 (signed, positive): match = YES 83 | case 4 (signed, negative): match = YES 84 | case 5 (unsigned): match = YES 85 | case 5 (signed, positive): match = YES 86 | case 5 (signed, negative): match = YES 87 | case 6 (unsigned): match = YES 88 | case 6 (signed, positive): match = YES 89 | case 6 (signed, negative): match = YES 90 | case 7 (unsigned): match = YES 91 | case 7 (signed, positive): match = YES 92 | case 7 (signed, negative): match = YES 93 | case 8 (unsigned): match = YES 94 | case 8 (signed, positive): match = YES 95 | case 8 (signed, negative): match = YES 96 | case 9 (unsigned): match = YES 97 | case 9 (signed, positive): match = YES 98 | case 9 (signed, negative): match = YES 99 | --- end --- 100 | -------------------------------------------------------------------------------- /stubs/ByteBufferReader_arginfo.h: -------------------------------------------------------------------------------- 1 | /* This is a generated file, edit the .stub.php file instead. 2 | * Stub hash: 62940f560eaa6f7714fc03a0d332265026a502f2 */ 3 | 4 | ZEND_BEGIN_ARG_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferReader___construct, 0, 0, 1) 5 | ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) 6 | ZEND_END_ARG_INFO() 7 | 8 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferReader_getData, 0, 0, IS_STRING, 0) 9 | ZEND_END_ARG_INFO() 10 | 11 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferReader_readByteArray, 0, 1, IS_STRING, 0) 12 | ZEND_ARG_TYPE_INFO(0, length, IS_LONG, 0) 13 | ZEND_END_ARG_INFO() 14 | 15 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferReader_getOffset, 0, 0, IS_LONG, 0) 16 | ZEND_END_ARG_INFO() 17 | 18 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferReader_setOffset, 0, 1, IS_VOID, 0) 19 | ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) 20 | ZEND_END_ARG_INFO() 21 | 22 | #define arginfo_class_pmmp_encoding_ByteBufferReader_getUnreadLength arginfo_class_pmmp_encoding_ByteBufferReader_getOffset 23 | 24 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferReader___serialize, 0, 0, IS_ARRAY, 0) 25 | ZEND_END_ARG_INFO() 26 | 27 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferReader___unserialize, 0, 1, IS_VOID, 0) 28 | ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) 29 | ZEND_END_ARG_INFO() 30 | 31 | #define arginfo_class_pmmp_encoding_ByteBufferReader___debugInfo arginfo_class_pmmp_encoding_ByteBufferReader___serialize 32 | 33 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, __construct); 34 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, getData); 35 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, readByteArray); 36 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, getOffset); 37 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, setOffset); 38 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, getUnreadLength); 39 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, __serialize); 40 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, __unserialize); 41 | ZEND_METHOD(pmmp_encoding_ByteBufferReader, __debugInfo); 42 | 43 | static const zend_function_entry class_pmmp_encoding_ByteBufferReader_methods[] = { 44 | #if (PHP_VERSION_ID >= 80400) 45 | ZEND_RAW_FENTRY("__construct", zim_pmmp_encoding_ByteBufferReader___construct, arginfo_class_pmmp_encoding_ByteBufferReader___construct, ZEND_ACC_PUBLIC, NULL, "/**\n * Constructs a new ByteBufferReader.\n * Offset will be initialized to 0.\n */") 46 | #else 47 | ZEND_RAW_FENTRY("__construct", zim_pmmp_encoding_ByteBufferReader___construct, arginfo_class_pmmp_encoding_ByteBufferReader___construct, ZEND_ACC_PUBLIC) 48 | #endif 49 | #if (PHP_VERSION_ID >= 80400) 50 | ZEND_RAW_FENTRY("getData", zim_pmmp_encoding_ByteBufferReader_getData, arginfo_class_pmmp_encoding_ByteBufferReader_getData, ZEND_ACC_PUBLIC, NULL, "/**\n * Returns the string (byte array) that the reader is reading.\n */") 51 | #else 52 | ZEND_RAW_FENTRY("getData", zim_pmmp_encoding_ByteBufferReader_getData, arginfo_class_pmmp_encoding_ByteBufferReader_getData, ZEND_ACC_PUBLIC) 53 | #endif 54 | #if (PHP_VERSION_ID >= 80400) 55 | ZEND_RAW_FENTRY("readByteArray", zim_pmmp_encoding_ByteBufferReader_readByteArray, arginfo_class_pmmp_encoding_ByteBufferReader_readByteArray, ZEND_ACC_PUBLIC, NULL, "/**\n * Reads $length raw bytes from the buffer at the current offset.\n * The internal offset will be updated by this operation.\n *\n * @throws DataDecodeException if there are not enough bytes available\n */") 56 | #else 57 | ZEND_RAW_FENTRY("readByteArray", zim_pmmp_encoding_ByteBufferReader_readByteArray, arginfo_class_pmmp_encoding_ByteBufferReader_readByteArray, ZEND_ACC_PUBLIC) 58 | #endif 59 | #if (PHP_VERSION_ID >= 80400) 60 | ZEND_RAW_FENTRY("getOffset", zim_pmmp_encoding_ByteBufferReader_getOffset, arginfo_class_pmmp_encoding_ByteBufferReader_getOffset, ZEND_ACC_PUBLIC, NULL, "/**\n * Returns the current internal read offset (the position\n * from which the next read operation will start).\n */") 61 | #else 62 | ZEND_RAW_FENTRY("getOffset", zim_pmmp_encoding_ByteBufferReader_getOffset, arginfo_class_pmmp_encoding_ByteBufferReader_getOffset, ZEND_ACC_PUBLIC) 63 | #endif 64 | #if (PHP_VERSION_ID >= 80400) 65 | ZEND_RAW_FENTRY("setOffset", zim_pmmp_encoding_ByteBufferReader_setOffset, arginfo_class_pmmp_encoding_ByteBufferReader_setOffset, ZEND_ACC_PUBLIC, NULL, "/**\n * Sets the internal read offset to the given value.\n * The offset must be within the bounds of the buffer\n * (0 <= offset <= used length).\n *\n * @throws \\ValueError if the offset is out of bounds\n */") 66 | #else 67 | ZEND_RAW_FENTRY("setOffset", zim_pmmp_encoding_ByteBufferReader_setOffset, arginfo_class_pmmp_encoding_ByteBufferReader_setOffset, ZEND_ACC_PUBLIC) 68 | #endif 69 | #if (PHP_VERSION_ID >= 80400) 70 | ZEND_RAW_FENTRY("getUnreadLength", zim_pmmp_encoding_ByteBufferReader_getUnreadLength, arginfo_class_pmmp_encoding_ByteBufferReader_getUnreadLength, ZEND_ACC_PUBLIC, NULL, "/**\n * Returns the number of bytes available to read after the\n * current offset.\n */") 71 | #else 72 | ZEND_RAW_FENTRY("getUnreadLength", zim_pmmp_encoding_ByteBufferReader_getUnreadLength, arginfo_class_pmmp_encoding_ByteBufferReader_getUnreadLength, ZEND_ACC_PUBLIC) 73 | #endif 74 | ZEND_ME(pmmp_encoding_ByteBufferReader, __serialize, arginfo_class_pmmp_encoding_ByteBufferReader___serialize, ZEND_ACC_PUBLIC) 75 | ZEND_ME(pmmp_encoding_ByteBufferReader, __unserialize, arginfo_class_pmmp_encoding_ByteBufferReader___unserialize, ZEND_ACC_PUBLIC) 76 | ZEND_ME(pmmp_encoding_ByteBufferReader, __debugInfo, arginfo_class_pmmp_encoding_ByteBufferReader___debugInfo, ZEND_ACC_PUBLIC) 77 | ZEND_FE_END 78 | }; 79 | 80 | static zend_class_entry *register_class_pmmp_encoding_ByteBufferReader(void) 81 | { 82 | zend_class_entry ce, *class_entry; 83 | 84 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "ByteBufferReader", class_pmmp_encoding_ByteBufferReader_methods); 85 | #if (PHP_VERSION_ID >= 80400) 86 | class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); 87 | #else 88 | class_entry = zend_register_internal_class_ex(&ce, NULL); 89 | class_entry->ce_flags |= ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES; 90 | #endif 91 | 92 | return class_entry; 93 | } 94 | -------------------------------------------------------------------------------- /classes/ByteBufferReader.cpp: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include "php.h" 3 | #include "Zend/zend_exceptions.h" 4 | #include "../stubs/ByteBufferReader_arginfo.h" 5 | } 6 | 7 | #include "ByteBufferReader.h" 8 | #include "DataDecodeException.h" 9 | #include "../Serializers.h" 10 | 11 | static zend_object_handlers byte_buffer_reader_zend_object_handlers; 12 | zend_class_entry* byte_buffer_reader_ce; 13 | 14 | static void reader_init_properties(byte_buffer_reader_zend_object* object, zend_string* buffer, size_t offset) { 15 | object->reader.buffer = buffer; 16 | zend_string_addref(buffer); 17 | object->reader.offset = offset; 18 | } 19 | 20 | static zend_object* reader_new(zend_class_entry* ce) { 21 | auto object = alloc_custom_zend_object(ce, &byte_buffer_reader_zend_object_handlers); 22 | 23 | reader_init_properties(object, zend_empty_string, 0); 24 | 25 | return &object->std; 26 | } 27 | 28 | static zend_object* reader_clone(zend_object* object) { 29 | auto old_object = fetch_from_zend_object(object); 30 | auto new_object = fetch_from_zend_object(reader_new(object->ce)); 31 | 32 | zend_objects_clone_members(&new_object->std, &old_object->std); 33 | 34 | reader_init_properties(new_object, old_object->reader.buffer, old_object->reader.offset); 35 | 36 | return &new_object->std; 37 | } 38 | 39 | static void reader_free(zend_object* std) { 40 | auto object = fetch_from_zend_object(std); 41 | 42 | zend_string_release_ex(object->reader.buffer, 0); 43 | } 44 | 45 | static int reader_compare_objects(zval* obj1, zval* obj2) { 46 | if (Z_TYPE_P(obj1) == IS_OBJECT && Z_TYPE_P(obj2) == IS_OBJECT) { 47 | if (instanceof_function(Z_OBJCE_P(obj1), byte_buffer_reader_ce) && instanceof_function(Z_OBJCE_P(obj2), byte_buffer_reader_ce)) { 48 | auto object1 = fetch_from_zend_object(Z_OBJ_P(obj1)); 49 | auto object2 = fetch_from_zend_object(Z_OBJ_P(obj2)); 50 | 51 | if ( 52 | object1->reader.offset == object2->reader.offset && 53 | zend_string_equals(object1->reader.buffer, object2->reader.buffer) 54 | ) { 55 | return 0; 56 | } 57 | } 58 | } 59 | 60 | return 1; 61 | } 62 | 63 | #define READER_METHOD(name) PHP_METHOD(pmmp_encoding_ByteBufferReader, name) 64 | 65 | READER_METHOD(__construct) { 66 | zend_string* buffer = NULL; 67 | byte_buffer_reader_zend_object* object; 68 | 69 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 70 | Z_PARAM_STR(buffer) 71 | ZEND_PARSE_PARAMETERS_END(); 72 | 73 | object = READER_THIS(); 74 | if (object->reader.buffer) { 75 | zend_string_release_ex(object->reader.buffer, 0); 76 | } 77 | 78 | reader_init_properties(object, buffer, 0); 79 | } 80 | 81 | READER_METHOD(getData) { 82 | zend_parse_parameters_none_throw(); 83 | 84 | auto object = READER_THIS(); 85 | RETURN_STR_COPY(object->reader.buffer); 86 | } 87 | 88 | READER_METHOD(readByteArray) { 89 | zend_long zlength; 90 | byte_buffer_reader_zend_object* object; 91 | 92 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 93 | Z_PARAM_LONG(zlength) 94 | ZEND_PARSE_PARAMETERS_END(); 95 | 96 | if (zlength < 0) { 97 | zend_value_error("Length cannot be negative"); 98 | return; 99 | } 100 | if (zlength == 0) { //to mirror PM BinaryStream behaviour 101 | RETURN_STR(zend_empty_string); 102 | } 103 | 104 | size_t length = static_cast(zlength); 105 | 106 | object = READER_THIS(); 107 | 108 | if (ZSTR_LEN(object->reader.buffer) - object->reader.offset < length) { 109 | zend_throw_exception_ex(data_decode_exception_ce, 0, "Need at least %zu bytes, but only have %zu bytes", length, ZSTR_LEN(object->reader.buffer) - object->reader.offset); 110 | return; 111 | } 112 | 113 | RETVAL_STRINGL(ZSTR_VAL(object->reader.buffer) + object->reader.offset, length); 114 | object->reader.offset += length; 115 | } 116 | 117 | READER_METHOD(getOffset) { 118 | zend_parse_parameters_none_throw(); 119 | auto object = READER_THIS(); 120 | RETURN_LONG(object->reader.offset); 121 | } 122 | 123 | READER_METHOD(setOffset) { 124 | zend_long offset; 125 | 126 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 127 | Z_PARAM_LONG(offset) 128 | ZEND_PARSE_PARAMETERS_END(); 129 | 130 | auto object = READER_THIS(); 131 | if (offset < 0 || static_cast(offset) > ZSTR_LEN(object->reader.buffer)) { 132 | zend_value_error("Offset must not be less than zero or greater than the available data size"); 133 | return; 134 | } 135 | 136 | object->reader.offset = static_cast(offset); 137 | } 138 | 139 | READER_METHOD(getUnreadLength) { 140 | zend_parse_parameters_none_throw(); 141 | 142 | auto object = READER_THIS(); 143 | RETURN_LONG(ZSTR_LEN(object->reader.buffer) - object->reader.offset); 144 | } 145 | 146 | READER_METHOD(__serialize) { 147 | zend_parse_parameters_none_throw(); 148 | 149 | auto object = READER_THIS(); 150 | array_init(return_value); 151 | zend_string_addref(object->reader.buffer); 152 | add_assoc_str(return_value, "buffer", object->reader.buffer); 153 | add_assoc_long(return_value, "offset", object->reader.offset); 154 | } 155 | 156 | static zval* fetch_serialized_property(HashTable* data, const char* name, int type) { 157 | zval* zv = zend_hash_str_find(data, name, strlen(name)); 158 | if (zv == NULL) { 159 | zend_throw_exception_ex(NULL, 0, "Serialized data is missing \"%s\"", name); 160 | return NULL; 161 | } 162 | if (Z_TYPE_P(zv) != type) { 163 | zend_throw_exception_ex(NULL, 0, "\"%s\" in serialized data should be of type %s, but have %s", name, zend_zval_type_name(zv), zend_get_type_by_const(type)); 164 | return NULL; 165 | } 166 | 167 | return zv; 168 | } 169 | 170 | READER_METHOD(__unserialize) { 171 | HashTable* data; 172 | 173 | ZEND_PARSE_PARAMETERS_START(1, 1) 174 | Z_PARAM_ARRAY_HT(data) 175 | ZEND_PARSE_PARAMETERS_END(); 176 | 177 | zval* buffer = fetch_serialized_property(data, "buffer", IS_STRING); 178 | if (buffer == NULL) { 179 | return; 180 | } 181 | zval* offset = fetch_serialized_property(data, "offset", IS_LONG); 182 | if (offset == NULL) { 183 | return; 184 | } 185 | 186 | auto object = READER_THIS(); 187 | 188 | reader_init_properties(object, Z_STR_P(buffer), Z_LVAL_P(offset)); 189 | } 190 | 191 | READER_METHOD(__debugInfo) { 192 | zend_parse_parameters_none_throw(); 193 | 194 | auto object = READER_THIS(); 195 | array_init(return_value); 196 | zend_string_addref(object->reader.buffer); 197 | add_assoc_str(return_value, "buffer", object->reader.buffer); 198 | add_assoc_long(return_value, "offset", object->reader.offset); 199 | } 200 | 201 | zend_class_entry* init_class_ByteBufferReader(void) { 202 | byte_buffer_reader_ce = register_class_pmmp_encoding_ByteBufferReader(); 203 | byte_buffer_reader_ce->create_object = reader_new; 204 | 205 | byte_buffer_reader_zend_object_handlers = *zend_get_std_object_handlers(); 206 | byte_buffer_reader_zend_object_handlers.offset = XtOffsetOf(byte_buffer_reader_zend_object, std); 207 | byte_buffer_reader_zend_object_handlers.clone_obj = reader_clone; 208 | byte_buffer_reader_zend_object_handlers.free_obj = reader_free; 209 | byte_buffer_reader_zend_object_handlers.compare = reader_compare_objects; 210 | 211 | return byte_buffer_reader_ce; 212 | } 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ext-encoding 2 | This extension provides high-performance raw data encoding & decoding utilities for PHP. 3 | 4 | It was designed to supersede [`pocketmine/binaryutils`](https://github.com/pmmp/BinaryUtils) and the painfully slow PHP functions [`pack()`](https://www.php.net/manual/en/function.pack.php) and [`unpack()`](https://www.php.net/manual/en/function.unpack.php). 5 | 6 | ## Real-world performance tests 7 | - [`pocketmine/nbt`](https://github.com/pmmp/NBT) was tested with release 0.2.1, and showed 1.5x read and 2x write performance with some basic synthetic tests. 8 | 9 | ## API 10 | A recent IDE stub can usually be found in our [custom stubs repository](https://github.com/pmmp/phpstorm-stubs/blob/fork/encoding/encoding.php). 11 | 12 | > [!NOTE] 13 | > Although `ext-encoding` provides similar functionality to `pocketmine/binaryutils`, it is *not* a drop-in replacement. 14 | > Its API is completely different and incompatible. 15 | 16 | The new API has been designed with the lessons learned from `pocketmine/binaryutils` in mind. Most notably: 17 | - Readers and writers have fully separated APIs - no more accidentally writing while intending to read or vice versa 18 | - Endian-reversible types are implemented in `LE::` and `BE::` static methods, which avoids accidentally using the wrong byte order 19 | - All integer-accepting and returning functions explicitly state whether they work with `Signed` or `Unsigned` integers 20 | 21 | ## FAQs 22 | ### Why are `BinaryStream` and generally `pocketmine/binaryutils` so slow? 23 | - [VarInt encode/decode](#varint-encodedecode) 24 | - [`pack()` and `unpack()`](#pack-and-unpack) 25 | - [Linear buffer reallocations](#linear-buffer-reallocations) 26 | - [Array-of-type](#array-of-type) 27 | 28 | #### VarInt encode/decode 29 | VarInts are heavily used by the Bedrock protocol. This format is borrowed from [protobuf](https://developers.google.com/protocol-buffers/docs/encoding). 30 | 31 | There's no fast way to implement them in pure PHP. They require repeated calls to `chr()` and `ord()` in a loop, as well as needing workarounds for PHP's lack of logical rightshift. 32 | 33 | Compared to `BinaryStream`, this extension's `VarInt::` functions offer a performance improvement of 5-10x (depending on the size of the value and other conditions, YMMV) with both signed and unsigned varints. 34 | 35 | This will significantly improve performance in PocketMine-MP when integrated. For example, chunk encoding will become significantly faster, and encoding & decoding of almost all packets will benefit too. 36 | 37 | #### `pack()` and `unpack()` 38 | PHP's [`pack()`](https://www.php.net/manual/en/function.pack.php) and [`unpack()`](https://www.php.net/manual/en/function.unpack.php) functions are abysmally slow. 39 | Parsing the formatting code argument takes over 90% of the time spent in these functions. 40 | This overhead can be easily avoided when the types of data used are known in advance. 41 | 42 | This extension implements specialized functions for writing big and little endian byte/short/int/long/float/double. 43 | Depending on the type and other factors, these functions typically show a 3-4x performance improvement compared to `BinaryStream`. 44 | 45 | #### Linear buffer reallocations 46 | `BinaryStream` and similar PHP-land byte-buffer implementations often use strings and use the `.=` concatenation operator. 47 | This is problematic, because the entire string will be reallocated every time something is appended to it. 48 | While this isn't a big issue for small buffers, the performance of writing to large buffers progressively degrades. 49 | 50 | `ByteBufferWriter` uses exponential scaling (factor of 2) to minimize buffer reallocations at the cost of potentially wasting some memory. 51 | This means that the internal buffer size is doubled when the buffer runs out of space. 52 | 53 | #### Array-of-type 54 | All the above problems contribute to this one, in addition to: 55 | - Extra function call overhead 56 | - Dealing with PHP `HashTable` structures is generally slow (a problem not solved by this extension currently) 57 | 58 | During testing in the 0.x phase, writing batches of varints in a single function call was found to be over 50 times faster in simple tests than a loop calling `BinaryStream::getVarInt()` when dealing with an array of 10k elements. 59 | The most obvious cases where this will benefit PocketMine-MP are in `LevelChunkPacket` encoding, and plugins using `ClientboundMapItemDataPacket` could also benefit from it. 60 | 61 | However, the extension currently doesn't offer any functions for array-of-type encoding and decoding. This is because there are unresolved questions about how they should work, and I don't want to be locked into a particular API before I figure out what makes the most sense. 62 | 63 | Some things to consider are: 64 | 65 | - Some places that could write batches of something (e.g. `int[]`) need to transform the values before writing. In this case, a `foreach` and repeated `write*()` often ends up being faster than allocating an `array` for the transformed values. 66 | - Some places that could write array-of-type don't actually store their data in PHP `array`s to begin with, but rather in native arrays (e.g. `PalettedBlockArray`). Forcing those cases to create a slow PHP `array`, when we just turn it back into a native array anyway, doesn't make any sense. 67 | - Most places that use PHP `array`s that could be written without transformation are already paying performance and memory penalties for using PHP `array`s in the first place (e.g. `IntArrayTag`). They'd probably benefit from thin wrappers around native fixed arrays, e.g. `IntArray`, `LongArray` etc. 68 | 69 | ### Why are there SO MANY functions? Why not just accept something like `bool $signed, ByteOrder $order` parameters? 70 | 71 | Runtime parameters would mean that these hot encoding paths would need to branch to decide how to encode everything. Branching is slow, so we want to avoid that. 72 | 73 | Internally, we only have a handful of functions (defined in `Serializers.h`), which use C++ templates to inject type, signedness, and byte order arguments. 74 | The compiler expands these templates into optimised branchless native functions for each `(type, signed, byte order)` combination. 75 | 76 | In addition, parsing arguments in PHP is slow, and since PHP doesn't have anything akin to C++ templates (or generics more generally), the only option to get compile-time knowledge of byte order and signedness is to bake them into the function name. There is a function for every combination of `(type, signed, byte order)`. 77 | 78 | The downside of this is that we can't use `.stub.php` files to generate arginfo, so the IDE stubs have to be generated from the extension using [extension-stub-generator](https://github.com/pmmp/extension-stub-generator). 79 | Also, you'll probably need eye bleach after seeing the [macros that generate the function matrix](https://github.com/pmmp/ext-encoding/blob/bfcc8243f1037d37efea53444dc17c11bd2d47df/classes/Types.cpp#L246-L365). 80 | 81 | However, considering how critical binary data handling is to performance in PocketMine-MP, this is a trade absolutely worth making. 82 | 83 | ### Why static methods instead of `ByteBuffer(Reader|Writer)` instance methods? 84 | 85 | Two reasons: 86 | - As described above, the static `read`/`write` methods can't be generated using `.stub.php` files. If we put the generated functions in `ByteBufferReader`/`ByteBufferWriter`, we'd be unable to use a `.stub.php` file to define the rest of its non-generated API. 87 | - I've made too many mistakes with byte order due to IDE auto complete. With this API design, byte order is decided by the very first character you type, so auto complete can't trip you up (and you have to import `BE` or `LE`). 88 | - For the other classes (`VarInt`, `Byte`), they don't really *need* to be static, but it makes more sense for the API to be consistent. 89 | 90 | ### Why fully specify `Signed` or `Unsigned` in every function name? Why not just have e.g. `readInt()` and `readUint()`? 91 | 92 | This library's first users will be people moving from `BinaryStream`, where the API is infamous for being inconsistent about signedness when not specified (https://github.com/pmmp/BinaryUtils/issues/15). For example, `getShort()` is unsigned, and `getInt()` is signed. 93 | 94 | I felt that it was better to be verbose to force developers to think about whether to use a signed or an unsigned type when migrating old code. 95 | -------------------------------------------------------------------------------- /classes/ByteBufferWriter.cpp: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include "php.h" 3 | #include "Zend/zend_exceptions.h" 4 | #include "../stubs/ByteBufferWriter_arginfo.h" 5 | } 6 | 7 | #include "ByteBufferWriter.h" 8 | #include "DataDecodeException.h" 9 | #include "../Serializers.h" 10 | 11 | static zend_object_handlers byte_buffer_writer_zend_object_handlers; 12 | zend_class_entry* byte_buffer_writer_ce; 13 | 14 | static void writer_init_properties(byte_buffer_writer_zend_object* object, unsigned char* buffer, size_t length, size_t offset) { 15 | object->writer.length = length; //we don't need to copy reserved memory 16 | object->writer.offset = offset; 17 | object->writer.used = length; 18 | if (length == 0) { 19 | const unsigned int preallocSize = 16; 20 | object->writer.buffer = reinterpret_cast(emalloc(preallocSize)); 21 | object->writer.length = preallocSize; 22 | } else { 23 | object->writer.buffer = reinterpret_cast(emalloc(length)); 24 | memcpy(object->writer.buffer, buffer, length); 25 | } 26 | } 27 | 28 | 29 | static zend_object* writer_new(zend_class_entry* ce) { 30 | auto object = alloc_custom_zend_object(ce, &byte_buffer_writer_zend_object_handlers); 31 | 32 | writer_init_properties(object, nullptr, 0, 0); 33 | 34 | return &object->std; 35 | } 36 | 37 | static zend_object* writer_clone(zend_object* object) { 38 | auto old_object = fetch_from_zend_object(object); 39 | auto new_object = alloc_custom_zend_object(object->ce, &byte_buffer_writer_zend_object_handlers); 40 | 41 | writer_init_properties(new_object, old_object->writer.buffer, old_object->writer.used, old_object->writer.offset); 42 | 43 | zend_objects_clone_members(&new_object->std, &old_object->std); 44 | 45 | return &new_object->std; 46 | } 47 | 48 | static void writer_free(zend_object* std) { 49 | auto object = fetch_from_zend_object(std); 50 | 51 | efree(object->writer.buffer); 52 | } 53 | 54 | static int writer_compare_objects(zval* obj1, zval* obj2) { 55 | if (Z_TYPE_P(obj1) == IS_OBJECT && Z_TYPE_P(obj2) == IS_OBJECT) { 56 | if (instanceof_function(Z_OBJCE_P(obj1), byte_buffer_writer_ce) && instanceof_function(Z_OBJCE_P(obj2), byte_buffer_writer_ce)) { 57 | auto object1 = fetch_from_zend_object(Z_OBJ_P(obj1)); 58 | auto object2 = fetch_from_zend_object(Z_OBJ_P(obj2)); 59 | 60 | if ( 61 | object1->writer.offset == object2->writer.offset && 62 | object1->writer.used == object2->writer.used && 63 | memcmp(object1->writer.buffer, object2->writer.buffer, object1->writer.used) == 0 64 | ) { 65 | return 0; 66 | } 67 | } 68 | } 69 | 70 | return 1; 71 | } 72 | 73 | #define WRITER_METHOD(name) PHP_METHOD(pmmp_encoding_ByteBufferWriter, name) 74 | 75 | WRITER_METHOD(__construct) { 76 | zend_string* buffer = NULL; 77 | byte_buffer_writer_zend_object* object; 78 | 79 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1) 80 | Z_PARAM_OPTIONAL 81 | Z_PARAM_STR(buffer) 82 | ZEND_PARSE_PARAMETERS_END(); 83 | 84 | object = WRITER_THIS(); 85 | if (object->writer.buffer) { 86 | efree(object->writer.buffer); 87 | } 88 | 89 | if (buffer == NULL) { 90 | buffer = zend_empty_string; 91 | } 92 | 93 | //write offset is placed at the end, as if the given string was written using writeByteArray() 94 | writer_init_properties(object, reinterpret_cast(ZSTR_VAL(buffer)), ZSTR_LEN(buffer), ZSTR_LEN(buffer)); 95 | } 96 | 97 | WRITER_METHOD(getData) { 98 | zend_parse_parameters_none_throw(); 99 | 100 | auto object = WRITER_THIS(); 101 | RETURN_STRINGL(reinterpret_cast(object->writer.buffer), object->writer.used); 102 | } 103 | 104 | WRITER_METHOD(writeByteArray) { 105 | zend_string* value; 106 | byte_buffer_writer_zend_object* object; 107 | 108 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 109 | Z_PARAM_STR(value) 110 | ZEND_PARSE_PARAMETERS_END(); 111 | 112 | 113 | object = WRITER_THIS(); 114 | 115 | auto size = ZSTR_LEN(value); 116 | 117 | extendBuffer(object->writer.buffer, object->writer.length, object->writer.offset, size); 118 | memcpy(&object->writer.buffer[object->writer.offset], ZSTR_VAL(value), size); 119 | object->writer.offset += size; 120 | if (object->writer.offset > object->writer.used) { 121 | object->writer.used = object->writer.offset; 122 | } 123 | } 124 | 125 | WRITER_METHOD(getOffset) { 126 | zend_parse_parameters_none_throw(); 127 | auto object = WRITER_THIS(); 128 | RETURN_LONG(object->writer.offset); 129 | } 130 | 131 | WRITER_METHOD(setOffset) { 132 | zend_long offset; 133 | 134 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 135 | Z_PARAM_LONG(offset) 136 | ZEND_PARSE_PARAMETERS_END(); 137 | 138 | auto object = WRITER_THIS(); 139 | if (offset < 0 || static_cast(offset) > object->writer.used) { 140 | zend_value_error("Offset must not be less than zero or greater than the buffer size"); 141 | return; 142 | } 143 | 144 | object->writer.offset = static_cast(offset); 145 | } 146 | 147 | WRITER_METHOD(getUsedLength) { 148 | zend_parse_parameters_none_throw(); 149 | 150 | auto object = WRITER_THIS(); 151 | RETURN_LONG(object->writer.used); 152 | } 153 | 154 | WRITER_METHOD(getReservedLength) { 155 | zend_parse_parameters_none_throw(); 156 | 157 | auto object = WRITER_THIS(); 158 | RETURN_LONG(object->writer.length); //don't count null terminator 159 | } 160 | 161 | WRITER_METHOD(reserve) { 162 | zend_long zlength; 163 | 164 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 165 | Z_PARAM_LONG(zlength) 166 | ZEND_PARSE_PARAMETERS_END(); 167 | 168 | if (zlength <= 0) { 169 | zend_value_error("Length must be greater than zero"); 170 | return; 171 | } 172 | auto object = WRITER_THIS(); 173 | extendBuffer(object->writer.buffer, object->writer.length, static_cast(zlength), 0); 174 | } 175 | 176 | WRITER_METHOD(trim) { 177 | zend_parse_parameters_none_throw(); 178 | 179 | auto object = WRITER_THIS(); 180 | if (object->writer.length > object->writer.used) { 181 | object->writer.buffer = reinterpret_cast(erealloc(object->writer.buffer, object->writer.used)); 182 | object->writer.length = object->writer.used; 183 | } 184 | } 185 | 186 | WRITER_METHOD(clear) { 187 | zend_parse_parameters_none_throw(); 188 | 189 | auto object = WRITER_THIS(); 190 | object->writer.offset = 0; 191 | object->writer.used = 0; 192 | } 193 | 194 | WRITER_METHOD(__serialize) { 195 | zend_parse_parameters_none_throw(); 196 | 197 | auto object = WRITER_THIS(); 198 | array_init(return_value); 199 | 200 | //don't return the writer buffer directly - it may have uninitialized reserved memory 201 | add_assoc_stringl(return_value, "buffer", reinterpret_cast(object->writer.buffer), object->writer.used); 202 | add_assoc_long(return_value, "offset", object->writer.offset); 203 | } 204 | 205 | static zval* fetch_serialized_property(HashTable* data, const char* name, int type) { 206 | zval* zv = zend_hash_str_find(data, name, strlen(name)); 207 | if (zv == NULL) { 208 | zend_throw_exception_ex(NULL, 0, "Serialized data is missing \"%s\"", name); 209 | return NULL; 210 | } 211 | if (Z_TYPE_P(zv) != type) { 212 | zend_throw_exception_ex(NULL, 0, "\"%s\" in serialized data should be of type %s, but have %s", name, zend_zval_type_name(zv), zend_get_type_by_const(type)); 213 | return NULL; 214 | } 215 | 216 | return zv; 217 | } 218 | 219 | WRITER_METHOD(__unserialize) { 220 | HashTable* data; 221 | 222 | ZEND_PARSE_PARAMETERS_START(1, 1) 223 | Z_PARAM_ARRAY_HT(data) 224 | ZEND_PARSE_PARAMETERS_END(); 225 | 226 | zval* buffer = fetch_serialized_property(data, "buffer", IS_STRING); 227 | if (buffer == NULL) { 228 | return; 229 | } 230 | zval* offset = fetch_serialized_property(data, "offset", IS_LONG); 231 | if (offset == NULL) { 232 | return; 233 | } 234 | 235 | auto object = WRITER_THIS(); 236 | //would be nice to prevent this from being allocated, but I suppose it's also possible someone could call __unserialize() directly 237 | efree(object->writer.buffer); 238 | 239 | writer_init_properties(object, reinterpret_cast(Z_STRVAL_P(buffer)), Z_STRLEN_P(buffer), Z_LVAL_P(offset)); 240 | } 241 | 242 | WRITER_METHOD(__debugInfo) { 243 | zend_parse_parameters_none_throw(); 244 | 245 | auto object = WRITER_THIS(); 246 | array_init(return_value); 247 | 248 | //don't return the writer buffer directly - it may have uninitialized reserved memory 249 | add_assoc_stringl(return_value, "buffer", reinterpret_cast(object->writer.buffer), object->writer.used); 250 | add_assoc_long(return_value, "offset", object->writer.offset); 251 | } 252 | 253 | zend_class_entry* init_class_ByteBufferWriter(void) { 254 | byte_buffer_writer_ce = register_class_pmmp_encoding_ByteBufferWriter(); 255 | byte_buffer_writer_ce->create_object = writer_new; 256 | 257 | byte_buffer_writer_zend_object_handlers = *zend_get_std_object_handlers(); 258 | byte_buffer_writer_zend_object_handlers.offset = XtOffsetOf(byte_buffer_writer_zend_object, std); 259 | byte_buffer_writer_zend_object_handlers.clone_obj = writer_clone; 260 | byte_buffer_writer_zend_object_handlers.free_obj = writer_free; 261 | byte_buffer_writer_zend_object_handlers.compare = writer_compare_objects; 262 | 263 | return byte_buffer_writer_ce; 264 | } 265 | -------------------------------------------------------------------------------- /stubs/ByteBufferWriter_arginfo.h: -------------------------------------------------------------------------------- 1 | /* This is a generated file, edit the .stub.php file instead. 2 | * Stub hash: 7b065b4b7170dbe7ecb0a00efd18a8c72dba4815 */ 3 | 4 | ZEND_BEGIN_ARG_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter___construct, 0, 0, 0) 5 | ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, prefix, IS_STRING, 0, "\"\"") 6 | ZEND_END_ARG_INFO() 7 | 8 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter_getData, 0, 0, IS_STRING, 0) 9 | ZEND_END_ARG_INFO() 10 | 11 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter_writeByteArray, 0, 1, IS_VOID, 0) 12 | ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) 13 | ZEND_END_ARG_INFO() 14 | 15 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter_getOffset, 0, 0, IS_LONG, 0) 16 | ZEND_END_ARG_INFO() 17 | 18 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter_setOffset, 0, 1, IS_VOID, 0) 19 | ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) 20 | ZEND_END_ARG_INFO() 21 | 22 | #define arginfo_class_pmmp_encoding_ByteBufferWriter_getUsedLength arginfo_class_pmmp_encoding_ByteBufferWriter_getOffset 23 | 24 | #define arginfo_class_pmmp_encoding_ByteBufferWriter_getReservedLength arginfo_class_pmmp_encoding_ByteBufferWriter_getOffset 25 | 26 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter_reserve, 0, 1, IS_VOID, 0) 27 | ZEND_ARG_TYPE_INFO(0, length, IS_LONG, 0) 28 | ZEND_END_ARG_INFO() 29 | 30 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter_trim, 0, 0, IS_VOID, 0) 31 | ZEND_END_ARG_INFO() 32 | 33 | #define arginfo_class_pmmp_encoding_ByteBufferWriter_clear arginfo_class_pmmp_encoding_ByteBufferWriter_trim 34 | 35 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter___serialize, 0, 0, IS_ARRAY, 0) 36 | ZEND_END_ARG_INFO() 37 | 38 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_encoding_ByteBufferWriter___unserialize, 0, 1, IS_VOID, 0) 39 | ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) 40 | ZEND_END_ARG_INFO() 41 | 42 | #define arginfo_class_pmmp_encoding_ByteBufferWriter___debugInfo arginfo_class_pmmp_encoding_ByteBufferWriter___serialize 43 | 44 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, __construct); 45 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, getData); 46 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, writeByteArray); 47 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, getOffset); 48 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, setOffset); 49 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, getUsedLength); 50 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, getReservedLength); 51 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, reserve); 52 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, trim); 53 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, clear); 54 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, __serialize); 55 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, __unserialize); 56 | ZEND_METHOD(pmmp_encoding_ByteBufferWriter, __debugInfo); 57 | 58 | static const zend_function_entry class_pmmp_encoding_ByteBufferWriter_methods[] = { 59 | #if (PHP_VERSION_ID >= 80400) 60 | ZEND_RAW_FENTRY("__construct", zim_pmmp_encoding_ByteBufferWriter___construct, arginfo_class_pmmp_encoding_ByteBufferWriter___construct, ZEND_ACC_PUBLIC, NULL, "/**\n * Constructs a new ByteBufferWriter.\n * The provided string will be written at the start of the buffer as if readByteArray() was called.\n */") 61 | #else 62 | ZEND_RAW_FENTRY("__construct", zim_pmmp_encoding_ByteBufferWriter___construct, arginfo_class_pmmp_encoding_ByteBufferWriter___construct, ZEND_ACC_PUBLIC) 63 | #endif 64 | #if (PHP_VERSION_ID >= 80400) 65 | ZEND_RAW_FENTRY("getData", zim_pmmp_encoding_ByteBufferWriter_getData, arginfo_class_pmmp_encoding_ByteBufferWriter_getData, ZEND_ACC_PUBLIC, NULL, "/**\n * Returns a string containing the written bytes.\n * Reserved memory is not included.\n */") 66 | #else 67 | ZEND_RAW_FENTRY("getData", zim_pmmp_encoding_ByteBufferWriter_getData, arginfo_class_pmmp_encoding_ByteBufferWriter_getData, ZEND_ACC_PUBLIC) 68 | #endif 69 | #if (PHP_VERSION_ID >= 80400) 70 | ZEND_RAW_FENTRY("writeByteArray", zim_pmmp_encoding_ByteBufferWriter_writeByteArray, arginfo_class_pmmp_encoding_ByteBufferWriter_writeByteArray, ZEND_ACC_PUBLIC, NULL, "/**\n * Writes the given bytes to the buffer at the current offset.\n * The internal offset will be updated by this operation.\n *\n * If the current buffer size is not big enough to add the given\n * bytes, the buffer will be resized to either 2x its current size,\n * or the actual size of the result, whichever is larger. This\n * ensures the lowest number of reallocations.\n */") 71 | #else 72 | ZEND_RAW_FENTRY("writeByteArray", zim_pmmp_encoding_ByteBufferWriter_writeByteArray, arginfo_class_pmmp_encoding_ByteBufferWriter_writeByteArray, ZEND_ACC_PUBLIC) 73 | #endif 74 | #if (PHP_VERSION_ID >= 80400) 75 | ZEND_RAW_FENTRY("getOffset", zim_pmmp_encoding_ByteBufferWriter_getOffset, arginfo_class_pmmp_encoding_ByteBufferWriter_getOffset, ZEND_ACC_PUBLIC, NULL, "/**\n * Returns the current internal write offset (the position\n * from which the next write operation will start).\n */") 76 | #else 77 | ZEND_RAW_FENTRY("getOffset", zim_pmmp_encoding_ByteBufferWriter_getOffset, arginfo_class_pmmp_encoding_ByteBufferWriter_getOffset, ZEND_ACC_PUBLIC) 78 | #endif 79 | #if (PHP_VERSION_ID >= 80400) 80 | ZEND_RAW_FENTRY("setOffset", zim_pmmp_encoding_ByteBufferWriter_setOffset, arginfo_class_pmmp_encoding_ByteBufferWriter_setOffset, ZEND_ACC_PUBLIC, NULL, "/**\n * Sets the internal write offset to the given value.\n * The offset must be within the bounds of the buffer\n * (0 <= offset <= reserved length).\n *\n * @throws \\ValueError if the offset is out of bounds\n */") 81 | #else 82 | ZEND_RAW_FENTRY("setOffset", zim_pmmp_encoding_ByteBufferWriter_setOffset, arginfo_class_pmmp_encoding_ByteBufferWriter_setOffset, ZEND_ACC_PUBLIC) 83 | #endif 84 | #if (PHP_VERSION_ID >= 80400) 85 | ZEND_RAW_FENTRY("getUsedLength", zim_pmmp_encoding_ByteBufferWriter_getUsedLength, arginfo_class_pmmp_encoding_ByteBufferWriter_getUsedLength, ZEND_ACC_PUBLIC, NULL, "/**\n * Returns the total number of bytes written.\n * This will always be less than or equal to the reserved length.\n */") 86 | #else 87 | ZEND_RAW_FENTRY("getUsedLength", zim_pmmp_encoding_ByteBufferWriter_getUsedLength, arginfo_class_pmmp_encoding_ByteBufferWriter_getUsedLength, ZEND_ACC_PUBLIC) 88 | #endif 89 | #if (PHP_VERSION_ID >= 80400) 90 | ZEND_RAW_FENTRY("getReservedLength", zim_pmmp_encoding_ByteBufferWriter_getReservedLength, arginfo_class_pmmp_encoding_ByteBufferWriter_getReservedLength, ZEND_ACC_PUBLIC, NULL, "/**\n * Returns the number of bytes reserved by the ByteBuffer.\n * This value may be larger than the number of written bytes, as\n * some memory may be preallocated to avoid reallocations.\n */") 91 | #else 92 | ZEND_RAW_FENTRY("getReservedLength", zim_pmmp_encoding_ByteBufferWriter_getReservedLength, arginfo_class_pmmp_encoding_ByteBufferWriter_getReservedLength, ZEND_ACC_PUBLIC) 93 | #endif 94 | #if (PHP_VERSION_ID >= 80400) 95 | ZEND_RAW_FENTRY("reserve", zim_pmmp_encoding_ByteBufferWriter_reserve, arginfo_class_pmmp_encoding_ByteBufferWriter_reserve, ZEND_ACC_PUBLIC, NULL, "/**\n * Increases buffer capacity to the given value, if the capacity\n * is less than this amount. Useful to avoid extra reallocations\n * during large write operations when the needed capacity of the\n * buffer is known in advance.\n */") 96 | #else 97 | ZEND_RAW_FENTRY("reserve", zim_pmmp_encoding_ByteBufferWriter_reserve, arginfo_class_pmmp_encoding_ByteBufferWriter_reserve, ZEND_ACC_PUBLIC) 98 | #endif 99 | #if (PHP_VERSION_ID >= 80400) 100 | ZEND_RAW_FENTRY("trim", zim_pmmp_encoding_ByteBufferWriter_trim, arginfo_class_pmmp_encoding_ByteBufferWriter_trim, ZEND_ACC_PUBLIC, NULL, "/**\n * Truncates the internal buffer to only the written part,\n * discarding any unused reserved memory.\n */") 101 | #else 102 | ZEND_RAW_FENTRY("trim", zim_pmmp_encoding_ByteBufferWriter_trim, arginfo_class_pmmp_encoding_ByteBufferWriter_trim, ZEND_ACC_PUBLIC) 103 | #endif 104 | #if (PHP_VERSION_ID >= 80400) 105 | ZEND_RAW_FENTRY("clear", zim_pmmp_encoding_ByteBufferWriter_clear, arginfo_class_pmmp_encoding_ByteBufferWriter_clear, ZEND_ACC_PUBLIC, NULL, "/**\n * Clears all data from the buffer. The memory used is retained\n * as reserved memory.\n */") 106 | #else 107 | ZEND_RAW_FENTRY("clear", zim_pmmp_encoding_ByteBufferWriter_clear, arginfo_class_pmmp_encoding_ByteBufferWriter_clear, ZEND_ACC_PUBLIC) 108 | #endif 109 | ZEND_ME(pmmp_encoding_ByteBufferWriter, __serialize, arginfo_class_pmmp_encoding_ByteBufferWriter___serialize, ZEND_ACC_PUBLIC) 110 | ZEND_ME(pmmp_encoding_ByteBufferWriter, __unserialize, arginfo_class_pmmp_encoding_ByteBufferWriter___unserialize, ZEND_ACC_PUBLIC) 111 | ZEND_ME(pmmp_encoding_ByteBufferWriter, __debugInfo, arginfo_class_pmmp_encoding_ByteBufferWriter___debugInfo, ZEND_ACC_PUBLIC) 112 | ZEND_FE_END 113 | }; 114 | 115 | static zend_class_entry *register_class_pmmp_encoding_ByteBufferWriter(void) 116 | { 117 | zend_class_entry ce, *class_entry; 118 | 119 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "ByteBufferWriter", class_pmmp_encoding_ByteBufferWriter_methods); 120 | #if (PHP_VERSION_ID >= 80400) 121 | class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); 122 | #else 123 | class_entry = zend_register_internal_class_ex(&ce, NULL); 124 | class_entry->ce_flags |= ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES; 125 | #endif 126 | 127 | return class_entry; 128 | } 129 | -------------------------------------------------------------------------------- /Serializers.h: -------------------------------------------------------------------------------- 1 | #ifndef SERIALIZERS_H 2 | #define SERIALIZERS_H 3 | 4 | extern "C" { 5 | #include "Zend/zend_exceptions.h" 6 | } 7 | #include "classes/DataDecodeException.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | enum class ByteOrder { 14 | BigEndian, 15 | LittleEndian, 16 | #ifdef WORDS_BIGENDIAN 17 | Native = BigEndian 18 | #else 19 | Native = LittleEndian 20 | #endif 21 | }; 22 | 23 | template 24 | union Flipper { 25 | char bytes[sizeof(TValue)]; 26 | TValue value; 27 | }; 28 | 29 | template 30 | static inline bool readByte(unsigned char* buffer, size_t used, size_t& offset, TValue& result) { 31 | const auto SIZE = sizeof(TValue); 32 | if (used - offset < SIZE) { 33 | zend_throw_exception_ex(data_decode_exception_ce, 0, "Need at least %zu bytes, but only have %zu bytes", SIZE, used - offset); 34 | return false; 35 | } 36 | 37 | result = *(reinterpret_cast(&buffer[offset])); 38 | 39 | offset += SIZE; 40 | return true; 41 | } 42 | template 43 | static inline bool readFixedSizeType(unsigned char* bytes, size_t used, size_t& offset, TValue& result) { 44 | 45 | const auto SIZE = sizeof(TValue); 46 | if (used - offset < SIZE) { 47 | zend_throw_exception_ex(data_decode_exception_ce, 0, "Need at least %zu bytes, but only have %zu bytes", SIZE, used - offset); 48 | return false; 49 | } 50 | 51 | Flipper flipper; 52 | 53 | memcpy(flipper.bytes, &bytes[offset], sizeof(flipper.bytes)); 54 | if (byteOrder != ByteOrder::Native) { 55 | std::reverse(std::begin(flipper.bytes), std::end(flipper.bytes)); 56 | } 57 | 58 | result = flipper.value; 59 | 60 | offset += SIZE; 61 | return true; 62 | } 63 | 64 | template 65 | static inline bool readFixedSizeTypeArray(unsigned char* bytes, size_t used, size_t& offset, size_t count, std::vector& resultArray) { 66 | size_t sizeBytes = count * sizeof(TValue); 67 | 68 | if (used - offset < sizeBytes) { 69 | zend_throw_exception_ex(data_decode_exception_ce, 0, "Need at least %zu bytes, but only have %zu bytes", sizeBytes, used - offset); 70 | return false; 71 | } 72 | 73 | TValue* rawValues = reinterpret_cast(&bytes[offset]); 74 | 75 | resultArray.assign(rawValues, rawValues + count); 76 | if (byteOrder != ByteOrder::Native) { 77 | Flipper flipper; 78 | 79 | for (size_t i = 0; i < count; i++) { 80 | flipper.value = resultArray[i]; 81 | std::reverse(std::begin(flipper.bytes), std::end(flipper.bytes)); 82 | resultArray[i] = flipper.value; 83 | } 84 | } 85 | 86 | offset += sizeBytes; 87 | return true; 88 | } 89 | 90 | template 91 | using readComplexTypeFunc_t = bool (*) (unsigned char* bytes, size_t used, size_t& offset, TValue& result); 92 | 93 | template readComplexTypeFunc> 94 | static inline bool readComplexTypeArray(unsigned char* bytes, size_t used, size_t& offset, size_t count, std::vector& resultArray) { 95 | for (size_t i = 0; i < count; i++) { 96 | TValue value; 97 | 98 | if (!readComplexTypeFunc(bytes, used, offset, value)) { 99 | return false; 100 | } 101 | 102 | resultArray.push_back(value); 103 | } 104 | 105 | return true; 106 | } 107 | 108 | template 109 | static inline bool readInt24(unsigned char* bytes, size_t used, size_t& offset, TValue& result) { 110 | const size_t SIZE = 3; 111 | 112 | if (used - offset < SIZE) { 113 | zend_throw_exception_ex(data_decode_exception_ce, 0, "Need at least %zu bytes, but only have %zu bytes", SIZE, used - offset); 114 | return false; 115 | } 116 | 117 | result = 0; 118 | if (byteOrder == ByteOrder::LittleEndian) { 119 | result |= bytes[offset]; 120 | result |= bytes[offset + 1] << 8; 121 | result |= bytes[offset + 2] << 16; 122 | } 123 | else { 124 | result |= bytes[offset + 2]; 125 | result |= bytes[offset + 1] << 8; 126 | result |= bytes[offset] << 16; 127 | } 128 | 129 | const size_t SIGNED_SHIFT = std::is_signed::value ? (sizeof(TValue) - SIZE) * CHAR_BIT : 0; 130 | if (SIGNED_SHIFT > 0) { 131 | result = (result << SIGNED_SHIFT) >> SIGNED_SHIFT; 132 | } 133 | 134 | offset += SIZE; 135 | return true; 136 | } 137 | 138 | struct VarIntConstants { 139 | static const unsigned char BITS_PER_BYTE = 7u; 140 | 141 | template 142 | static const unsigned char MAX_BYTES = TYPE_BITS / BITS_PER_BYTE + ((TYPE_BITS % BITS_PER_BYTE) > 0); 143 | 144 | static const unsigned char VALUE_MASK = static_cast(~(1u << BITS_PER_BYTE)); 145 | static const unsigned char MSB_MASK = static_cast(1u << BITS_PER_BYTE); 146 | }; 147 | 148 | template 149 | static inline bool readUnsignedVarInt(unsigned char* bytes, size_t used, size_t& offset, TValue& result) { 150 | const auto TYPE_BITS = sizeof(TValue) * CHAR_BIT; 151 | result = 0; 152 | for (unsigned int shift = 0; shift < TYPE_BITS; shift += VarIntConstants::BITS_PER_BYTE) { 153 | if (offset >= used) { 154 | zend_throw_exception(data_decode_exception_ce, "No bytes left in buffer", 0); 155 | return false; 156 | } 157 | 158 | auto byte = bytes[offset++]; 159 | result |= static_cast(byte & VarIntConstants::VALUE_MASK) << shift; 160 | if ((byte & VarIntConstants::MSB_MASK) == 0) { 161 | return true; 162 | } 163 | } 164 | 165 | zend_throw_exception_ex(data_decode_exception_ce, 0, "VarInt did not terminate after %u bytes!", VarIntConstants::MAX_BYTES); 166 | return false; 167 | } 168 | 169 | template 170 | static inline bool readSignedVarInt(unsigned char* bytes, size_t used, size_t& offset, TSignedValue& result) { 171 | TUnsignedValue unsignedResult; 172 | if (!readUnsignedVarInt(bytes, used, offset, unsignedResult)) { 173 | return false; 174 | } 175 | 176 | TUnsignedValue mask = 0; 177 | if (unsignedResult & 1) { 178 | //we don't know the type of TUnsignedValue here so we can't just use ~0 179 | mask = ~mask; 180 | } 181 | 182 | result = static_cast((unsignedResult >> 1) ^ mask); 183 | return true; 184 | } 185 | 186 | static inline void extendBuffer(unsigned char*& buffer, size_t& length, size_t offset, size_t usedBytes) { 187 | size_t requiredSize = offset + usedBytes; 188 | if (length < requiredSize) { 189 | size_t doubleSize = length * 2; 190 | length = doubleSize > requiredSize ? doubleSize : requiredSize; 191 | buffer = reinterpret_cast(erealloc(buffer, length)); 192 | } 193 | } 194 | 195 | template 196 | static void writeByte(unsigned char*& buffer, size_t& length, size_t& offset, TValue value) { 197 | extendBuffer(buffer, length, offset, sizeof(TValue)); 198 | 199 | buffer[offset] = *reinterpret_cast(&value); 200 | 201 | offset += sizeof(TValue); 202 | } 203 | 204 | template 205 | static void writeFixedSizeType(unsigned char*& buffer, size_t& length, size_t& offset, TValue value) { 206 | extendBuffer(buffer, length, offset, sizeof(TValue)); 207 | 208 | Flipper flipper; 209 | flipper.value = value; 210 | 211 | if (byteOrder != ByteOrder::Native) { 212 | std::reverse(std::begin(flipper.bytes), std::end(flipper.bytes)); 213 | } 214 | 215 | memcpy(&buffer[offset], flipper.bytes, sizeof(flipper.bytes)); 216 | 217 | offset += sizeof(TValue); 218 | } 219 | 220 | template 221 | static void writeFixedSizeTypeArray(unsigned char*& buffer, size_t& length, size_t& offset, std::vector& valueArray) { 222 | size_t arraySizeBytes = valueArray.size() * sizeof(TValue); 223 | extendBuffer(buffer, length, offset, arraySizeBytes); 224 | 225 | if (byteOrder != ByteOrder::Native) { 226 | Flipper flipper; 227 | 228 | for (size_t i = 0; i < valueArray.size(); i++) { 229 | flipper.value = valueArray[i]; 230 | std::reverse(std::begin(flipper.bytes), std::end(flipper.bytes)); 231 | valueArray[i] = flipper.value; 232 | } 233 | } 234 | 235 | memcpy(&buffer[offset], reinterpret_cast(valueArray.data()), arraySizeBytes); 236 | offset += arraySizeBytes; 237 | } 238 | 239 | template 240 | using writeComplexTypeFunc_t = void (*) (unsigned char*& buffer, size_t& length, size_t& offset, TValue value); 241 | 242 | template writeNaiveTypeFunc> 243 | static void writeComplexTypeArray(unsigned char*& buffer, size_t& length, size_t& offset, std::vector& valueArray) { 244 | for (size_t i = 0; i < valueArray.size(); i++) { 245 | writeNaiveTypeFunc(buffer, length, offset, valueArray[i]); 246 | } 247 | } 248 | 249 | template 250 | static void writeInt24(unsigned char*& buffer, size_t& length, size_t& offset, TValue value) { 251 | const size_t SIZE = 3; 252 | extendBuffer(buffer, length, offset, SIZE); 253 | 254 | if (byteOrder == ByteOrder::LittleEndian) { 255 | buffer[offset] = value & 0xff; 256 | buffer[offset + 1] = (value >> 8) & 0xff; 257 | buffer[offset + 2] = (value >> 16) & 0xff; 258 | } 259 | else { 260 | buffer[offset] = (value >> 16) & 0xff; 261 | buffer[offset + 1] = (value >> 8) & 0xff; 262 | buffer[offset + 2] = value & 0xff; 263 | } 264 | 265 | offset += SIZE; 266 | } 267 | 268 | template 269 | static inline void writeUnsignedVarInt(unsigned char*& buffer, size_t& length, size_t& offset, TValue value) { 270 | static_assert(std::is_unsigned::value, "TValue must be an unsigned type"); 271 | 272 | const auto TYPE_BITS = sizeof(TValue) * CHAR_BIT; 273 | 274 | extendBuffer(buffer, length, offset, VarIntConstants::MAX_BYTES); 275 | 276 | TValue remaining = value; 277 | 278 | for (auto i = 0; i < VarIntConstants::MAX_BYTES; i++) { 279 | unsigned char nextByte = remaining & VarIntConstants::VALUE_MASK; 280 | 281 | TValue nextRemaining = remaining >> VarIntConstants::BITS_PER_BYTE; 282 | 283 | if (nextRemaining == 0) { 284 | buffer[i + offset] = nextByte; 285 | 286 | auto usedBytes = i + 1; 287 | offset += usedBytes; 288 | 289 | return; 290 | } 291 | 292 | buffer[i + offset] = nextByte | VarIntConstants::MSB_MASK; 293 | remaining = nextRemaining; 294 | } 295 | } 296 | 297 | template 298 | static inline void writeSignedVarInt(unsigned char*& buffer, size_t& length, size_t& offset, TSignedType value) { 299 | TUnsignedType mask = 0; 300 | if (value < 0) { 301 | //we don't know the type of TUnsignedType here, can't use ~0 directly (the compiler will optimise this anyway) 302 | mask = ~mask; 303 | } 304 | 305 | writeUnsignedVarInt(buffer, length, offset, (static_cast(value) << 1) ^ mask); 306 | } 307 | 308 | #endif 309 | -------------------------------------------------------------------------------- /classes/Types.cpp: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include "php.h" 3 | #include "Zend/zend_errors.h" 4 | } 5 | #include "ByteBufferReader.h" 6 | #include "ByteBufferWriter.h" 7 | #include "DataDecodeException.h" 8 | #include "../Serializers.h" 9 | #include 10 | #include 11 | 12 | #define ARG_INFOS(name, type_code) \ 13 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_read_##name, 0, 1, type_code, 0) \ 14 | ZEND_ARG_OBJ_INFO(0, buffer, pmmp\\encoding\\ByteBufferReader, 0) \ 15 | ZEND_END_ARG_INFO() \ 16 | \ 17 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_unpack_##name, 0, 1, type_code, 0) \ 18 | ZEND_ARG_TYPE_INFO(0, bytes, IS_STRING, 0) \ 19 | ZEND_END_ARG_INFO() \ 20 | \ 21 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_write_##name, 0, 2, IS_VOID, 0) \ 22 | ZEND_ARG_OBJ_INFO(0, buffer, pmmp\\encoding\\ByteBufferWriter, 0) \ 23 | ZEND_ARG_TYPE_INFO(0, value, type_code, 0) \ 24 | ZEND_END_ARG_INFO() \ 25 | \ 26 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pack_##name, 0, 1, IS_STRING, 0) \ 27 | ZEND_ARG_TYPE_INFO(0, value, type_code, 0) \ 28 | ZEND_END_ARG_INFO() 29 | 30 | ARG_INFOS(zend_long, IS_LONG) 31 | ARG_INFOS(double, IS_DOUBLE) 32 | 33 | #if PHP_VERSION_ID >= 80400 34 | static const char* read_generic_doc_comment = "/** @throws DataDecodeException */"; 35 | #endif 36 | 37 | template 38 | static inline void assignZval(zval* zv, TValue value, std::type_identity zendType) = delete; 39 | 40 | template 41 | static inline void assignZval(zval* zv, TValue value, std::type_identity zendType) { 42 | //TODO: shouldn't this cast the type before returning? Probably implicit casting going on here 43 | ZVAL_LONG(zv, value); 44 | } 45 | 46 | template 47 | static inline void assignZval(zval* zv, TValue value, std::type_identity zendType) { 48 | ZVAL_DOUBLE(zv, value); 49 | } 50 | 51 | template 52 | using readTypeFunc_t = bool (*)(unsigned char* bytes, size_t used, size_t& offset, TValue& result); 53 | 54 | template readTypeFunc, typename TZendValue> 55 | inline void readTypeCommon(INTERNAL_FUNCTION_PARAMETERS, byte_buffer_reader_t& reader) { 56 | TValue result; 57 | auto bytes = reinterpret_cast(ZSTR_VAL(reader.buffer)); 58 | if (readTypeFunc(bytes, ZSTR_LEN(reader.buffer), reader.offset, result)) { 59 | assignZval(return_value, result, std::type_identity{}); 60 | } 61 | } 62 | 63 | template readTypeFunc, typename TZendValue> 64 | void ZEND_FASTCALL zif_readType(INTERNAL_FUNCTION_PARAMETERS) { 65 | zval* object_zv; 66 | byte_buffer_reader_zend_object* object; 67 | 68 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 69 | Z_PARAM_OBJECT_OF_CLASS_EX(object_zv, byte_buffer_reader_ce, 0, 0) 70 | ZEND_PARSE_PARAMETERS_END_EX(return); 71 | 72 | object = READER_FROM_ZVAL(object_zv); 73 | 74 | readTypeCommon(INTERNAL_FUNCTION_PARAM_PASSTHRU, object->reader); 75 | } 76 | 77 | template readTypeFunc, typename TZendValue> 78 | void ZEND_FASTCALL zif_unpackType(INTERNAL_FUNCTION_PARAMETERS) { 79 | byte_buffer_reader_t reader = { 0 }; 80 | 81 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 82 | Z_PARAM_STR(reader.buffer) 83 | ZEND_PARSE_PARAMETERS_END_EX(return); 84 | 85 | //TODO: we could allow passing the offset? but keep it simple for now 86 | reader.offset = 0; 87 | 88 | readTypeCommon(INTERNAL_FUNCTION_PARAM_PASSTHRU, reader); 89 | } 90 | 91 | //this must be reimplemented for any new zend types handled, because ZPP macros can't be easily templated 92 | template 93 | bool parseWriteTypeParams(zend_execute_data* execute_data, byte_buffer_writer_zend_object*& object, TValue& value, std::type_identity zend_type) = delete; 94 | 95 | template 96 | bool parseWriteTypeParams(zend_execute_data* execute_data, byte_buffer_writer_zend_object*& object, TValue& value, std::type_identity zend_type) { 97 | zval* object_zv; 98 | zend_long actualValue; 99 | 100 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2) 101 | Z_PARAM_OBJECT_OF_CLASS_EX(object_zv, byte_buffer_writer_ce, 0, 0) 102 | Z_PARAM_LONG(actualValue) 103 | ZEND_PARSE_PARAMETERS_END_EX(return false); 104 | 105 | object = WRITER_FROM_ZVAL(object_zv); 106 | value = static_cast(actualValue); 107 | 108 | return true; 109 | } 110 | 111 | template 112 | bool parseWriteTypeParams(zend_execute_data* execute_data, byte_buffer_writer_zend_object*& object, TValue& value, std::type_identity zend_type) { 113 | zval* object_zv; 114 | double actualValue; 115 | 116 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2) 117 | Z_PARAM_OBJECT_OF_CLASS_EX(object_zv, byte_buffer_writer_ce, 0, 0) 118 | Z_PARAM_DOUBLE(actualValue) 119 | ZEND_PARSE_PARAMETERS_END_EX(return false); 120 | 121 | object = WRITER_FROM_ZVAL(object_zv); 122 | value = static_cast(actualValue); 123 | 124 | return true; 125 | } 126 | 127 | template 128 | using writeTypeFunc_t = void (*)(unsigned char*& buffer, size_t& length, size_t& offset, TValue value); 129 | 130 | template writeTypeFunc> 131 | inline void writeTypeCommon(INTERNAL_FUNCTION_PARAMETERS, byte_buffer_writer_t& writer, TValue value) { 132 | writeTypeFunc(writer.buffer, writer.length, writer.offset, value); 133 | if (writer.offset > writer.used) { 134 | writer.used = writer.offset; 135 | } 136 | } 137 | 138 | template writeTypeFunc, typename TZendValue> 139 | void ZEND_FASTCALL zif_writeType(INTERNAL_FUNCTION_PARAMETERS) { 140 | TValue value; 141 | byte_buffer_writer_zend_object* object; 142 | 143 | if (!parseWriteTypeParams(execute_data, object, value, std::type_identity{})) { 144 | return; 145 | } 146 | 147 | writeTypeCommon(INTERNAL_FUNCTION_PARAM_PASSTHRU, object->writer, value); 148 | } 149 | 150 | template 151 | bool parsePackTypeParams(zend_execute_data* execute_data, TValue& value, std::type_identity zend_type) = delete; 152 | 153 | template 154 | bool parsePackTypeParams(zend_execute_data* execute_data, TValue& value, std::type_identity zend_type) { 155 | zend_long actualValue; 156 | 157 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 158 | Z_PARAM_LONG(actualValue) 159 | ZEND_PARSE_PARAMETERS_END_EX(return false); 160 | 161 | value = static_cast(actualValue); 162 | return true; 163 | } 164 | 165 | template 166 | bool parsePackTypeParams(zend_execute_data* execute_data, TValue& value, std::type_identity zend_type) { 167 | double actualValue; 168 | 169 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 170 | Z_PARAM_DOUBLE(actualValue) 171 | ZEND_PARSE_PARAMETERS_END_EX(return false); 172 | 173 | value = static_cast(actualValue); 174 | return true; 175 | } 176 | 177 | template writeTypeFunc, typename TZendValue> 178 | void ZEND_FASTCALL zif_packType(INTERNAL_FUNCTION_PARAMETERS) { 179 | TValue value; 180 | byte_buffer_writer_t writer = { 0 }; 181 | unsigned char stackBuffer[16]; //big enough for all currently supported types - it'd be good if the serializers told us how big the type is, but that's a problem for another time 182 | 183 | if (!parsePackTypeParams(execute_data, value, std::type_identity{})) { 184 | return; 185 | } 186 | 187 | writer.buffer = stackBuffer; 188 | writer.length = sizeof(stackBuffer); 189 | 190 | writeTypeCommon(INTERNAL_FUNCTION_PARAM_PASSTHRU, writer, value); 191 | 192 | RETVAL_STRINGL(reinterpret_cast(writer.buffer), writer.used); 193 | if (writer.buffer != stackBuffer) { 194 | printf("stack buffer wasn't big enough :(\n"); 195 | efree(writer.buffer); 196 | } 197 | } 198 | 199 | ZEND_NAMED_FUNCTION(pmmp_encoding_private_constructor) { 200 | //NOOP 201 | } 202 | 203 | #if PHP_VERSION_ID >= 80400 204 | #define BC_ZEND_RAW_FENTRY(zend_name, name, arg_info) ZEND_RAW_FENTRY(zend_name, name, arg_info, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, NULL, NULL) 205 | #define BC_ZEND_RAW_FENTRY_WITH_DOC_COMMENT(zend_name, name, arg_info, doc_comment) ZEND_RAW_FENTRY(zend_name, name, arg_info, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, NULL, doc_comment) 206 | #else 207 | #define BC_ZEND_RAW_FENTRY(zend_name, name, arg_info) ZEND_RAW_FENTRY(zend_name, name, arg_info, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) 208 | #define BC_ZEND_RAW_FENTRY_WITH_DOC_COMMENT(zend_name, name, arg_info, doc_comment) ZEND_RAW_FENTRY(zend_name, name, arg_info, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) 209 | #endif 210 | 211 | #define TYPE_ENTRIES(type_name, native_type, zend_type, read_type_func, write_type_func) \ 212 | BC_ZEND_RAW_FENTRY_WITH_DOC_COMMENT( \ 213 | "read" type_name, \ 214 | (zif_readType), \ 215 | arginfo_read_##zend_type, \ 216 | read_generic_doc_comment \ 217 | ) \ 218 | \ 219 | BC_ZEND_RAW_FENTRY_WITH_DOC_COMMENT( \ 220 | "unpack" type_name, \ 221 | (zif_unpackType), \ 222 | arginfo_unpack_##zend_type, \ 223 | read_generic_doc_comment \ 224 | ) \ 225 | BC_ZEND_RAW_FENTRY( \ 226 | "write" type_name, \ 227 | (zif_writeType), \ 228 | arginfo_write_##zend_type \ 229 | ) \ 230 | BC_ZEND_RAW_FENTRY( \ 231 | "pack" type_name, \ 232 | (zif_packType), \ 233 | arginfo_pack_##zend_type \ 234 | ) 235 | 236 | #define FIXED_TYPE_ENTRIES(type_name, native_type, zend_type, byte_order) \ 237 | TYPE_ENTRIES( \ 238 | type_name, \ 239 | native_type, \ 240 | zend_type, \ 241 | (readFixedSizeType), \ 242 | (writeFixedSizeType) \ 243 | ) 244 | 245 | #define FIXED_INT_BASE_ENTRIES(zend_name, native_type, byte_order) \ 246 | FIXED_TYPE_ENTRIES(zend_name, native_type, zend_long, byte_order) 247 | 248 | #define FIXED_INT_ENTRIES(zend_name, unsigned_native_type, signed_native_type, byte_order) \ 249 | FIXED_INT_BASE_ENTRIES("Unsigned" zend_name, unsigned_native_type, byte_order) \ 250 | \ 251 | FIXED_INT_BASE_ENTRIES("Signed" zend_name, signed_native_type, byte_order) 252 | 253 | #define FLOAT_ENTRIES(zend_name, native_type, byte_order) \ 254 | FIXED_TYPE_ENTRIES(zend_name, native_type, double, byte_order) 255 | 256 | #define COMPLEX_INT_ENTRIES(zend_name, native_type, read_type, write_type) \ 257 | TYPE_ENTRIES( \ 258 | zend_name, \ 259 | native_type, \ 260 | zend_long, \ 261 | read_type, \ 262 | write_type \ 263 | ) 264 | 265 | //triad can't used readFixedSizeType because it's not a power of 2 bytes 266 | #define TRIAD_ENTRIES(zend_name, unsigned_type, signed_type, byte_order) \ 267 | COMPLEX_INT_ENTRIES("Unsigned" zend_name, unsigned_type, (readInt24), (writeInt24)) \ 268 | COMPLEX_INT_ENTRIES("Signed" zend_name, signed_type, (readInt24), (writeInt24)) 269 | 270 | #define VARINT_ENTRIES(zend_name, unsigned_type, signed_type) \ 271 | COMPLEX_INT_ENTRIES("Unsigned" zend_name, unsigned_type, (readUnsignedVarInt), (writeUnsignedVarInt)) \ 272 | COMPLEX_INT_ENTRIES("Signed" zend_name, signed_type, (readSignedVarInt), (writeSignedVarInt)) 273 | 274 | #define BYTE_ENTRY(zend_name, native_type) \ 275 | TYPE_ENTRIES(zend_name, native_type, zend_long, (readByte), (writeByte)) 276 | 277 | #define ENDIAN_ENTRIES(enum_case) \ 278 | FIXED_INT_ENTRIES("Short", uint16_t, int16_t, enum_case) \ 279 | FIXED_INT_ENTRIES("Int", uint32_t, int32_t, enum_case) \ 280 | \ 281 | /* Technically, PHP doesn't support unsigned longs in userland, however the functions are still useful for making code intent obvious */ \ 282 | FIXED_INT_ENTRIES("Long", uint64_t, int64_t, enum_case) \ 283 | \ 284 | FLOAT_ENTRIES("Float", float, enum_case) \ 285 | FLOAT_ENTRIES("Double", double, enum_case) \ 286 | \ 287 | TRIAD_ENTRIES("Triad", uint32_t, int32_t, enum_case) 288 | 289 | ZEND_BEGIN_ARG_INFO_EX(empty_constructor_arg_info, 0, 0, 0) 290 | ZEND_END_ARG_INFO() 291 | 292 | static zend_function_entry byte_methods[] = { 293 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 294 | 295 | BYTE_ENTRY("Unsigned", uint8_t) 296 | BYTE_ENTRY("Signed", int8_t) 297 | 298 | PHP_FE_END 299 | }; 300 | 301 | static zend_function_entry big_endian_methods[] = { 302 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 303 | 304 | ENDIAN_ENTRIES(ByteOrder::BigEndian) 305 | PHP_FE_END 306 | }; 307 | static zend_function_entry little_endian_methods[] = { 308 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 309 | 310 | ENDIAN_ENTRIES(ByteOrder::LittleEndian) 311 | PHP_FE_END 312 | }; 313 | 314 | static zend_function_entry varint_methods[] = { 315 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 316 | 317 | VARINT_ENTRIES("Int", uint32_t, int32_t) 318 | VARINT_ENTRIES("Long", uint64_t, int64_t) 319 | PHP_FE_END 320 | }; 321 | 322 | void init_class_Types(void) { 323 | zend_class_entry ce; 324 | 325 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "Byte", byte_methods); 326 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 327 | zend_register_internal_class(&ce); 328 | 329 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "BE", big_endian_methods); 330 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 331 | zend_register_internal_class(&ce); 332 | 333 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "LE", little_endian_methods); 334 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 335 | zend_register_internal_class(&ce); 336 | 337 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "VarInt", varint_methods); 338 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 339 | zend_register_internal_class(&ce); 340 | } 341 | -------------------------------------------------------------------------------- /tests/symmetry.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test symmetry of all encode/decode methods and their array variants 3 | --FILE-- 4 | getSamples(); 23 | testFullSymmetry("big endian " . $case->name, $samples, ...$case->getMethods(true)); 24 | testFullSymmetry("little endian " . $case->name, $samples, ...$case->getMethods(false)); 25 | } 26 | 27 | 28 | foreach(FloatSamples::cases() as $case){ 29 | $samples = [-1.5, -1.0, 0.0, 1.0, 1.5]; //TODO: float bounds are hard to test 30 | testFullSymmetry("big endian " . $case->name, $samples, ...$case->getMethods(true)); 31 | testFullSymmetry("little endian " . $case->name, $samples, ...$case->getMethods(false)); 32 | } 33 | 34 | testFullSymmetry("varint", EndianInts::UNSIGNED_INT->getSamples(), VarInt::readUnsignedInt(...), VarInt::writeUnsignedInt(...), VarInt::unpackUnsignedInt(...), VarInt::packUnsignedInt(...)); 35 | testFullSymmetry("varint zigzag", EndianInts::SIGNED_INT->getSamples(), VarInt::readSignedInt(...), VarInt::writeSignedInt(...), VarInt::unpackSignedInt(...), VarInt::packSignedInt(...)); 36 | testFullSymmetry("varlong", EndianInts::UNSIGNED_LONG->getSamples(), VarInt::readUnsignedLong(...), VarInt::writeUnsignedLong(...), VarInt::unpackUnsignedLong(...), VarInt::packUnsignedLong(...)); 37 | testFullSymmetry("varlong zigzag", EndianInts::SIGNED_LONG->getSamples(), VarInt::readSignedLong(...), VarInt::writeSignedLong(...), VarInt::unpackSignedLong(...), VarInt::packSignedLong(...)); 38 | ?> 39 | --EXPECT-- 40 | ########## TEST unsigned byte ########## 41 | --- single read symmetry --- 42 | (0): match = YES 43 | (1): match = YES 44 | (127): match = YES 45 | (128): match = YES 46 | (254): match = YES 47 | (255): match = YES 48 | ########## END TEST unsigned byte ########## 49 | 50 | ########## TEST signed byte ########## 51 | --- single read symmetry --- 52 | (-128): match = YES 53 | (-1): match = YES 54 | (0): match = YES 55 | (1): match = YES 56 | (126): match = YES 57 | (127): match = YES 58 | ########## END TEST signed byte ########## 59 | 60 | ########## TEST big endian UNSIGNED_SHORT ########## 61 | --- single read symmetry --- 62 | (0): match = YES 63 | (1): match = YES 64 | (65534): match = YES 65 | (65535): match = YES 66 | --- single pack vs buffer read symmetry --- 67 | (0): match = YES 68 | (1): match = YES 69 | (65534): match = YES 70 | (65535): match = YES 71 | --- single buffer write vs unpack symmetry --- 72 | (0): match = YES 73 | (1): match = YES 74 | (65534): match = YES 75 | (65535): match = YES 76 | ########## END TEST big endian UNSIGNED_SHORT ########## 77 | 78 | ########## TEST little endian UNSIGNED_SHORT ########## 79 | --- single read symmetry --- 80 | (0): match = YES 81 | (1): match = YES 82 | (65534): match = YES 83 | (65535): match = YES 84 | --- single pack vs buffer read symmetry --- 85 | (0): match = YES 86 | (1): match = YES 87 | (65534): match = YES 88 | (65535): match = YES 89 | --- single buffer write vs unpack symmetry --- 90 | (0): match = YES 91 | (1): match = YES 92 | (65534): match = YES 93 | (65535): match = YES 94 | ########## END TEST little endian UNSIGNED_SHORT ########## 95 | 96 | ########## TEST big endian SIGNED_SHORT ########## 97 | --- single read symmetry --- 98 | (-32768): match = YES 99 | (-32767): match = YES 100 | (-1): match = YES 101 | (0): match = YES 102 | (1): match = YES 103 | (32766): match = YES 104 | (32767): match = YES 105 | --- single pack vs buffer read symmetry --- 106 | (-32768): match = YES 107 | (-32767): match = YES 108 | (-1): match = YES 109 | (0): match = YES 110 | (1): match = YES 111 | (32766): match = YES 112 | (32767): match = YES 113 | --- single buffer write vs unpack symmetry --- 114 | (-32768): match = YES 115 | (-32767): match = YES 116 | (-1): match = YES 117 | (0): match = YES 118 | (1): match = YES 119 | (32766): match = YES 120 | (32767): match = YES 121 | ########## END TEST big endian SIGNED_SHORT ########## 122 | 123 | ########## TEST little endian SIGNED_SHORT ########## 124 | --- single read symmetry --- 125 | (-32768): match = YES 126 | (-32767): match = YES 127 | (-1): match = YES 128 | (0): match = YES 129 | (1): match = YES 130 | (32766): match = YES 131 | (32767): match = YES 132 | --- single pack vs buffer read symmetry --- 133 | (-32768): match = YES 134 | (-32767): match = YES 135 | (-1): match = YES 136 | (0): match = YES 137 | (1): match = YES 138 | (32766): match = YES 139 | (32767): match = YES 140 | --- single buffer write vs unpack symmetry --- 141 | (-32768): match = YES 142 | (-32767): match = YES 143 | (-1): match = YES 144 | (0): match = YES 145 | (1): match = YES 146 | (32766): match = YES 147 | (32767): match = YES 148 | ########## END TEST little endian SIGNED_SHORT ########## 149 | 150 | ########## TEST big endian UNSIGNED_TRIAD ########## 151 | --- single read symmetry --- 152 | (0): match = YES 153 | (1): match = YES 154 | (16777214): match = YES 155 | (16777215): match = YES 156 | --- single pack vs buffer read symmetry --- 157 | (0): match = YES 158 | (1): match = YES 159 | (16777214): match = YES 160 | (16777215): match = YES 161 | --- single buffer write vs unpack symmetry --- 162 | (0): match = YES 163 | (1): match = YES 164 | (16777214): match = YES 165 | (16777215): match = YES 166 | ########## END TEST big endian UNSIGNED_TRIAD ########## 167 | 168 | ########## TEST little endian UNSIGNED_TRIAD ########## 169 | --- single read symmetry --- 170 | (0): match = YES 171 | (1): match = YES 172 | (16777214): match = YES 173 | (16777215): match = YES 174 | --- single pack vs buffer read symmetry --- 175 | (0): match = YES 176 | (1): match = YES 177 | (16777214): match = YES 178 | (16777215): match = YES 179 | --- single buffer write vs unpack symmetry --- 180 | (0): match = YES 181 | (1): match = YES 182 | (16777214): match = YES 183 | (16777215): match = YES 184 | ########## END TEST little endian UNSIGNED_TRIAD ########## 185 | 186 | ########## TEST big endian SIGNED_TRIAD ########## 187 | --- single read symmetry --- 188 | (-8388608): match = YES 189 | (-8388607): match = YES 190 | (-1): match = YES 191 | (0): match = YES 192 | (1): match = YES 193 | (8388606): match = YES 194 | (8388607): match = YES 195 | --- single pack vs buffer read symmetry --- 196 | (-8388608): match = YES 197 | (-8388607): match = YES 198 | (-1): match = YES 199 | (0): match = YES 200 | (1): match = YES 201 | (8388606): match = YES 202 | (8388607): match = YES 203 | --- single buffer write vs unpack symmetry --- 204 | (-8388608): match = YES 205 | (-8388607): match = YES 206 | (-1): match = YES 207 | (0): match = YES 208 | (1): match = YES 209 | (8388606): match = YES 210 | (8388607): match = YES 211 | ########## END TEST big endian SIGNED_TRIAD ########## 212 | 213 | ########## TEST little endian SIGNED_TRIAD ########## 214 | --- single read symmetry --- 215 | (-8388608): match = YES 216 | (-8388607): match = YES 217 | (-1): match = YES 218 | (0): match = YES 219 | (1): match = YES 220 | (8388606): match = YES 221 | (8388607): match = YES 222 | --- single pack vs buffer read symmetry --- 223 | (-8388608): match = YES 224 | (-8388607): match = YES 225 | (-1): match = YES 226 | (0): match = YES 227 | (1): match = YES 228 | (8388606): match = YES 229 | (8388607): match = YES 230 | --- single buffer write vs unpack symmetry --- 231 | (-8388608): match = YES 232 | (-8388607): match = YES 233 | (-1): match = YES 234 | (0): match = YES 235 | (1): match = YES 236 | (8388606): match = YES 237 | (8388607): match = YES 238 | ########## END TEST little endian SIGNED_TRIAD ########## 239 | 240 | ########## TEST big endian UNSIGNED_INT ########## 241 | --- single read symmetry --- 242 | (0): match = YES 243 | (1): match = YES 244 | (4294967294): match = YES 245 | (4294967295): match = YES 246 | --- single pack vs buffer read symmetry --- 247 | (0): match = YES 248 | (1): match = YES 249 | (4294967294): match = YES 250 | (4294967295): match = YES 251 | --- single buffer write vs unpack symmetry --- 252 | (0): match = YES 253 | (1): match = YES 254 | (4294967294): match = YES 255 | (4294967295): match = YES 256 | ########## END TEST big endian UNSIGNED_INT ########## 257 | 258 | ########## TEST little endian UNSIGNED_INT ########## 259 | --- single read symmetry --- 260 | (0): match = YES 261 | (1): match = YES 262 | (4294967294): match = YES 263 | (4294967295): match = YES 264 | --- single pack vs buffer read symmetry --- 265 | (0): match = YES 266 | (1): match = YES 267 | (4294967294): match = YES 268 | (4294967295): match = YES 269 | --- single buffer write vs unpack symmetry --- 270 | (0): match = YES 271 | (1): match = YES 272 | (4294967294): match = YES 273 | (4294967295): match = YES 274 | ########## END TEST little endian UNSIGNED_INT ########## 275 | 276 | ########## TEST big endian SIGNED_INT ########## 277 | --- single read symmetry --- 278 | (-2147483648): match = YES 279 | (-2147483647): match = YES 280 | (-1): match = YES 281 | (0): match = YES 282 | (1): match = YES 283 | (2147483646): match = YES 284 | (2147483647): match = YES 285 | --- single pack vs buffer read symmetry --- 286 | (-2147483648): match = YES 287 | (-2147483647): match = YES 288 | (-1): match = YES 289 | (0): match = YES 290 | (1): match = YES 291 | (2147483646): match = YES 292 | (2147483647): match = YES 293 | --- single buffer write vs unpack symmetry --- 294 | (-2147483648): match = YES 295 | (-2147483647): match = YES 296 | (-1): match = YES 297 | (0): match = YES 298 | (1): match = YES 299 | (2147483646): match = YES 300 | (2147483647): match = YES 301 | ########## END TEST big endian SIGNED_INT ########## 302 | 303 | ########## TEST little endian SIGNED_INT ########## 304 | --- single read symmetry --- 305 | (-2147483648): match = YES 306 | (-2147483647): match = YES 307 | (-1): match = YES 308 | (0): match = YES 309 | (1): match = YES 310 | (2147483646): match = YES 311 | (2147483647): match = YES 312 | --- single pack vs buffer read symmetry --- 313 | (-2147483648): match = YES 314 | (-2147483647): match = YES 315 | (-1): match = YES 316 | (0): match = YES 317 | (1): match = YES 318 | (2147483646): match = YES 319 | (2147483647): match = YES 320 | --- single buffer write vs unpack symmetry --- 321 | (-2147483648): match = YES 322 | (-2147483647): match = YES 323 | (-1): match = YES 324 | (0): match = YES 325 | (1): match = YES 326 | (2147483646): match = YES 327 | (2147483647): match = YES 328 | ########## END TEST little endian SIGNED_INT ########## 329 | 330 | ########## TEST big endian UNSIGNED_LONG ########## 331 | --- single read symmetry --- 332 | (-9223372036854775808): match = YES 333 | (-9223372036854775807): match = YES 334 | (-1): match = YES 335 | (0): match = YES 336 | (1): match = YES 337 | (9223372036854775806): match = YES 338 | (9223372036854775807): match = YES 339 | --- single pack vs buffer read symmetry --- 340 | (-9223372036854775808): match = YES 341 | (-9223372036854775807): match = YES 342 | (-1): match = YES 343 | (0): match = YES 344 | (1): match = YES 345 | (9223372036854775806): match = YES 346 | (9223372036854775807): match = YES 347 | --- single buffer write vs unpack symmetry --- 348 | (-9223372036854775808): match = YES 349 | (-9223372036854775807): match = YES 350 | (-1): match = YES 351 | (0): match = YES 352 | (1): match = YES 353 | (9223372036854775806): match = YES 354 | (9223372036854775807): match = YES 355 | ########## END TEST big endian UNSIGNED_LONG ########## 356 | 357 | ########## TEST little endian UNSIGNED_LONG ########## 358 | --- single read symmetry --- 359 | (-9223372036854775808): match = YES 360 | (-9223372036854775807): match = YES 361 | (-1): match = YES 362 | (0): match = YES 363 | (1): match = YES 364 | (9223372036854775806): match = YES 365 | (9223372036854775807): match = YES 366 | --- single pack vs buffer read symmetry --- 367 | (-9223372036854775808): match = YES 368 | (-9223372036854775807): match = YES 369 | (-1): match = YES 370 | (0): match = YES 371 | (1): match = YES 372 | (9223372036854775806): match = YES 373 | (9223372036854775807): match = YES 374 | --- single buffer write vs unpack symmetry --- 375 | (-9223372036854775808): match = YES 376 | (-9223372036854775807): match = YES 377 | (-1): match = YES 378 | (0): match = YES 379 | (1): match = YES 380 | (9223372036854775806): match = YES 381 | (9223372036854775807): match = YES 382 | ########## END TEST little endian UNSIGNED_LONG ########## 383 | 384 | ########## TEST big endian SIGNED_LONG ########## 385 | --- single read symmetry --- 386 | (-9223372036854775808): match = YES 387 | (-9223372036854775807): match = YES 388 | (-1): match = YES 389 | (0): match = YES 390 | (1): match = YES 391 | (9223372036854775806): match = YES 392 | (9223372036854775807): match = YES 393 | --- single pack vs buffer read symmetry --- 394 | (-9223372036854775808): match = YES 395 | (-9223372036854775807): match = YES 396 | (-1): match = YES 397 | (0): match = YES 398 | (1): match = YES 399 | (9223372036854775806): match = YES 400 | (9223372036854775807): match = YES 401 | --- single buffer write vs unpack symmetry --- 402 | (-9223372036854775808): match = YES 403 | (-9223372036854775807): match = YES 404 | (-1): match = YES 405 | (0): match = YES 406 | (1): match = YES 407 | (9223372036854775806): match = YES 408 | (9223372036854775807): match = YES 409 | ########## END TEST big endian SIGNED_LONG ########## 410 | 411 | ########## TEST little endian SIGNED_LONG ########## 412 | --- single read symmetry --- 413 | (-9223372036854775808): match = YES 414 | (-9223372036854775807): match = YES 415 | (-1): match = YES 416 | (0): match = YES 417 | (1): match = YES 418 | (9223372036854775806): match = YES 419 | (9223372036854775807): match = YES 420 | --- single pack vs buffer read symmetry --- 421 | (-9223372036854775808): match = YES 422 | (-9223372036854775807): match = YES 423 | (-1): match = YES 424 | (0): match = YES 425 | (1): match = YES 426 | (9223372036854775806): match = YES 427 | (9223372036854775807): match = YES 428 | --- single buffer write vs unpack symmetry --- 429 | (-9223372036854775808): match = YES 430 | (-9223372036854775807): match = YES 431 | (-1): match = YES 432 | (0): match = YES 433 | (1): match = YES 434 | (9223372036854775806): match = YES 435 | (9223372036854775807): match = YES 436 | ########## END TEST little endian SIGNED_LONG ########## 437 | 438 | ########## TEST big endian FLOAT ########## 439 | --- single read symmetry --- 440 | (-1.5): match = YES 441 | (-1): match = YES 442 | (0): match = YES 443 | (1): match = YES 444 | (1.5): match = YES 445 | --- single pack vs buffer read symmetry --- 446 | (-1.5): match = YES 447 | (-1): match = YES 448 | (0): match = YES 449 | (1): match = YES 450 | (1.5): match = YES 451 | --- single buffer write vs unpack symmetry --- 452 | (-1.5): match = YES 453 | (-1): match = YES 454 | (0): match = YES 455 | (1): match = YES 456 | (1.5): match = YES 457 | ########## END TEST big endian FLOAT ########## 458 | 459 | ########## TEST little endian FLOAT ########## 460 | --- single read symmetry --- 461 | (-1.5): match = YES 462 | (-1): match = YES 463 | (0): match = YES 464 | (1): match = YES 465 | (1.5): match = YES 466 | --- single pack vs buffer read symmetry --- 467 | (-1.5): match = YES 468 | (-1): match = YES 469 | (0): match = YES 470 | (1): match = YES 471 | (1.5): match = YES 472 | --- single buffer write vs unpack symmetry --- 473 | (-1.5): match = YES 474 | (-1): match = YES 475 | (0): match = YES 476 | (1): match = YES 477 | (1.5): match = YES 478 | ########## END TEST little endian FLOAT ########## 479 | 480 | ########## TEST big endian DOUBLE ########## 481 | --- single read symmetry --- 482 | (-1.5): match = YES 483 | (-1): match = YES 484 | (0): match = YES 485 | (1): match = YES 486 | (1.5): match = YES 487 | --- single pack vs buffer read symmetry --- 488 | (-1.5): match = YES 489 | (-1): match = YES 490 | (0): match = YES 491 | (1): match = YES 492 | (1.5): match = YES 493 | --- single buffer write vs unpack symmetry --- 494 | (-1.5): match = YES 495 | (-1): match = YES 496 | (0): match = YES 497 | (1): match = YES 498 | (1.5): match = YES 499 | ########## END TEST big endian DOUBLE ########## 500 | 501 | ########## TEST little endian DOUBLE ########## 502 | --- single read symmetry --- 503 | (-1.5): match = YES 504 | (-1): match = YES 505 | (0): match = YES 506 | (1): match = YES 507 | (1.5): match = YES 508 | --- single pack vs buffer read symmetry --- 509 | (-1.5): match = YES 510 | (-1): match = YES 511 | (0): match = YES 512 | (1): match = YES 513 | (1.5): match = YES 514 | --- single buffer write vs unpack symmetry --- 515 | (-1.5): match = YES 516 | (-1): match = YES 517 | (0): match = YES 518 | (1): match = YES 519 | (1.5): match = YES 520 | ########## END TEST little endian DOUBLE ########## 521 | 522 | ########## TEST varint ########## 523 | --- single read symmetry --- 524 | (0): match = YES 525 | (1): match = YES 526 | (4294967294): match = YES 527 | (4294967295): match = YES 528 | --- single pack vs buffer read symmetry --- 529 | (0): match = YES 530 | (1): match = YES 531 | (4294967294): match = YES 532 | (4294967295): match = YES 533 | --- single buffer write vs unpack symmetry --- 534 | (0): match = YES 535 | (1): match = YES 536 | (4294967294): match = YES 537 | (4294967295): match = YES 538 | ########## END TEST varint ########## 539 | 540 | ########## TEST varint zigzag ########## 541 | --- single read symmetry --- 542 | (-2147483648): match = YES 543 | (-2147483647): match = YES 544 | (-1): match = YES 545 | (0): match = YES 546 | (1): match = YES 547 | (2147483646): match = YES 548 | (2147483647): match = YES 549 | --- single pack vs buffer read symmetry --- 550 | (-2147483648): match = YES 551 | (-2147483647): match = YES 552 | (-1): match = YES 553 | (0): match = YES 554 | (1): match = YES 555 | (2147483646): match = YES 556 | (2147483647): match = YES 557 | --- single buffer write vs unpack symmetry --- 558 | (-2147483648): match = YES 559 | (-2147483647): match = YES 560 | (-1): match = YES 561 | (0): match = YES 562 | (1): match = YES 563 | (2147483646): match = YES 564 | (2147483647): match = YES 565 | ########## END TEST varint zigzag ########## 566 | 567 | ########## TEST varlong ########## 568 | --- single read symmetry --- 569 | (-9223372036854775808): match = YES 570 | (-9223372036854775807): match = YES 571 | (-1): match = YES 572 | (0): match = YES 573 | (1): match = YES 574 | (9223372036854775806): match = YES 575 | (9223372036854775807): match = YES 576 | --- single pack vs buffer read symmetry --- 577 | (-9223372036854775808): match = YES 578 | (-9223372036854775807): match = YES 579 | (-1): match = YES 580 | (0): match = YES 581 | (1): match = YES 582 | (9223372036854775806): match = YES 583 | (9223372036854775807): match = YES 584 | --- single buffer write vs unpack symmetry --- 585 | (-9223372036854775808): match = YES 586 | (-9223372036854775807): match = YES 587 | (-1): match = YES 588 | (0): match = YES 589 | (1): match = YES 590 | (9223372036854775806): match = YES 591 | (9223372036854775807): match = YES 592 | ########## END TEST varlong ########## 593 | 594 | ########## TEST varlong zigzag ########## 595 | --- single read symmetry --- 596 | (-9223372036854775808): match = YES 597 | (-9223372036854775807): match = YES 598 | (-1): match = YES 599 | (0): match = YES 600 | (1): match = YES 601 | (9223372036854775806): match = YES 602 | (9223372036854775807): match = YES 603 | --- single pack vs buffer read symmetry --- 604 | (-9223372036854775808): match = YES 605 | (-9223372036854775807): match = YES 606 | (-1): match = YES 607 | (0): match = YES 608 | (1): match = YES 609 | (9223372036854775806): match = YES 610 | (9223372036854775807): match = YES 611 | --- single buffer write vs unpack symmetry --- 612 | (-9223372036854775808): match = YES 613 | (-9223372036854775807): match = YES 614 | (-1): match = YES 615 | (0): match = YES 616 | (1): match = YES 617 | (9223372036854775806): match = YES 618 | (9223372036854775807): match = YES 619 | ########## END TEST varlong zigzag ########## 620 | --------------------------------------------------------------------------------