├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── Serializers.h ├── ZendUtil.h ├── classes ├── ByteBuffer.cpp ├── ByteBuffer.h ├── DataDecodeException.h ├── Types.cpp └── Types.h ├── config.m4 ├── config.w32 ├── encoding.cpp ├── php_encoding.h ├── stubs ├── ByteBuffer.stub.php ├── ByteBuffer_arginfo.h ├── DataDecodeException.stub.php └── DataDecodeException_arginfo.h └── tests ├── buffer-clone.phpt ├── buffer-serialize-unserialize.phpt ├── buffer-to-string.phpt ├── buffer-var-dump.phpt ├── byte-buffer-new.phpt ├── fixed-size-types.inc ├── negative-input-unsigned-varint.phpt ├── not-enough-bytes-fixed-size.phpt ├── not-enough-bytes-varint.phpt ├── pack-unpack-parity.phpt ├── read-byte-array.phpt ├── read-reserved-memory.phpt ├── read-triad.phpt ├── reserve.phpt ├── set-write-offset.phpt ├── too-many-bytes-varint.phpt ├── trim.phpt ├── update-offset-fixed-size.phpt ├── update-offset-varint.phpt ├── write-byte-array.phpt └── write-triad.phpt /.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 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /.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-20.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@v4 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 }}-generic 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@v4 93 | with: 94 | name: test-results-${{ matrix.php }}-valgrind-${{ matrix.valgrind }} 95 | path: | 96 | ${{ github.workspace }}/tests/* 97 | !${{ github.workspace }}/tests/*.phpt 98 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ext-encoding 2 | This extension implements a `ByteBuffer` class, a high-performance alternative for [`pocketmine/binaryutils`](https://github.com/pmmp/BinaryUtils). 3 | 4 | ## :warning: This extension is EXPERIMENTAL 5 | 6 | There is a high likelihood that the extension's classes may crash or behave incorrectly. 7 | Do not use this extension for anything you don't want to get horribly corrupted. 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 | #### :warning: The API design is not yet finalized. Everything is still subject to change prior to the 1.0.0 release. 13 | 14 | ## Real-world performance tests 15 | - [`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. 16 | 17 | ## Performance considerations 18 | ### VarInts 19 | VarInts are heavily used by the Bedrock protocol, the theory being to reduce the size of integer types on the wire. 20 | This format is borrowed from [protobuf](https://developers.google.com/protocol-buffers/docs/encoding). 21 | 22 | Implemented in PHP, it's abysmally slow, due to repeated calls to `chr()` and `ord()` in a loop, as well as needing workarounds for PHP's lack of logical rightshift. 23 | 24 | Compared to `BinaryStream`, `ByteBuffer` offers a performance improvement of 5-10x (depending on the size of the value and other conditions, YMMV) with both signed and unsigned varints. 25 | 26 | This is extremely significant for PocketMine-MP due to the number of hot paths affected by such a performance gain (e.g. chunk encoding will benefit significantly). 27 | 28 | ### Fixed size types 29 | Under a profiler, it becomes obvious that PHP's `pack()` and `unpack()` functions are abysmally slow, due to the cost of parsing the formatting code argument. 30 | This parsing takes over 90% of the time spent in `pack()` and `unpack()`. 31 | This overhead can be easily avoided when the types of data used are known in advance. 32 | 33 | This extension implements specialized functions for writing big and little endian byte/short/int/long/float/double. 34 | Depending on the type and other factors, these functions typically show a 3-4x performance improvement compared to `BinaryStream`. 35 | 36 | ### Linear memory allocations 37 | `BinaryStream` and similar PHP-land byte-buffer implementations often use strings and use the `.=` concatenation operator. 38 | This is problematic, because the entire string will be reallocated every time something is appended to it. 39 | While this isn't a big issue for small buffers, the performance of writing to large buffers progressively degrades. 40 | 41 | `ByteBuffer` uses exponential scaling (factor of 2) to minimize buffer reallocations at the cost of potentially wasting some memory. 42 | This means that the internal buffer size is doubled when the buffer runs out of space. 43 | -------------------------------------------------------------------------------- /Serializers.h: -------------------------------------------------------------------------------- 1 | #ifndef SERIALIZERS_H 2 | #define SERIALIZERS_H 3 | 4 | extern "C" { 5 | #include "Zend/zend_string.h" 6 | #include "Zend/zend_exceptions.h" 7 | } 8 | #include "classes/DataDecodeException.h" 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 readInt24(unsigned char* bytes, size_t used, size_t& offset, TValue& result) { 66 | const size_t SIZE = 3; 67 | 68 | if (used - offset < SIZE) { 69 | zend_throw_exception_ex(data_decode_exception_ce, 0, "Need at least %zu bytes, but only have %zu bytes", SIZE, used - offset); 70 | return false; 71 | } 72 | 73 | result = 0; 74 | if (byteOrder == ByteOrder::LittleEndian) { 75 | result |= bytes[offset]; 76 | result |= bytes[offset + 1] << 8; 77 | result |= bytes[offset + 2] << 16; 78 | } 79 | else { 80 | result |= bytes[offset + 2]; 81 | result |= bytes[offset + 1] << 8; 82 | result |= bytes[offset] << 16; 83 | } 84 | 85 | const size_t SIGNED_SHIFT = std::is_signed::value ? (sizeof(TValue) - SIZE) * CHAR_BIT : 0; 86 | if (SIGNED_SHIFT > 0) { 87 | result = (result << SIGNED_SHIFT) >> SIGNED_SHIFT; 88 | } 89 | 90 | offset += SIZE; 91 | return true; 92 | } 93 | 94 | struct VarIntConstants { 95 | static const unsigned char BITS_PER_BYTE = 7u; 96 | 97 | template 98 | static const unsigned char MAX_BYTES = TYPE_BITS / BITS_PER_BYTE + ((TYPE_BITS % BITS_PER_BYTE) > 0); 99 | 100 | static const unsigned char VALUE_MASK = static_cast(~(1u << BITS_PER_BYTE)); 101 | static const unsigned char MSB_MASK = static_cast(1u << BITS_PER_BYTE); 102 | }; 103 | 104 | template 105 | static inline bool readUnsignedVarInt(unsigned char* bytes, size_t used, size_t& offset, TValue& result) { 106 | const auto TYPE_BITS = sizeof(TValue) * CHAR_BIT; 107 | result = 0; 108 | for (unsigned int shift = 0; shift < TYPE_BITS; shift += VarIntConstants::BITS_PER_BYTE) { 109 | if (offset >= used) { 110 | zend_throw_exception(data_decode_exception_ce, "No bytes left in buffer", 0); 111 | return false; 112 | } 113 | 114 | auto byte = bytes[offset++]; 115 | result |= (byte & VarIntConstants::VALUE_MASK) << shift; 116 | if ((byte & VarIntConstants::MSB_MASK) == 0) { 117 | return true; 118 | } 119 | } 120 | 121 | zend_throw_exception_ex(data_decode_exception_ce, 0, "VarInt did not terminate after %u bytes!", VarIntConstants::MAX_BYTES); 122 | return false; 123 | } 124 | 125 | template 126 | static inline bool readSignedVarInt(unsigned char* bytes, size_t used, size_t& offset, TSignedValue& result) { 127 | TUnsignedValue unsignedResult; 128 | if (!readUnsignedVarInt(bytes, used, offset, unsignedResult)) { 129 | return false; 130 | } 131 | 132 | TUnsignedValue mask = 0; 133 | if (unsignedResult & 1) { 134 | //we don't know the type of TUnsignedValue here so we can't just use ~0 135 | mask = ~mask; 136 | } 137 | 138 | result = static_cast((unsignedResult >> 1) ^ mask); 139 | return true; 140 | } 141 | 142 | 143 | static inline void extendBuffer(zend_string*& buffer, size_t offset, size_t usedBytes) { 144 | size_t requiredSize = offset + usedBytes; 145 | if (ZSTR_LEN(buffer) < requiredSize) { 146 | size_t doubleSize = ZSTR_LEN(buffer) * 2; 147 | buffer = zend_string_realloc(buffer, doubleSize > requiredSize ? doubleSize : requiredSize, 0); 148 | ZSTR_VAL(buffer)[ZSTR_LEN(buffer)] = '\0'; //make sure null terminator is always set, to stop sprintf reading out-of-bounds 149 | } 150 | else { 151 | buffer = zend_string_separate(buffer, 0); 152 | } 153 | } 154 | 155 | template 156 | static void writeByte(zend_string*& buffer, size_t& offset, TValue value) { 157 | extendBuffer(buffer, offset, sizeof(TValue)); 158 | 159 | ZSTR_VAL(buffer)[offset] = *reinterpret_cast(&value); 160 | 161 | offset += sizeof(TValue); 162 | } 163 | 164 | template 165 | static void writeFixedSizeType(zend_string*& buffer, size_t& offset, TValue value) { 166 | extendBuffer(buffer, offset, sizeof(TValue)); 167 | 168 | Flipper flipper; 169 | flipper.value = value; 170 | 171 | if (byteOrder != ByteOrder::Native) { 172 | std::reverse(std::begin(flipper.bytes), std::end(flipper.bytes)); 173 | } 174 | 175 | memcpy(&ZSTR_VAL(buffer)[offset], flipper.bytes, sizeof(flipper.bytes)); 176 | 177 | offset += sizeof(TValue); 178 | } 179 | 180 | template 181 | static void writeInt24(zend_string*& buffer, size_t& offset, TValue value) { 182 | const size_t SIZE = 3; 183 | extendBuffer(buffer, offset, SIZE); 184 | 185 | if (byteOrder == ByteOrder::LittleEndian) { 186 | ZSTR_VAL(buffer)[offset] = value & 0xff; 187 | ZSTR_VAL(buffer)[offset + 1] = (value >> 8) & 0xff; 188 | ZSTR_VAL(buffer)[offset + 2] = (value >> 16) & 0xff; 189 | } 190 | else { 191 | ZSTR_VAL(buffer)[offset] = (value >> 16) & 0xff; 192 | ZSTR_VAL(buffer)[offset + 1] = (value >> 8) & 0xff; 193 | ZSTR_VAL(buffer)[offset + 2] = value & 0xff; 194 | } 195 | 196 | offset += SIZE; 197 | } 198 | 199 | template 200 | static inline void writeUnsignedVarInt(zend_string*& buffer, size_t& offset, TValue value) { 201 | const auto TYPE_BITS = sizeof(TValue) * CHAR_BIT; 202 | char result[VarIntConstants::MAX_BYTES]; 203 | 204 | TValue remaining = value; 205 | 206 | for (auto i = 0; i < VarIntConstants::MAX_BYTES; i++) { 207 | unsigned char nextByte = remaining & VarIntConstants::VALUE_MASK; 208 | 209 | TValue nextRemaining = remaining >> VarIntConstants::BITS_PER_BYTE; 210 | 211 | if (nextRemaining == 0) { 212 | result[i] = nextByte; 213 | 214 | auto usedBytes = i + 1; 215 | extendBuffer(buffer, offset, usedBytes); 216 | memcpy(&ZSTR_VAL(buffer)[offset], &result[0], usedBytes); 217 | offset += usedBytes; 218 | 219 | return; 220 | } 221 | 222 | result[i] = nextByte | VarIntConstants::MSB_MASK; 223 | remaining = nextRemaining; 224 | } 225 | 226 | zend_value_error("Value too large to be encoded as a VarInt"); 227 | } 228 | 229 | template 230 | static inline void writeSignedVarInt(zend_string*& buffer, size_t& offset, TSignedType value) { 231 | TUnsignedType mask = 0; 232 | if (value < 0) { 233 | //we don't know the type of TUnsignedType here, can't use ~0 directly (the compiler will optimise this anyway) 234 | mask = ~mask; 235 | } 236 | 237 | writeUnsignedVarInt(buffer, offset, (static_cast(value) << 1) ^ mask); 238 | } 239 | 240 | #endif 241 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /classes/ByteBuffer.cpp: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include "php.h" 3 | #include "Zend/zend_exceptions.h" 4 | #include "../stubs/ByteBuffer_arginfo.h" 5 | } 6 | 7 | #include "ByteBuffer.h" 8 | #include "DataDecodeException.h" 9 | #include "../Serializers.h" 10 | 11 | static zend_object_handlers byte_buffer_zend_object_handlers; 12 | zend_class_entry* byte_buffer_ce; 13 | 14 | static void byte_buffer_init_properties(byte_buffer_zend_object* object, zend_string* buffer, size_t used, size_t read_offset, size_t write_offset) { 15 | object->buffer = buffer; 16 | zend_string_addref(buffer); 17 | object->read_offset = read_offset; 18 | object->write_offset = write_offset; 19 | object->used = used; 20 | } 21 | 22 | static zend_object* byte_buffer_new(zend_class_entry* ce) { 23 | auto object = alloc_custom_zend_object(ce, &byte_buffer_zend_object_handlers); 24 | 25 | byte_buffer_init_properties(object, zend_empty_string, 0, 0, 0); 26 | 27 | return &object->std; 28 | } 29 | 30 | static zend_object* byte_buffer_clone(zend_object* object) { 31 | auto old_object = fetch_from_zend_object(object); 32 | auto new_object = fetch_from_zend_object(byte_buffer_new(object->ce)); 33 | 34 | zend_objects_clone_members(&new_object->std, &old_object->std); 35 | 36 | byte_buffer_init_properties(new_object, old_object->buffer, old_object->used, old_object->read_offset, old_object->write_offset); 37 | 38 | return &new_object->std; 39 | } 40 | 41 | static void byte_buffer_free(zend_object* std) { 42 | auto object = fetch_from_zend_object(std); 43 | 44 | zend_string_release_ex(object->buffer, 0); 45 | } 46 | 47 | static int byte_buffer_compare_objects(zval* obj1, zval* obj2) { 48 | if (Z_TYPE_P(obj1) == IS_OBJECT && Z_TYPE_P(obj2) == IS_OBJECT) { 49 | if (instanceof_function(Z_OBJCE_P(obj1), byte_buffer_ce) && instanceof_function(Z_OBJCE_P(obj2), byte_buffer_ce)) { 50 | auto object1 = fetch_from_zend_object(Z_OBJ_P(obj1)); 51 | auto object2 = fetch_from_zend_object(Z_OBJ_P(obj2)); 52 | 53 | if ( 54 | object1->read_offset == object2->read_offset && 55 | object1->write_offset == object2->write_offset && 56 | object1->used == object2->used && 57 | zend_string_equals(object1->buffer, object2->buffer) 58 | ) { 59 | return 0; 60 | } 61 | } 62 | } 63 | 64 | return 1; 65 | } 66 | 67 | #define BYTE_BUFFER_METHOD(name) PHP_METHOD(pmmp_encoding_ByteBuffer, name) 68 | 69 | BYTE_BUFFER_METHOD(__construct) { 70 | zend_string* buffer = NULL; 71 | byte_buffer_zend_object* object; 72 | 73 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1) 74 | Z_PARAM_OPTIONAL 75 | Z_PARAM_STR(buffer) 76 | ZEND_PARSE_PARAMETERS_END(); 77 | 78 | object = BYTE_BUFFER_THIS(); 79 | if (object->buffer) { 80 | zend_string_release_ex(object->buffer, 0); 81 | } 82 | 83 | if (buffer == NULL) { 84 | buffer = zend_empty_string; 85 | } 86 | //read offset is placed at the start, and write offset at the end (to mirror PM BinaryStream behaviour) 87 | byte_buffer_init_properties(object, buffer, ZSTR_LEN(buffer), 0, ZSTR_LEN(buffer)); 88 | } 89 | 90 | BYTE_BUFFER_METHOD(toString) { 91 | zend_parse_parameters_none_throw(); 92 | 93 | auto object = BYTE_BUFFER_THIS(); 94 | RETURN_STRINGL(ZSTR_VAL(object->buffer), object->used); 95 | } 96 | 97 | BYTE_BUFFER_METHOD(readByteArray) { 98 | zend_long zlength; 99 | byte_buffer_zend_object* object; 100 | 101 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 102 | Z_PARAM_LONG(zlength) 103 | ZEND_PARSE_PARAMETERS_END(); 104 | 105 | if (zlength < 0) { 106 | zend_value_error("Length cannot be negative"); 107 | return; 108 | } 109 | if (zlength == 0) { //to mirror PM BinaryStream behaviour 110 | RETURN_STR(zend_empty_string); 111 | } 112 | 113 | size_t length = static_cast(zlength); 114 | 115 | object = BYTE_BUFFER_THIS(); 116 | 117 | if (object->used - object->read_offset < length) { 118 | zend_throw_exception_ex(data_decode_exception_ce, 0, "Need at least %zu bytes, but only have %zu bytes", length, object->used - object->read_offset); 119 | return; 120 | } 121 | 122 | RETVAL_STRINGL(ZSTR_VAL(object->buffer) + object->read_offset, length); 123 | object->read_offset += length; 124 | } 125 | 126 | BYTE_BUFFER_METHOD(writeByteArray) { 127 | zend_string* value; 128 | byte_buffer_zend_object* object; 129 | 130 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 131 | Z_PARAM_STR(value) 132 | ZEND_PARSE_PARAMETERS_END(); 133 | 134 | 135 | object = BYTE_BUFFER_THIS(); 136 | 137 | auto size = ZSTR_LEN(value); 138 | 139 | extendBuffer(object->buffer, object->write_offset, size); 140 | memcpy(ZSTR_VAL(object->buffer) + object->write_offset, ZSTR_VAL(value), size); 141 | object->write_offset += size; 142 | if (object->write_offset > object->used) { 143 | object->used = object->write_offset; 144 | } 145 | } 146 | 147 | #define OFFSET_METHODS(func_name, which_offset) \ 148 | BYTE_BUFFER_METHOD(get##func_name) { \ 149 | zend_parse_parameters_none_throw(); \ 150 | auto object = BYTE_BUFFER_THIS(); \ 151 | RETURN_LONG(object->which_offset); \ 152 | } \ 153 | BYTE_BUFFER_METHOD(set##func_name) { \ 154 | zend_long offset; \ 155 | \ 156 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) \ 157 | Z_PARAM_LONG(offset) \ 158 | ZEND_PARSE_PARAMETERS_END(); \ 159 | \ 160 | auto object = BYTE_BUFFER_THIS(); \ 161 | if (offset < 0 || static_cast(offset) > object->used) { \ 162 | zend_value_error("Offset must not be less than zero or greater than the buffer size"); \ 163 | return; \ 164 | } \ 165 | \ 166 | object->which_offset = static_cast(offset); \ 167 | } 168 | 169 | OFFSET_METHODS(ReadOffset, read_offset); 170 | OFFSET_METHODS(WriteOffset, write_offset); 171 | 172 | BYTE_BUFFER_METHOD(getUsedLength) { 173 | zend_parse_parameters_none_throw(); 174 | 175 | auto object = BYTE_BUFFER_THIS(); 176 | RETURN_LONG(object->used); 177 | } 178 | 179 | BYTE_BUFFER_METHOD(getReservedLength) { 180 | zend_parse_parameters_none_throw(); 181 | 182 | auto object = BYTE_BUFFER_THIS(); 183 | RETURN_LONG(ZSTR_LEN(object->buffer)); 184 | } 185 | 186 | BYTE_BUFFER_METHOD(reserve) { 187 | zend_long zlength; 188 | 189 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 190 | Z_PARAM_LONG(zlength) 191 | ZEND_PARSE_PARAMETERS_END(); 192 | 193 | if (zlength <= 0) { 194 | zend_value_error("Length must be greater than zero"); 195 | return; 196 | } 197 | auto object = BYTE_BUFFER_THIS(); 198 | extendBuffer(object->buffer, static_cast(zlength), 0); 199 | } 200 | 201 | BYTE_BUFFER_METHOD(trim) { 202 | zend_parse_parameters_none_throw(); 203 | 204 | auto object = BYTE_BUFFER_THIS(); 205 | if (ZSTR_LEN(object->buffer) > object->used) { 206 | object->buffer = zend_string_truncate(object->buffer, object->used, 0); 207 | } 208 | } 209 | 210 | BYTE_BUFFER_METHOD(clear) { 211 | zend_parse_parameters_none_throw(); 212 | 213 | auto object = BYTE_BUFFER_THIS(); 214 | object->read_offset = 0; 215 | object->write_offset = 0; 216 | object->used = 0; 217 | } 218 | 219 | BYTE_BUFFER_METHOD(__serialize) { 220 | zend_parse_parameters_none_throw(); 221 | 222 | auto object = BYTE_BUFFER_THIS(); 223 | array_init(return_value); 224 | add_assoc_stringl(return_value, "buffer", ZSTR_VAL(object->buffer), object->used); 225 | add_assoc_long(return_value, "read_offset", object->read_offset); 226 | add_assoc_long(return_value, "write_offset", object->write_offset); 227 | } 228 | 229 | static zval* fetch_serialized_property(HashTable* data, const char* name, int type) { 230 | zval* zv = zend_hash_str_find(data, name, strlen(name)); 231 | if (zv == NULL) { 232 | zend_throw_exception_ex(NULL, 0, "Serialized data is missing \"%s\"", name); 233 | return NULL; 234 | } 235 | if (Z_TYPE_P(zv) != type) { 236 | 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)); 237 | return NULL; 238 | } 239 | 240 | return zv; 241 | } 242 | 243 | BYTE_BUFFER_METHOD(__unserialize) { 244 | HashTable* data; 245 | 246 | ZEND_PARSE_PARAMETERS_START(1, 1) 247 | Z_PARAM_ARRAY_HT(data) 248 | ZEND_PARSE_PARAMETERS_END(); 249 | 250 | zval* buffer = fetch_serialized_property(data, "buffer", IS_STRING); 251 | if (buffer == NULL) { 252 | return; 253 | } 254 | zval* read_offset = fetch_serialized_property(data, "read_offset", IS_LONG); 255 | if (read_offset == NULL) { 256 | return; 257 | } 258 | zval* write_offset = fetch_serialized_property(data, "write_offset", IS_LONG); 259 | if (write_offset == NULL) { 260 | return; 261 | } 262 | 263 | auto object = BYTE_BUFFER_THIS(); 264 | 265 | byte_buffer_init_properties(object, Z_STR_P(buffer), Z_STRLEN_P(buffer), Z_LVAL_P(read_offset), Z_LVAL_P(write_offset)); 266 | } 267 | 268 | BYTE_BUFFER_METHOD(__debugInfo) { 269 | zend_parse_parameters_none_throw(); 270 | 271 | auto object = BYTE_BUFFER_THIS(); 272 | array_init(return_value); 273 | add_assoc_stringl(return_value, "buffer", ZSTR_VAL(object->buffer), object->used); 274 | add_assoc_long(return_value, "read_offset", object->read_offset); 275 | add_assoc_long(return_value, "write_offset", object->write_offset); 276 | } 277 | 278 | zend_class_entry* init_class_ByteBuffer(void) { 279 | byte_buffer_ce = register_class_pmmp_encoding_ByteBuffer(); 280 | byte_buffer_ce->create_object = byte_buffer_new; 281 | 282 | byte_buffer_zend_object_handlers = *zend_get_std_object_handlers(); 283 | byte_buffer_zend_object_handlers.offset = XtOffsetOf(byte_buffer_zend_object, std); 284 | byte_buffer_zend_object_handlers.clone_obj = byte_buffer_clone; 285 | byte_buffer_zend_object_handlers.free_obj = byte_buffer_free; 286 | byte_buffer_zend_object_handlers.compare = byte_buffer_compare_objects; 287 | 288 | return byte_buffer_ce; 289 | } 290 | -------------------------------------------------------------------------------- /classes/ByteBuffer.h: -------------------------------------------------------------------------------- 1 | #ifndef BYTE_BUFFER_H 2 | #define BYTE_BUFFER_H 3 | 4 | extern "C" { 5 | #include "php.h" 6 | } 7 | #include "../ZendUtil.h" 8 | 9 | typedef struct _byte_buffer_zend_object { 10 | zend_string* buffer; 11 | size_t read_offset; 12 | size_t write_offset; 13 | size_t used; 14 | zend_object std; 15 | } byte_buffer_zend_object; 16 | 17 | #define BYTE_BUFFER_FROM_ZVAL(zv) fetch_from_zend_object(Z_OBJ_P(zv)) 18 | #define BYTE_BUFFER_THIS() BYTE_BUFFER_FROM_ZVAL(ZEND_THIS) 19 | 20 | extern zend_class_entry* byte_buffer_ce; 21 | 22 | zend_class_entry* init_class_ByteBuffer(void); 23 | #endif 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /classes/Types.cpp: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include "php.h" 3 | } 4 | #include "ByteBuffer.h" 5 | #include "DataDecodeException.h" 6 | #include "../Serializers.h" 7 | 8 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_read_integer, 0, 1, IS_LONG, 0) 9 | ZEND_ARG_OBJ_INFO(0, buffer, pmmp\\encoding\\ByteBuffer, 0) 10 | ZEND_END_ARG_INFO() 11 | 12 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_write_integer, 0, 2, IS_VOID, 0) 13 | ZEND_ARG_OBJ_INFO(0, buffer, pmmp\\encoding\\ByteBuffer, 0) 14 | ZEND_ARG_TYPE_INFO(0, value, IS_LONG, 0) 15 | ZEND_END_ARG_INFO() 16 | 17 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_read_float, 0, 1, IS_DOUBLE, 0) 18 | ZEND_ARG_OBJ_INFO(0, buffer, pmmp\\encoding\\ByteBuffer, 0) 19 | ZEND_END_ARG_INFO() 20 | 21 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_write_float, 0, 2, IS_VOID, 0) 22 | ZEND_ARG_OBJ_INFO(0, buffer, pmmp\\encoding\\ByteBuffer, 0) 23 | ZEND_ARG_TYPE_INFO(0, value, IS_DOUBLE, 0) 24 | ZEND_END_ARG_INFO() 25 | 26 | template 27 | static inline void zval_long_wrapper(zval* zv, TValue value) { 28 | ZVAL_LONG(zv, value); 29 | } 30 | 31 | template 32 | static inline void zval_double_wrapper(zval* zv, TValue value) { 33 | ZVAL_DOUBLE(zv, value); 34 | } 35 | 36 | template 37 | void ZEND_FASTCALL zif_readType(INTERNAL_FUNCTION_PARAMETERS) { 38 | zval* object_zv; 39 | byte_buffer_zend_object* object; 40 | 41 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1) 42 | Z_PARAM_OBJECT_OF_CLASS_EX(object_zv, byte_buffer_ce, 0, 0) 43 | ZEND_PARSE_PARAMETERS_END_EX(return); 44 | 45 | object = BYTE_BUFFER_FROM_ZVAL(object_zv); 46 | 47 | TValue result; 48 | auto bytes = reinterpret_cast(ZSTR_VAL(object->buffer)); 49 | if (readTypeFunc(bytes, object->used, object->read_offset, result)) { 50 | assignResult(return_value, result); 51 | } 52 | } 53 | 54 | 55 | template 56 | static bool zend_parse_parameters_long_wrapper(zend_execute_data* execute_data, byte_buffer_zend_object*& object, TValue& value) { 57 | zval* object_zv; 58 | zend_long actualValue; 59 | 60 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2) 61 | Z_PARAM_OBJECT_OF_CLASS_EX(object_zv, byte_buffer_ce, 0, 0) 62 | Z_PARAM_LONG(actualValue) 63 | ZEND_PARSE_PARAMETERS_END_EX(return false); 64 | 65 | object = BYTE_BUFFER_FROM_ZVAL(object_zv); 66 | value = static_cast(actualValue); 67 | 68 | return true; 69 | } 70 | 71 | template 72 | static bool zend_parse_parameters_double_wrapper(zend_execute_data* execute_data, byte_buffer_zend_object*& object, TValue& value) { 73 | zval* object_zv; 74 | double actualValue; 75 | 76 | ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2) 77 | Z_PARAM_OBJECT_OF_CLASS_EX(object_zv, byte_buffer_ce, 0, 0) 78 | Z_PARAM_DOUBLE(actualValue) 79 | ZEND_PARSE_PARAMETERS_END_EX(return false); 80 | 81 | object = BYTE_BUFFER_FROM_ZVAL(object_zv); 82 | value = static_cast(actualValue); 83 | 84 | return true; 85 | } 86 | 87 | template 88 | using parseParametersFunc_t = bool (*)(zend_execute_data* execute_data, byte_buffer_zend_object*& object, TValue& value); 89 | 90 | template 91 | using writeTypeFunc_t = void (*)(zend_string*& buffer, size_t& offset, TValue value); 92 | 93 | template parseParametersFunc, writeTypeFunc_t writeTypeFunc> 94 | void ZEND_FASTCALL zif_writeType(INTERNAL_FUNCTION_PARAMETERS) { 95 | TValue value; 96 | byte_buffer_zend_object* object; 97 | 98 | //offsets beyond the end of the buffer are allowed, and result in automatic buffer extension 99 | if (!parseParametersFunc(execute_data, object, value)) { 100 | return; 101 | } 102 | 103 | writeTypeFunc(object->buffer, object->write_offset, value); 104 | if (object->write_offset > object->used) { 105 | object->used = object->write_offset; 106 | } 107 | } 108 | 109 | ZEND_NAMED_FUNCTION(pmmp_encoding_private_constructor) { 110 | //NOOP 111 | } 112 | 113 | #if PHP_VERSION_ID >= 80400 114 | #define BC_ZEND_RAW_FENTRY(zend_name, name, arg_info, flags) ZEND_RAW_FENTRY(zend_name, name, arg_info, flags, NULL, NULL) 115 | #else 116 | #define BC_ZEND_RAW_FENTRY(zend_name, name, arg_info, flags) ZEND_RAW_FENTRY(zend_name, name, arg_info, flags) 117 | #endif 118 | 119 | #define FIXED_TYPE_ENTRIES(zend_name, native_type, parse_parameters_wrapper, result_wrapper, arg_info_read, arg_info_write, byte_order) \ 120 | BC_ZEND_RAW_FENTRY("read" zend_name, (zif_readType), result_wrapper>), arg_info_read, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) \ 121 | BC_ZEND_RAW_FENTRY("write" zend_name, (zif_writeType, (writeFixedSizeType)>), arg_info_write, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) 122 | 123 | //triad can't used readFixedSizeType because it's not a power of 2 bytes 124 | #define TRIAD_ENTRIES(zend_name, native_type, byte_order) \ 125 | BC_ZEND_RAW_FENTRY("read" zend_name, (zif_readType, zval_long_wrapper>), arginfo_read_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) \ 126 | BC_ZEND_RAW_FENTRY("write" zend_name, (zif_writeType, writeInt24>), arginfo_write_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) 127 | 128 | #define LONG_ENTRIES(zend_name, unsigned_native, signed_native, byte_order) \ 129 | FIXED_TYPE_ENTRIES("Unsigned" zend_name, unsigned_native, zend_parse_parameters_long_wrapper, zval_long_wrapper, arginfo_read_integer, arginfo_write_integer, byte_order) \ 130 | FIXED_TYPE_ENTRIES("Signed" zend_name, signed_native, zend_parse_parameters_long_wrapper, zval_long_wrapper, arginfo_read_integer, arginfo_write_integer, byte_order) \ 131 | 132 | #define FLOAT_ENTRIES(zend_name, native_type, byte_order) \ 133 | FIXED_TYPE_ENTRIES(zend_name, native_type, zend_parse_parameters_double_wrapper, zval_double_wrapper, arginfo_read_float, arginfo_write_float, byte_order) 134 | 135 | #define READ_VARINT_FENTRY(size_name, unsigned_type, signed_type) \ 136 | BC_ZEND_RAW_FENTRY("readUnsigned" size_name, (zif_readType), zval_long_wrapper>), arginfo_read_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) \ 137 | BC_ZEND_RAW_FENTRY("readSigned" size_name, (zif_readType), zval_long_wrapper>), arginfo_read_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) 138 | 139 | #define WRITE_VARINT_FENTRY(size_name, unsigned_type, signed_type) \ 140 | BC_ZEND_RAW_FENTRY("writeUnsigned" size_name, (zif_writeType, (writeUnsignedVarInt)>), arginfo_write_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) \ 141 | BC_ZEND_RAW_FENTRY("writeSigned" size_name, (zif_writeType, (writeSignedVarInt)>), arginfo_write_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) 142 | 143 | #define READ_WRITE_VARINT_ENTRY(zend_name, unsigned_type, signed_type) \ 144 | READ_VARINT_FENTRY(zend_name, unsigned_type, signed_type) \ 145 | WRITE_VARINT_FENTRY(zend_name, unsigned_type, signed_type) 146 | 147 | #define READ_WRITE_BYTE_ENTRY(zend_name, native_type) \ 148 | BC_ZEND_RAW_FENTRY("read" zend_name, (zif_readType, zval_long_wrapper>), arginfo_read_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) \ 149 | BC_ZEND_RAW_FENTRY("write" zend_name, (zif_writeType, writeByte>), arginfo_write_integer, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) 150 | 151 | #define ENDIAN_ENTRIES(enum_case) \ 152 | LONG_ENTRIES("Short", uint16_t, int16_t, enum_case) \ 153 | LONG_ENTRIES("Int", uint32_t, int32_t, enum_case) \ 154 | \ 155 | FIXED_TYPE_ENTRIES("SignedLong", int64_t, zend_parse_parameters_long_wrapper, zval_long_wrapper, arginfo_read_integer, arginfo_write_integer, enum_case) \ 156 | \ 157 | FLOAT_ENTRIES("Float", float, enum_case) \ 158 | FLOAT_ENTRIES("Double", double, enum_case) \ 159 | \ 160 | TRIAD_ENTRIES("UnsignedTriad", uint32_t, enum_case) \ 161 | TRIAD_ENTRIES("SignedTriad", int32_t, enum_case) 162 | 163 | ZEND_BEGIN_ARG_INFO_EX(empty_constructor_arg_info, 0, 0, 0) 164 | ZEND_END_ARG_INFO() 165 | 166 | static zend_function_entry byte_methods[] = { 167 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 168 | 169 | READ_WRITE_BYTE_ENTRY("Unsigned", uint8_t) 170 | READ_WRITE_BYTE_ENTRY("Signed", int8_t) 171 | 172 | PHP_FE_END 173 | }; 174 | 175 | static zend_function_entry big_endian_methods[] = { 176 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 177 | 178 | ENDIAN_ENTRIES(ByteOrder::BigEndian) 179 | PHP_FE_END 180 | }; 181 | static zend_function_entry little_endian_methods[] = { 182 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 183 | 184 | ENDIAN_ENTRIES(ByteOrder::LittleEndian) 185 | PHP_FE_END 186 | }; 187 | 188 | static zend_function_entry varint_methods[] = { 189 | ZEND_NAMED_ME(__construct, pmmp_encoding_private_constructor, empty_constructor_arg_info, ZEND_ACC_PRIVATE) 190 | 191 | READ_WRITE_VARINT_ENTRY("Int", uint32_t, int32_t) 192 | READ_WRITE_VARINT_ENTRY("Long", uint64_t, int64_t) 193 | PHP_FE_END 194 | }; 195 | 196 | void init_class_Types(void) { 197 | zend_class_entry ce; 198 | 199 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "Byte", byte_methods); 200 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 201 | zend_register_internal_class(&ce); 202 | 203 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "BE", big_endian_methods); 204 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 205 | zend_register_internal_class(&ce); 206 | 207 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "LE", little_endian_methods); 208 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 209 | zend_register_internal_class(&ce); 210 | 211 | INIT_NS_CLASS_ENTRY(ce, "pmmp\\encoding", "VarInt", varint_methods); 212 | ce.ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; 213 | zend_register_internal_class(&ce); 214 | } 215 | -------------------------------------------------------------------------------- /classes/Types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | void init_class_Types(void); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /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/ByteBuffer.cpp classes/Types.cpp, $ext_shared,,-std=c++14 -Wall -Werror, yes) 12 | PHP_ADD_BUILD_DIR($ext_builddir/classes, 1) 13 | fi 14 | -------------------------------------------------------------------------------- /config.w32: -------------------------------------------------------------------------------- 1 | ARG_ENABLE('encoding', 'encoding support', 'no'); 2 | 3 | if (PHP_ENCODING != 'no') { 4 | AC_DEFINE('HAVE_ENCODING', 1, 'encoding support enabled'); 5 | 6 | EXTENSION('encoding', 'encoding.cpp', null, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 /permissive-'); 7 | ADD_SOURCES(configure_module_dirname + '/classes', 'ByteBuffer.cpp Types.cpp', 'encoding'); 8 | } 9 | -------------------------------------------------------------------------------- /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/ByteBuffer.h" 15 | #include "classes/Types.h" 16 | #include "classes/DataDecodeException.h" 17 | 18 | /* {{{ PHP_MINFO_FUNCTION */ 19 | PHP_MINFO_FUNCTION(encoding) 20 | { 21 | php_info_print_table_start(); 22 | php_info_print_table_header(2, "Version", PHP_ENCODING_VERSION); 23 | php_info_print_table_header(2, "Experimental", "YES"); 24 | php_info_print_table_end(); 25 | } 26 | /* }}} */ 27 | 28 | /* {{{ PHP_RINIT_FUNCTION 29 | */ 30 | PHP_RINIT_FUNCTION(encoding) 31 | { 32 | #if defined(ZTS) && defined(COMPILE_DL_ENCODING) 33 | ZEND_TSRMLS_CACHE_UPDATE(); 34 | #endif 35 | 36 | return SUCCESS; 37 | } 38 | /* }}} */ 39 | 40 | zend_class_entry* data_decode_exception_ce; 41 | 42 | PHP_MINIT_FUNCTION(encoding) { 43 | data_decode_exception_ce = register_class_pmmp_encoding_DataDecodeException(spl_ce_RuntimeException); 44 | init_class_ByteBuffer(); 45 | init_class_Types(); 46 | 47 | return SUCCESS; 48 | } 49 | 50 | static const zend_module_dep module_dependencies[] = { 51 | ZEND_MOD_REQUIRED("spl") 52 | ZEND_MOD_END 53 | }; 54 | 55 | /* {{{ encoding_module_entry */ 56 | zend_module_entry encoding_module_entry = { 57 | STANDARD_MODULE_HEADER_EX, 58 | NULL, /* ini_entries */ 59 | module_dependencies, 60 | "encoding", /* Extension name */ 61 | NULL, /* zend_function_entry */ 62 | PHP_MINIT(encoding), /* PHP_MINIT - Module initialization */ 63 | NULL, /* PHP_MSHUTDOWN - Module shutdown */ 64 | PHP_RINIT(encoding), /* PHP_RINIT - Request initialization */ 65 | NULL, /* PHP_RSHUTDOWN - Request shutdown */ 66 | PHP_MINFO(encoding), /* PHP_MINFO - Module info */ 67 | PHP_ENCODING_VERSION, /* Version */ 68 | STANDARD_MODULE_PROPERTIES 69 | }; 70 | /* }}} */ 71 | 72 | #ifdef COMPILE_DL_ENCODING 73 | # ifdef ZTS 74 | ZEND_TSRMLS_CACHE_DEFINE() 75 | # endif 76 | ZEND_GET_MODULE(encoding) 77 | #endif 78 | -------------------------------------------------------------------------------- /php_encoding.h: -------------------------------------------------------------------------------- 1 | /* encoding extension for PHP */ 2 | 3 | #ifndef PHP_ENCODING_H 4 | # define PHP_ENCODING_H 5 | 6 | extern zend_module_entry encoding_module_entry; 7 | # define phpext_encoding_ptr &encoding_module_entry 8 | 9 | # define PHP_ENCODING_VERSION "0.4.1-dev" 10 | 11 | # if defined(ZTS) && defined(COMPILE_DL_ENCODING) 12 | ZEND_TSRMLS_CACHE_EXTERN() 13 | # endif 14 | 15 | #endif /* PHP_ENCODING_H */ 16 | -------------------------------------------------------------------------------- /stubs/ByteBuffer.stub.php: -------------------------------------------------------------------------------- 1 | ce_flags |= ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES; 98 | 99 | return class_entry; 100 | } 101 | -------------------------------------------------------------------------------- /stubs/DataDecodeException.stub.php: -------------------------------------------------------------------------------- 1 | ce_flags |= ZEND_ACC_FINAL; 18 | 19 | return class_entry; 20 | } 21 | -------------------------------------------------------------------------------- /tests/buffer-clone.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that cloning ByteBuffer 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 | toString()); 16 | var_dump($buffer2->toString()); 17 | 18 | ?> 19 | --EXPECTF-- 20 | object(pmmp\encoding\ByteBuffer)#%d (3) { 21 | ["buffer"]=> 22 | string(9) "Some Data" 23 | ["read_offset"]=> 24 | int(0) 25 | ["write_offset"]=> 26 | int(9) 27 | } 28 | object(pmmp\encoding\ByteBuffer)#%d (3) { 29 | ["buffer"]=> 30 | string(9) "Some Data" 31 | ["read_offset"]=> 32 | int(0) 33 | ["write_offset"]=> 34 | int(9) 35 | } 36 | string(9) "Some Data" 37 | string(9) "Some Data" 38 | -------------------------------------------------------------------------------- /tests/buffer-serialize-unserialize.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer serializes and unserializes correctly 3 | --FILE-- 4 | 14 | --EXPECTF-- 15 | object(pmmp\encoding\ByteBuffer)#%d (3) { 16 | ["buffer"]=> 17 | string(11) "hello world" 18 | ["read_offset"]=> 19 | int(0) 20 | ["write_offset"]=> 21 | int(11) 22 | } 23 | object(pmmp\encoding\ByteBuffer)#%d (3) { 24 | ["buffer"]=> 25 | string(11) "hello world" 26 | ["read_offset"]=> 27 | int(0) 28 | ["write_offset"]=> 29 | int(11) 30 | } 31 | -------------------------------------------------------------------------------- /tests/buffer-to-string.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer::toString() doesn't show unused bytes in reserved space 3 | --DESCRIPTION-- 4 | ByteBuffer may allocate more bytes than it needs in order to minimize allocations. 5 | --FILE-- 6 | toString())); 17 | 18 | $buffer = new ByteBuffer("aaaaaaaaaa"); 19 | var_dump($buffer->toString()); 20 | ?> 21 | --EXPECT-- 22 | string(10) "0000000000" 23 | string(10) "aaaaaaaaaa" 24 | -------------------------------------------------------------------------------- /tests/buffer-var-dump.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer::__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\ByteBuffer)#%d (3) { 18 | ["buffer"]=> 19 | string(22) "looooooooooooong short" 20 | ["read_offset"]=> 21 | int(0) 22 | ["write_offset"]=> 23 | int(22) 24 | } 25 | -------------------------------------------------------------------------------- /tests/byte-buffer-new.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that new ByteBuffer() works correctly 3 | --FILE-- 4 | toString()); 10 | 11 | $buffer = new ByteBuffer(); 12 | var_dump($buffer->toString()); 13 | 14 | $buffer = new ByteBuffer("hello world"); 15 | $buffer->setReadOffset(6); 16 | var_dump($buffer->readByteArray(5)); 17 | ?> 18 | --EXPECT-- 19 | string(3) "abc" 20 | string(0) "" 21 | string(5) "world" 22 | -------------------------------------------------------------------------------- /tests/fixed-size-types.inc: -------------------------------------------------------------------------------- 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 | setReadOffset(0); 29 | var_dump(VarInt::readUnsignedInt($buffer)); 30 | 31 | $buffer->setWriteOffset(0); 32 | VarInt::writeUnsignedLong($buffer, -1); 33 | $buffer->setReadOffset(0); 34 | var_dump(VarInt::readUnsignedLong($buffer)); 35 | 36 | ?> 37 | --EXPECT-- 38 | int(4294967295) 39 | int(-1) 40 | 41 | -------------------------------------------------------------------------------- /tests/not-enough-bytes-fixed-size.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 ByteBuffer($test); 26 | $buffer->setReadOffset(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/not-enough-bytes-varint.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 ByteBuffer("\x00\x00\x00\x00\x80"); 21 | try{ 22 | $buffer->setReadOffset(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/pack-unpack-parity.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer read/write methods behave the same as their pack/unpack equivalents 3 | --FILE-- 4 | [Byte::readUnsigned(...), Byte::writeUnsigned(...), [0, 127, 128, 255]], 13 | "c" => [Byte::readSigned(...), Byte::writeSigned(...), [-128, -1, 0, 1, 127]], 14 | 15 | //signed short doesn't have any pack() equivalent 16 | "n" => [BE::readUnsignedShort(...), BE::writeUnsignedShort(...), [0, 1, 32767, 32768, 65535]], 17 | "v" => [LE::readUnsignedShort(...), LE::writeUnsignedShort(...), [0, 1, 32767, 32768, 65535]], 18 | 19 | //signed long doesn't have any pack() equivalent 20 | "N" => [BE::readUnsignedInt(...), BE::writeUnsignedInt(...), [0, 1, 2147483647, 2147483648, 4294967295]], 21 | "V" => [LE::readUnsignedInt(...), LE::writeUnsignedInt(...), [0, 1, 2147483647, 2147483648, 4294967295]], 22 | 23 | //these codes are supposed to be unsigned int64, but there's no such thing in PHP 24 | //the negative bounds must be written weirdly here due to weirdness in PHP parser 25 | "J" => [BE::readSignedLong(...), BE::writeSignedLong(...), [-9223372036854775807-1, -1, 0, 1, 9223372036854775807]], 26 | "P" => [LE::readSignedLong(...), LE::writeSignedLong(...), [-9223372036854775807-1, -1, 0, 1, 9223372036854775807]], 27 | 28 | "G" => [BE::readFloat(...), BE::writeFloat(...), [-1.0, 0.0, 1.0]], 29 | "g" => [LE::readFloat(...), LE::writeFloat(...), [-1.0, 0.0, 1.0]], 30 | 31 | "E" => [BE::readDouble(...), BE::writeDouble(...), [-1.0, 0.0, 1.0]], 32 | "e" => [LE::readDouble(...), LE::writeDouble(...), [-1.0, 0.0, 1.0]], 33 | ]; 34 | 35 | foreach($map as $packCode => [$readFunc, $writeFunc, $testValues]){ 36 | echo "--- Testing equivalents for pack code \"$packCode\" with " . count($testValues) . " samples ---\n"; 37 | foreach($testValues as $value){ 38 | $expectedBytes = pack($packCode, $value); 39 | 40 | $buffer = new ByteBuffer(); 41 | $writeFunc($buffer, $value); 42 | 43 | if($expectedBytes !== $buffer->toString()){ 44 | echo "Mismatch \"$packCode\" write: " . bin2hex($expectedBytes) . " " . bin2hex($buffer->toString()) . "\n"; 45 | } 46 | 47 | $buffer = new ByteBuffer($expectedBytes); 48 | $decodedValue = $readFunc($buffer); 49 | 50 | if($value !== $decodedValue){ 51 | echo "Mismatch \"$packCode\" read: " . $value . " " . $decodedValue . "\n"; 52 | } 53 | } 54 | } 55 | ?> 56 | --EXPECT-- 57 | --- Testing equivalents for pack code "C" with 4 samples --- 58 | --- Testing equivalents for pack code "c" with 5 samples --- 59 | --- Testing equivalents for pack code "n" with 5 samples --- 60 | --- Testing equivalents for pack code "v" with 5 samples --- 61 | --- Testing equivalents for pack code "N" with 5 samples --- 62 | --- Testing equivalents for pack code "V" with 5 samples --- 63 | --- Testing equivalents for pack code "J" with 5 samples --- 64 | --- Testing equivalents for pack code "P" with 5 samples --- 65 | --- Testing equivalents for pack code "G" with 3 samples --- 66 | --- Testing equivalents for pack code "g" with 3 samples --- 67 | --- Testing equivalents for pack code "E" with 3 samples --- 68 | --- Testing equivalents for pack code "e" with 3 samples --- 69 | -------------------------------------------------------------------------------- /tests/read-byte-array.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer::readByteArray() works as expected 3 | --FILE-- 4 | readByteArray(1); 14 | }catch(DataDecodeException $e){ 15 | echo $e->getMessage() . PHP_EOL; 16 | } 17 | 18 | $buffer = new ByteBuffer("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 ByteBuffer("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->setReadOffset(1); 36 | var_dump($buffer->readByteArray(2)); 37 | var_dump($buffer->getReadOffset()); 38 | 39 | //read with bytes, but all before the buffer start 40 | $buffer->setReadOffset(5); 41 | try{ 42 | $buffer->readByteArray(2); 43 | }catch(DataDecodeException $e){ 44 | echo $e->getMessage() . PHP_EOL; 45 | } 46 | --EXPECT-- 47 | Need at least 1 bytes, but only have 0 bytes 48 | string(3) "abc" 49 | string(1) "d" 50 | Need at least 2 bytes, but only have 1 bytes 51 | Length cannot be negative 52 | string(2) "bc" 53 | int(3) 54 | Need at least 2 bytes, but only have 0 bytes 55 | -------------------------------------------------------------------------------- /tests/read-reserved-memory.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer::read*() can't read reserved unused memory 3 | --DESCRIPTION-- 4 | Since we may have allocated more memory than we're currently using, we need to make sure that said extra memory isn't readable, otherwise we will return 5 | uninitialized values to PHP. 6 | --FILE-- 7 | reserve(100); 15 | 16 | try{ 17 | var_dump($buffer->readByteArray(10)); 18 | }catch(DataDecodeException $e){ 19 | echo $e->getMessage() . PHP_EOL; 20 | } 21 | 22 | try{ 23 | var_dump(Byte::readUnsigned($buffer)); 24 | }catch(DataDecodeException $e){ 25 | echo $e->getMessage() . PHP_EOL; 26 | } 27 | 28 | ?> 29 | --EXPECT-- 30 | Need at least 10 bytes, but only have 0 bytes 31 | Need at least 1 bytes, but only have 0 bytes 32 | -------------------------------------------------------------------------------- /tests/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 | setReadOffset(0); 17 | var_dump(LE::readSignedTriad($buffer)); 18 | 19 | $buffer->setReadOffset(0); 20 | var_dump(BE::readUnsignedTriad($buffer)); 21 | 22 | $buffer->setReadOffset(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/reserve.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that reserving works correctly 3 | --FILE-- 4 | getReservedLength()); //none 11 | 12 | $buffer->reserve(40); 13 | var_dump($buffer->getReservedLength()); //40 14 | var_dump($buffer->toString()); //still empty, we haven't used any space 15 | 16 | Byte::writeSigned($buffer, ord("a")); 17 | var_dump($buffer->getReservedLength()); //40 18 | var_dump($buffer->toString()); 19 | 20 | $buffer->writeByteArray(str_repeat("a", 40)); //cause new allocation, this should double the buffer size to 80 21 | var_dump($buffer->getReservedLength()); //80 22 | var_dump($buffer->toString()); 23 | 24 | try{ 25 | $buffer->reserve(-1); 26 | }catch(\ValueError $e){ 27 | echo $e->getMessage() . PHP_EOL; 28 | } 29 | ?> 30 | --EXPECT-- 31 | int(0) 32 | int(40) 33 | string(0) "" 34 | int(40) 35 | string(1) "a" 36 | int(80) 37 | string(41) "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 38 | Length must be greater than zero 39 | -------------------------------------------------------------------------------- /tests/set-write-offset.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer::setWriteOffset() works as expected 3 | --FILE-- 4 | setWriteOffset(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->setWriteOffset(4); 14 | $buffer->writeByteArray("bbbb"); 15 | 16 | var_dump($buffer->toString()); 17 | 18 | $buffer->setWriteOffset(6); 19 | $buffer->writeByteArray("cccc"); 20 | 21 | var_dump($buffer->toString()); 22 | 23 | try{ 24 | $buffer->setWriteOffset(-1); 25 | }catch(\ValueError $e){ 26 | echo $e->getMessage() . PHP_EOL; 27 | } 28 | 29 | try{ 30 | $buffer->setWriteOffset(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 | -------------------------------------------------------------------------------- /tests/too-many-bytes-varint.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 ByteBuffer($shortBuf)); 31 | }catch(DataDecodeException $e){ 32 | echo "sv32: " . $e->getMessage() . PHP_EOL; 33 | } 34 | 35 | try{ 36 | VarInt::readUnsignedLong(new ByteBuffer($longBuf)); 37 | }catch(DataDecodeException $e){ 38 | echo "uv64: " . $e->getMessage() . PHP_EOL; 39 | } 40 | 41 | try{ 42 | VarInt::readSignedLong(new ByteBuffer($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 | -------------------------------------------------------------------------------- /tests/trim.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer::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 ByteBuffer("aaaaaaaaaa"); 16 | $buffer->trim(); 17 | var_dump($buffer->getReservedLength()); 18 | ?> 19 | --EXPECT-- 20 | int(100) 21 | int(5) 22 | int(10) 23 | -------------------------------------------------------------------------------- /tests/update-offset-fixed-size.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | read*() for fixed-size type must correctly update the reference $offset parameter if given 3 | --EXTENSIONS-- 4 | encoding 5 | --FILE-- 6 | setReadOffset($originalOffset); 16 | 17 | $function($buffer); 18 | var_dump($buffer->getReadOffset() === $originalOffset + $size); 19 | } 20 | 21 | $functions = require __DIR__ . '/fixed-size-types.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/update-offset-varint.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | read(Un)SignedVar(Int|Long)() must correctly update the internal offset 3 | --EXTENSIONS-- 4 | encoding 5 | --FILE-- 6 | setReadOffset($originalOffset); 16 | 17 | $function($buffer); 18 | var_dump($buffer->getReadOffset() === $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/write-byte-array.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test that ByteBuffer::writeByteArray() works as expected 3 | --FILE-- 4 | writeByteArray("12345"); 10 | 11 | var_dump($buffer->getWriteOffset()); 12 | var_dump($buffer->toString()); 13 | 14 | $buffer->writeByteArray("67"); 15 | 16 | var_dump($buffer->getWriteOffset()); 17 | var_dump($buffer->toString()); 18 | 19 | $buffer->setWriteOffset(2); 20 | $buffer->writeByteArray("890"); 21 | var_dump($buffer->getWriteOffset()); 22 | var_dump($buffer->toString()); 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 | -------------------------------------------------------------------------------- /tests/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 | toString() === "\xff\x00\x00"); 15 | 16 | $buffer = new ByteBuffer(); 17 | LE::writeSignedTriad($buffer, -65536); 18 | var_dump($buffer->toString() === "\x00\x00\xff"); 19 | 20 | $buffer = new ByteBuffer(); 21 | BE::writeUnsignedTriad($buffer, -65536); 22 | var_dump($buffer->toString() === "\xff\x00\x00"); 23 | 24 | $buffer = new ByteBuffer(); 25 | LE::writeUnsignedTriad($buffer, -65536); 26 | var_dump($buffer->toString() === "\x00\x00\xff"); 27 | 28 | ?> 29 | --EXPECT-- 30 | bool(true) 31 | bool(true) 32 | bool(true) 33 | bool(true) 34 | --------------------------------------------------------------------------------