├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── Zerg │ ├── DataSet.php │ ├── Field │ ├── AbstractField.php │ ├── Arr.php │ ├── AssertException.php │ ├── Collection.php │ ├── Conditional.php │ ├── ConfigurationException.php │ ├── Enum.php │ ├── Factory.php │ ├── Int.php │ ├── InvalidKeyException.php │ ├── Padding.php │ ├── Scalar.php │ └── String.php │ └── Stream │ ├── AbstractStream.php │ ├── FileStream.php │ └── StringStream.php └── tests ├── Zerg ├── DataSetTest.php ├── Field │ ├── ArrTest.php │ ├── CollectionTest.php │ ├── ConditionalTest.php │ ├── EnumTest.php │ ├── FactoryTest.php │ ├── IntTest.php │ ├── PaddingTest.php │ ├── ScalarTest.php │ ├── SizeableTest.php │ └── StringTest.php └── Stream │ ├── FileStreamTest.php │ └── StringStreamTest.php ├── bootstrap.php └── data /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.phar 3 | *.lock 4 | vendor -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - php 3 | 4 | filter: 5 | paths: [src/*] 6 | 7 | tools: 8 | external_code_coverage: 9 | enabled: true 10 | timeout: 1800 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | 9 | install: 10 | - composer install 11 | 12 | script: 13 | - phpunit --coverage-clover=clover.xml 14 | 15 | after_script: 16 | - wget https://scrutinizer-ci.com/ocular.phar 17 | - php ocular.phar code-coverage:upload --format=php-clover clover.xml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Павел 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zerg [![Build Status](https://travis-ci.org/klermonte/zerg.svg)](https://travis-ci.org/klermonte/zerg) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/klermonte/zerg/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/klermonte/zerg/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/klermonte/zerg/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/klermonte/zerg/?branch=master) 2 | ==== 3 | 4 | Zerg is a small PHP tool that allow you simply parse structured binary files like lsdj memory dump file, jpeg encoded image or your custom binary format file. 5 | 6 | ## Introdution 7 | If you are reading this, chances are you know exactly why you need to read binary files in PHP. So I will not explain to you that this is not a good idea. Nevertheless, I like you needed to do this is in PHP. That's why I create zerg project. During creation, I was inspired by following projects: [alexras/bread](https://github.com/alexras/bread) and [themainframe/php-binary](https://github.com/themainframe/php-binary). 8 | 9 | ## Installation 10 | `composer require klermonte/zerg dev-master` 11 | Or add `"klermonte/zerg": "dev-master"` to your dependancy list in composer.json and run `composer update` 12 | 13 | ## Usage 14 | ```php 15 | // Describe your binary format in zerg language 16 | $fieldCollection = new \Zerg\Field\Collection([ 17 | 'stringValue' => ['string', 15], 18 | 'intValue' => ['arr', 5, ['int', 8]], 19 | 'enumValue' => ['enum', 8, [ 20 | 0 => 'zero', 21 | 10 => 'ten', 22 | 32 => 'many' 23 | ], ['default' => 'not found'] 24 | ] 25 | ]); 26 | 27 | // Wrap your data in one of zerg streams 28 | $sourceStream = new \Zerg\Stream\StringStream("Hello from zerg123456"); 29 | 30 | //Get your data structure 31 | $data = $fieldCollection->parse($sourceStream); 32 | print_r($data); 33 | /* 34 | Array 35 | ( 36 | [stringValue] => Hello from zerg 37 | [intValue] => Array 38 | ( 39 | [0] => 49 40 | [1] => 50 41 | [2] => 51 42 | [3] => 52 43 | [4] => 53 44 | ) 45 | 46 | [enumValue] => not found 47 | ) 48 | */ 49 | ``` 50 | 51 | ## Field types 52 | ### Integer 53 | ```php 54 | // Object notation 55 | // -------------------------------------- 56 | // $field = new Int(, ); 57 | 58 | $field = new Int(4); 59 | $field = new Int('byte', [ 60 | 'signed' => true, 61 | 'formatter' => function($value) { 62 | return $value * 100; 63 | } 64 | ]); 65 | 66 | // Array notation 67 | // -------------------------------------- 68 | // $fieldArray = ['int', , ]; 69 | ``` 70 | Avaliable options 71 | 72 | Option name | Avaliable values | Description 73 | ------------|-------------|------------- 74 | signed | `boolean`, default `false` | Whether field value is signed or not 75 | endian | `PhpBio\Endian::ENDIAN_BIG` or
`PhpBio\Endian::ENDIAN_LITTLE` | Endianess of field 76 | formatter | `callable` | callback, that take 2 arguments:
`function ($parsedValue, $dataSetInstance) {...}` 77 | 78 | ### String 79 | ```php 80 | // Object notation 81 | // -------------------------------------- 82 | // $field = new String(, ); 83 | 84 | $field = new String(16); 85 | $field = new String('short', [ 86 | 'endian' => PhpBio\Endian::ENDIAN_BIG, 87 | 'formatter' => function($value) { 88 | return str_repeat($value, 2); 89 | } 90 | ]); 91 | 92 | // Array notation 93 | // -------------------------------------- 94 | // $fieldArray = ['string', , ]; 95 | ``` 96 | Avaliable options 97 | 98 | Option name | Avaliable values | Description 99 | ------------|-------------|------------- 100 | endian | `PhpBio\Endian::ENDIAN_BIG` or
`PhpBio\Endian::ENDIAN_LITTLE` | Endianess of field 101 | formatter | `callable` | callback, that take 2 arguments:
`function ($parsedValue, DataSet $dataSet) {...}` 102 | 103 | ### Padding 104 | ```php 105 | // Object notation 106 | // -------------------------------------- 107 | // $field = new Padding(); 108 | 109 | $field = new Padding(16); 110 | 111 | // Array notation 112 | // -------------------------------------- 113 | // $fieldArray = ['padding', ]; 114 | ``` 115 | 116 | ### Enum 117 | ```php 118 | // Object notation 119 | // -------------------------------------- 120 | // $field = new Enum(, , ); 121 | 122 | $field = new Enum(8, [0, 1, 2, 3]); 123 | $field = new Enum('short', [ 124 | 1234 => 'qwerty1', 125 | 2345 => 'qwerty2' 126 | ], [ 127 | 'default' => 'abcdef' 128 | ] 129 | ); 130 | 131 | // Array notation 132 | // -------------------------------------- 133 | // $fieldArray = ['enum', , ]; 134 | ``` 135 | Avaliable options 136 | 137 | Option name | Avaliable values | Description 138 | ------------|-------------|------------- 139 | default | `mixed`, optional | Value, that will be returned, if no one key from `values` matchs to parsed value 140 | 141 | And all options from **Integer** field type. 142 | 143 | ### Conditional 144 | ```php 145 | // Object notation 146 | // -------------------------------------- 147 | // $field = new Conditional(, , ); 148 | 149 | $field = new Conditional('/path/to/key/value', [ 150 | 1 => ['int', 32], 151 | 2 => ['string', 32] 152 | ], [ 153 | 'default' => ['padding', 32] 154 | ] 155 | ); 156 | 157 | // Array notation 158 | // -------------------------------------- 159 | // $fieldArray = ['conditional', , ]; 160 | ``` 161 | Avaliable options 162 | 163 | Option name | Avaliable values | Description 164 | ------------|-------------|------------- 165 | default | `array`, optional | Field in array notation, that will be used, if no one key from `field` matchs to parsed value 166 | 167 | ### Array 168 | ```php 169 | // Object notation 170 | // -------------------------------------- 171 | // $field = new Arr(, , ); 172 | 173 | $field = new Arr(10, ['int', 32]); 174 | 175 | // Array notation 176 | // -------------------------------------- 177 | // $fieldArray = ['arr', , ]; 178 | ``` 179 | Avaliable options 180 | 181 | Option name | Avaliable values | Description 182 | ------------|-------------|------------- 183 | until | `'eof'` or `callable` | If set, array field count parameter will be ignored, and field will parse values until End of File or callback return false, callback take one argument:
`function ($lastParsedValue) {...}` 184 | 185 | ### Collection 186 | ```php 187 | // Object notation 188 | // -------------------------------------- 189 | // $field = new Collection(, ); 190 | 191 | $field = new Collection([ 192 | 'firstValue' => ['int', 32], 193 | 'secondValue' => ['string', 32] 194 | ]); 195 | 196 | // Array notation 197 | // -------------------------------------- 198 | // $fieldArray = ['collection', , ]; 199 | // or just 200 | // $fieldArray = ; 201 | ``` 202 | 203 | ### Back links 204 | Size, count and conditional key parameters may be declared as a back link - path to already parsed value. Path can starts with `/` sign, that means root of data set or with '../' for relative path. 205 | ```php 206 | $fieldCollection = new \Zerg\Field\Collection([ 207 | 'count' => ['string', 2], 208 | 'intValue' => ['arr', '/count', ['int', 8]] 209 | ]); 210 | $sourceStream = new \Zerg\Stream\StringStream("101234567890"); 211 | $data = $fieldCollection->parse($sourceStream); 212 | print_r($data); 213 | /* 214 | Array 215 | ( 216 | [count] => 10 217 | [intValue] => Array 218 | ( 219 | [0] => 49 220 | [1] => 50 221 | [2] => 51 222 | [3] => 52 223 | [4] => 53 224 | [5] => 54 225 | [6] => 55 226 | [7] => 56 227 | [8] => 57 228 | [9] => 48 229 | ) 230 | ) 231 | */ 232 | ``` 233 | ### Conditional example 234 | ```php 235 | $fieldCollection = new \Zerg\Field\Collection([ 236 | 'count' => ['string', 2], 237 | 'conditional' => ['conditional', '/count', [ 238 | 0 => ['string', 80], 239 | 10 => ['int', 16] 240 | ], 241 | [ 242 | 'default' => ['string', 2] 243 | ] 244 | ] 245 | ]); 246 | $sourceStream = new \Zerg\Stream\StringStream("101234567890"); 247 | $data = $fieldCollection->parse($sourceStream); 248 | print_r($data); 249 | /* 250 | Array 251 | ( 252 | [count] => 10 253 | [conditional] => 12849 254 | ) 255 | */ 256 | ``` 257 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klermonte/zerg", 3 | "description": "PHP structured binary parser", 4 | "keywords": ["binary"], 5 | "minimum-stability": "dev", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Pavel Klementyev", 10 | "email": "klimchek@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4.0", 15 | "klermonte/php-bio": "dev-master" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "4.2.*" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Zerg\\": "src/Zerg" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests/Zerg/ 6 | 7 | 8 | 9 | 10 | src/Zerg/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Zerg/DataSet.php: -------------------------------------------------------------------------------- 1 | setData($data); 32 | } 33 | 34 | /** 35 | * Get wrapped data. 36 | * 37 | * @return array Currently wrapped data. 38 | */ 39 | public function getData() 40 | { 41 | return $this->data; 42 | } 43 | 44 | /** 45 | * Assign new data to DataSet. 46 | * 47 | * @param array $data Data to be wrapped by DataSet. 48 | */ 49 | public function setData(array $data) 50 | { 51 | $this->data = $data; 52 | } 53 | 54 | /** 55 | * Move into a level. 56 | * 57 | * @param string|int $level The level to move into. 58 | */ 59 | public function push($level) 60 | { 61 | array_push($this->currentPath, $level); 62 | } 63 | 64 | /** 65 | * Move back out of the current level. 66 | */ 67 | public function pop() 68 | { 69 | array_pop($this->currentPath); 70 | } 71 | 72 | /** 73 | * Set a value in the current level. 74 | * 75 | * @param string|int $name The name of the value to add. 76 | * @param string|int|array|null $value The value to add. 77 | */ 78 | public function setValue($name, $value) 79 | { 80 | $child = & $this->data; 81 | 82 | foreach ($this->currentPath as $part) { 83 | if (isset($child[$part])) { 84 | $child = & $child[$part]; 85 | } else { 86 | $child[$part] = []; 87 | $child = & $child[$part]; 88 | } 89 | } 90 | 91 | $child[$name] = $value; 92 | } 93 | 94 | /** 95 | * Get a value by name from the current level. 96 | * 97 | * @param string|int $name The name of the value to retrieve. 98 | * @return string|int|array|null The found value. Returns null if the value cannot be found. 99 | */ 100 | public function getValue($name) 101 | { 102 | $child = & $this->data; 103 | 104 | foreach ($this->currentPath as $part) { 105 | if (isset($child[$part])) { 106 | $child = & $child[$part]; 107 | } else { 108 | return null; 109 | } 110 | } 111 | 112 | return isset($child[$name]) ? $child[$name] : null; 113 | } 114 | 115 | /** 116 | * Find a value by path within the DataSet instance. 117 | * 118 | * @see $currentPath 119 | * @param string|array $path Path in internal or human format. 120 | * @return string|int|array|null The found value. Returns null if the value cannot be found. 121 | */ 122 | public function getValueByPath($path) 123 | { 124 | if (is_string($path)) { 125 | $path = $this->parsePath($path); 126 | } 127 | 128 | $child = $this->data; 129 | 130 | foreach ($path as $part) { 131 | if (isset($child[$part])) { 132 | $child = $child[$part]; 133 | } else { 134 | return null; 135 | } 136 | } 137 | 138 | return $child; 139 | } 140 | 141 | /** 142 | * Assign a value by path within the DataSet instance, 143 | * overwrites any existing value. 144 | * 145 | * @see $currentPath 146 | * @param string|array $path A path in internal or human format. 147 | * @param string|int|array|null $value The value to assign. 148 | */ 149 | public function setValueByPath($path, $value) 150 | { 151 | if (is_string($path)) { 152 | $path = $this->parsePath($path); 153 | } 154 | 155 | $endPart = array_pop($path); 156 | $child = & $this->data; 157 | 158 | foreach ($path as $part) { 159 | if (isset($child[$part])) { 160 | $child = & $child[$part]; 161 | } else { 162 | $child[$part] = []; 163 | $child = & $child[$part]; 164 | } 165 | } 166 | 167 | $child[$endPart] = $value; 168 | } 169 | 170 | /** 171 | * Transform human path to internal DataSet format. 172 | * 173 | * @see $currentPath 174 | * @param string $path Path in human format ('/a/b' or 'a/../b' or './b/c'). 175 | * @return array Path in internal format. 176 | * @throws Field\ConfigurationException If path could not be parsed. 177 | */ 178 | public function parsePath($path) 179 | { 180 | $parts = explode('/', trim($path, '/')); 181 | 182 | $pathArray = []; 183 | if ($parts[0] == '.') { 184 | $pathArray = $this->currentPath; 185 | array_shift($parts); 186 | } 187 | 188 | foreach ($parts as $part) { 189 | if ($part == '..') { 190 | if (count($pathArray)) { 191 | array_pop($pathArray); 192 | } else { 193 | throw new Field\ConfigurationException("Invalid path. To many '..', can't move higher root."); 194 | } 195 | } else { 196 | $pathArray[] = $part; 197 | } 198 | } 199 | 200 | return $pathArray; 201 | } 202 | 203 | /** 204 | * Determines whether a given string is a DataSet path. 205 | * 206 | * @param mixed $value Tested string. 207 | * @return bool Whether tested string is a DataSet path. 208 | * @since 0.2 209 | */ 210 | public static function isPath($value) 211 | { 212 | return is_string($value) && strpos($value, '/') !== false; 213 | } 214 | 215 | /** 216 | * Recursively find value by path. 217 | * 218 | * @param string $value Value path. 219 | * @return array|int|null|string 220 | * @since 1.0 221 | */ 222 | public function resolvePath($value) 223 | { 224 | do { 225 | $value = $this->getValueByPath($value); 226 | } while (self::isPath($value)); 227 | 228 | return $value; 229 | } 230 | 231 | /** 232 | * Return current internal read|write position. 233 | * 234 | * @return array 235 | */ 236 | public function getCurrentPath() 237 | { 238 | return $this->currentPath; 239 | } 240 | 241 | /** 242 | * Return value, stored in DataSet by current internal read|write position. 243 | * 244 | * @return array|int|null|string 245 | */ 246 | public function getValueByCurrentPath() 247 | { 248 | return $this->getValueByPath($this->getCurrentPath()); 249 | } 250 | 251 | /** 252 | * @inheritdoc 253 | * */ 254 | public function current() 255 | { 256 | return current($this->data); 257 | } 258 | 259 | /** 260 | * @inheritdoc 261 | * */ 262 | public function next() 263 | { 264 | next($this->data); 265 | } 266 | 267 | /** 268 | * @inheritdoc 269 | * */ 270 | public function key() 271 | { 272 | return key($this->data); 273 | } 274 | 275 | /** 276 | * @inheritdoc 277 | * */ 278 | public function valid() 279 | { 280 | return isset($this->data[$this->key()]); 281 | } 282 | 283 | /** 284 | * @inheritdoc 285 | * */ 286 | public function rewind() 287 | { 288 | reset($this->data); 289 | } 290 | 291 | /** 292 | * @inheritdoc 293 | * */ 294 | public function offsetExists($offset) 295 | { 296 | return isset($this->data[$offset]); 297 | } 298 | 299 | /** 300 | * @inheritdoc 301 | * */ 302 | public function offsetGet($offset) 303 | { 304 | return $this->data[$offset]; 305 | } 306 | 307 | /** 308 | * @inheritdoc 309 | * */ 310 | public function offsetSet($offset, $value) 311 | { 312 | $this->data[$offset] = $value; 313 | } 314 | 315 | /** 316 | * @inheritdoc 317 | * */ 318 | public function offsetUnset($offset) 319 | { 320 | unset($this->data[$offset]); 321 | } 322 | } -------------------------------------------------------------------------------- /src/Zerg/Field/AbstractField.php: -------------------------------------------------------------------------------- 1 | $value) { 50 | $methodName = 'set' . ucfirst(strtolower($name)); 51 | if (method_exists($this, $methodName)) { 52 | $this->$methodName($value); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Return DataSet instance from which this field take back linked values. 59 | * 60 | * @return DataSet DataSet corresponding this field. 61 | */ 62 | public function getDataSet() 63 | { 64 | return $this->dataSet; 65 | } 66 | 67 | /** 68 | * Set to field DataSet instance for back links. 69 | * 70 | * @param DataSet $dataSet DataSet ro correspond to this field. 71 | * @return self For chaining. 72 | */ 73 | public function setDataSet(DataSet $dataSet) 74 | { 75 | $this->dataSet = $dataSet; 76 | return $this; 77 | } 78 | 79 | /** 80 | * @return mixed 81 | */ 82 | public function getAssert() 83 | { 84 | return $this->assert; 85 | } 86 | 87 | /** 88 | * @param mixed $assert 89 | * @return $this 90 | */ 91 | public function setAssert($assert) 92 | { 93 | $this->assert = $assert; 94 | return $this; 95 | } 96 | 97 | /** 98 | * Check that given value is valid. 99 | * 100 | * @param $value mixed Checked value. 101 | * @return true On success validation. 102 | * @throws AssertException On assertion fail. 103 | */ 104 | public function validate($value) 105 | { 106 | $assert = $this->getAssert(); 107 | if ($assert !== null) { 108 | if (is_callable($assert)) { 109 | if (!call_user_func($assert, $value, $this)) { 110 | throw new AssertException( 111 | sprintf('Custom validation fail with value (%s) "%s"', gettype($value), print_r($value, true)) 112 | ); 113 | } 114 | } else { 115 | if ($value !== $assert) { 116 | throw new AssertException( 117 | sprintf( 118 | 'Failed asserting that actual value (%s) "%s" matches expected value (%s) "%s".', 119 | gettype($value), 120 | print_r($value, true), 121 | gettype($assert), 122 | print_r($assert, true) 123 | ) 124 | ); 125 | } 126 | } 127 | } 128 | 129 | return true; 130 | } 131 | 132 | /** 133 | * Process, set and return given property. 134 | * 135 | * Find given property in DataSet if it was set as path string and return it. 136 | * Otherwise already set value will be returned. 137 | * 138 | * @param string $name Property name. 139 | * @return int|string|array|null Found or already set property value. 140 | */ 141 | protected function resolveProperty($name) 142 | { 143 | $value = $this->$name; 144 | if (is_callable($value)) { 145 | return call_user_func($value, $this); 146 | } 147 | 148 | return $this->resolveValue($value); 149 | } 150 | 151 | /** 152 | * Find value in DataSet by given value if it is a path string. 153 | * Otherwise given value will be returned. 154 | * 155 | * @param $value 156 | * @return array|int|null|string 157 | * @since 0.2 158 | */ 159 | private function resolveValue($value) 160 | { 161 | if (DataSet::isPath($value)) { 162 | if (empty($this->dataSet)) { 163 | throw new ConfigurationException('DataSet is required to resole value by path.'); 164 | } 165 | $value = $this->dataSet->resolvePath($value); 166 | } 167 | 168 | if (is_array($value)) { 169 | foreach ($value as $key => $subValue) { 170 | $value[$key] = $this->resolveValue($subValue); 171 | } 172 | } 173 | 174 | return $value; 175 | } 176 | } -------------------------------------------------------------------------------- /src/Zerg/Field/Arr.php: -------------------------------------------------------------------------------- 1 | index = 0; 41 | if (is_array($count)) { 42 | $this->configure($count); 43 | } else { 44 | $this->setCount($count); 45 | $this->setField($field); 46 | $this->configure($options); 47 | } 48 | } 49 | 50 | /** 51 | * Call parse method on arrayed field needed times. 52 | * 53 | * @api 54 | * @param AbstractStream $stream Stream from which children read. 55 | * @return array Array of parsed values. 56 | * @since 1.0 57 | */ 58 | public function parse(AbstractStream $stream) 59 | { 60 | try { 61 | return parent::parse($stream); 62 | } catch (\OutOfBoundsException $e) { 63 | if ($this->isUntilEof()) { 64 | return $this->dataSet->getData(); 65 | } 66 | 67 | throw $e; 68 | } 69 | } 70 | 71 | /** 72 | * @return int 73 | */ 74 | public function getCount() 75 | { 76 | return (int) $this->resolveProperty('count'); 77 | } 78 | 79 | /** 80 | * @param int $count 81 | * @return $this 82 | */ 83 | public function setCount($count) 84 | { 85 | $this->count = $count; 86 | return $this; 87 | } 88 | 89 | /** 90 | * @return callable|string 91 | */ 92 | public function getUntil() 93 | { 94 | return $this->until; 95 | } 96 | 97 | /** 98 | * @param callable|string $until 99 | * @return $this 100 | */ 101 | public function setUntil($until) 102 | { 103 | $this->until = $until; 104 | return $this; 105 | } 106 | 107 | /** 108 | * @return AbstractField 109 | */ 110 | public function getField() 111 | { 112 | $field = $this->resolveProperty('field'); 113 | if (is_array($field)) { 114 | $field = Factory::get($field); 115 | } 116 | return $field; 117 | } 118 | 119 | /** 120 | * @param array|AbstractField $field 121 | * @return $this 122 | */ 123 | public function setField($field) 124 | { 125 | if (is_array($field)) { 126 | $field = Factory::get($field); 127 | } 128 | $this->field = $field; 129 | return $this; 130 | } 131 | 132 | /** 133 | * @inheritdoc 134 | * @return AbstractField 135 | */ 136 | public function current() 137 | { 138 | return $this->getField(); 139 | } 140 | 141 | /** 142 | * @inheritdoc 143 | */ 144 | public function next() 145 | { 146 | $this->index++; 147 | } 148 | 149 | /** 150 | * @inheritdoc 151 | */ 152 | public function key() 153 | { 154 | return $this->index; 155 | } 156 | 157 | /** 158 | * @inheritdoc 159 | */ 160 | public function valid() 161 | { 162 | if (is_callable($this->getUntil())) { 163 | $value = $this->getDataSet()->getValueByCurrentPath(); 164 | return call_user_func($this->getUntil(), end($value)); 165 | } 166 | return $this->index < $this->getCount() || $this->isUntilEof(); 167 | } 168 | 169 | /** 170 | * @inheritdoc 171 | */ 172 | public function rewind() 173 | { 174 | $this->index = 0; 175 | } 176 | 177 | /** 178 | * Whether array must read until end of file. 179 | * 180 | * @return bool 181 | */ 182 | private function isUntilEof() 183 | { 184 | $until = $this->getUntil(); 185 | return is_string($until) && strtolower($until) === 'eof'; 186 | } 187 | } -------------------------------------------------------------------------------- /src/Zerg/Field/AssertException.php: -------------------------------------------------------------------------------- 1 | initFromArray($schema); 26 | $this->configure($options); 27 | } 28 | 29 | /** 30 | * Add a new child node to field list. 31 | * 32 | * @param string $name The field name. 33 | * @param AbstractField $child Field instance. 34 | */ 35 | public function addField($name, AbstractField $child) 36 | { 37 | $this->fields[$name] = $child; 38 | } 39 | 40 | /** 41 | * Recursively call parse method of all children and store values in associated DataSet. 42 | * 43 | * @api 44 | * @param AbstractStream $stream Stream from which children read. 45 | * @return array Array of parsed values. 46 | */ 47 | public function parse(AbstractStream $stream) 48 | { 49 | if (!($this->dataSet instanceof DataSet)) { 50 | $this->dataSet = new DataSet; 51 | } 52 | 53 | $this->rewind(); 54 | do { 55 | $field = $this->current(); 56 | $field->setDataSet($this->getDataSet()); 57 | if ($field instanceof Conditional) { 58 | $field = $field->resolveField(); 59 | } 60 | if ($field instanceof self) { 61 | $this->dataSet->push($this->key()); 62 | $field->parse($stream); 63 | $this->dataSet->pop(); 64 | } else { 65 | $this->dataSet->setValue($this->key(), $field->parse($stream)); 66 | } 67 | $this->next(); 68 | } while ($this->valid()); 69 | 70 | if (isset($this->assert)) { 71 | $this->validate($this->dataSet->getValueByCurrentPath()); 72 | } 73 | 74 | return $this->dataSet->getData(); 75 | } 76 | 77 | /** 78 | * Recursively creates field instances form their declarations. 79 | * 80 | * @param array $fieldArray Array of declarations. 81 | * @throws ConfigurationException If one of declarations are invalid. 82 | */ 83 | private function initFromArray(array $fieldArray = []) 84 | { 85 | foreach ($fieldArray as $fieldName => $fieldParams) { 86 | $this->addField($fieldName, Factory::get($fieldParams)); 87 | } 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | */ 93 | public function offsetExists($offset) 94 | { 95 | return isset($this->fields[$offset]); 96 | } 97 | 98 | /** 99 | * @inheritdoc 100 | */ 101 | public function offsetGet($offset) 102 | { 103 | return $this->fields[$offset]; 104 | } 105 | 106 | /** 107 | * @inheritdoc 108 | */ 109 | public function offsetSet($offset, $value) 110 | { 111 | $this->fields[$offset] = $value; 112 | } 113 | 114 | /** 115 | * @inheritdoc 116 | */ 117 | public function offsetUnset($offset) 118 | { 119 | unset($this->fields[$offset]); 120 | } 121 | 122 | /** 123 | * @inheritdoc 124 | * @return AbstractField 125 | */ 126 | public function current() 127 | { 128 | return current($this->fields); 129 | } 130 | 131 | /** 132 | * @inheritdoc 133 | */ 134 | public function next() 135 | { 136 | next($this->fields); 137 | } 138 | 139 | /** 140 | * @inheritdoc 141 | */ 142 | public function key() 143 | { 144 | return key($this->fields); 145 | } 146 | 147 | /** 148 | * @inheritdoc 149 | */ 150 | public function valid() 151 | { 152 | return isset($this->fields[$this->key()]); 153 | } 154 | 155 | /** 156 | * @inheritdoc 157 | */ 158 | public function rewind() 159 | { 160 | reset($this->fields); 161 | } 162 | } -------------------------------------------------------------------------------- /src/Zerg/Field/Conditional.php: -------------------------------------------------------------------------------- 1 | configure($key); 34 | } else { 35 | $this->setKey($key); 36 | $this->setFields($fields); 37 | $this->configure($options); 38 | } 39 | } 40 | 41 | /** 42 | * Resolve needed field instance and call it's parse method. 43 | * 44 | * @api 45 | * @param AbstractStream $stream Stream from which resolved field reads. 46 | * @return mixed Value returned by resolved field. 47 | */ 48 | public function parse(AbstractStream $stream) 49 | { 50 | $field = $this->resolveField(); 51 | 52 | return $field->parse($stream); 53 | } 54 | 55 | /** 56 | * Resolve value by DataSet path, choose related declaration and return field instance. 57 | * 58 | * @return AbstractField Field instance created by chosen declaration. 59 | * @throws InvalidKeyException If no one declaration is not related to resolved value and 60 | * default declaration is not presented. 61 | */ 62 | public function resolveField() 63 | { 64 | $field = $this; 65 | do { 66 | $field = $field->resolve(); 67 | } while ($field instanceof self); 68 | 69 | return $field; 70 | } 71 | 72 | private function resolve() 73 | { 74 | $key = $this->resolveProperty('key'); 75 | 76 | if (isset($this->fields[$key])) { 77 | $field = $this->fields[$key]; 78 | } elseif ($this->default !== null) { 79 | $field = $this->default; 80 | } else { 81 | throw new InvalidKeyException( 82 | "Value '{$key}' does not correspond to a valid conditional key. Presented keys: '" . 83 | implode("', '", array_keys($this->fields)) . "'" 84 | ); 85 | } 86 | 87 | // get form cache 88 | if ($field instanceof AbstractField) { 89 | return $field; 90 | } 91 | 92 | $field = Factory::get($field); 93 | $field->setDataSet($this->getDataSet()); 94 | 95 | // cache field instance 96 | if (isset($this->fields[$key])) { 97 | $this->fields[$key] = $field; 98 | } else { 99 | $this->default = $field; 100 | } 101 | 102 | return $field; 103 | } 104 | 105 | /** 106 | * @param int|string $key 107 | */ 108 | public function setKey($key) 109 | { 110 | $this->key = $key; 111 | } 112 | 113 | /** 114 | * @param array|null|AbstractField $default 115 | */ 116 | public function setDefault($default) 117 | { 118 | $this->default = $default; 119 | } 120 | 121 | /** 122 | * @param array|AbstractField[] $fields 123 | */ 124 | public function setFields($fields) 125 | { 126 | $this->fields = $fields; 127 | } 128 | } -------------------------------------------------------------------------------- /src/Zerg/Field/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | setValues($values); 30 | } 31 | } 32 | 33 | /** 34 | * Read key from Stream, and return value by this key or default value. 35 | * 36 | * @param AbstractStream $stream Stream from which resolved field reads. 37 | * @return object|integer|double|string|array|boolean|callable Value by read key or default value if present. 38 | * @throws InvalidKeyException If read key is not exist and default value is not presented. 39 | */ 40 | public function read(AbstractStream $stream) 41 | { 42 | $key = parent::read($stream); 43 | $values = $this->getValues(); 44 | 45 | if (array_key_exists($key, $values)) { 46 | $value = $values[$key]; 47 | } else { 48 | $value = $this->getDefault(); 49 | } 50 | 51 | if ($value === null) { 52 | throw new InvalidKeyException( 53 | "Value '{$key}' does not correspond to a valid enum key. Presented keys: '" . 54 | implode("', '", array_keys($values)) . "'" 55 | ); 56 | } 57 | 58 | return $value; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function getValues() 65 | { 66 | return $this->values; 67 | } 68 | 69 | /** 70 | * @param array $values 71 | */ 72 | public function setValues($values) 73 | { 74 | $this->values = $values; 75 | } 76 | 77 | /** 78 | * @return mixed 79 | */ 80 | public function getDefault() 81 | { 82 | return $this->default; 83 | } 84 | 85 | /** 86 | * @param mixed $default 87 | */ 88 | public function setDefault($default) 89 | { 90 | $this->default = $default; 91 | } 92 | } -------------------------------------------------------------------------------- /src/Zerg/Field/Factory.php: -------------------------------------------------------------------------------- 1 | newInstanceArgs($declaration); 28 | } 29 | 30 | throw new ConfigurationException("Field {$fieldType} doesn't exist"); 31 | } 32 | 33 | public static function get($array) 34 | { 35 | if (!is_array($array)) { 36 | throw new ConfigurationException('Unknown element declaration'); 37 | } 38 | 39 | $isAssoc = array_keys(array_keys($array)) !== array_keys($array); 40 | 41 | if ($isAssoc || is_array(reset($array))) { 42 | $array = ['collection', $array]; 43 | } 44 | 45 | return Factory::instantiate($array); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Zerg/Field/Int.php: -------------------------------------------------------------------------------- 1 | signed; 28 | } 29 | 30 | /** 31 | * Setter for signed property. 32 | * 33 | * @param bool $signed 34 | */ 35 | public function setSigned($signed) 36 | { 37 | $this->signed = $signed; 38 | } 39 | 40 | /** 41 | * Read data from Stream and cast it to integer. 42 | * 43 | * @param AbstractStream $stream Stream from which read. 44 | * @return int Result value. 45 | */ 46 | public function read(AbstractStream $stream) 47 | { 48 | return $stream->getBuffer()->readInt($this->getSize(), $this->getSigned(), $this->getEndian()); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Zerg/Field/InvalidKeyException.php: -------------------------------------------------------------------------------- 1 | skip($this->getSize()); 28 | return null; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Zerg/Field/Scalar.php: -------------------------------------------------------------------------------- 1 | 1, 45 | 'SEMI_NIBBLE' => 2, 46 | 'NIBBLE' => 4, 47 | 'BYTE' => 8, 48 | 'SHORT' => 16, 49 | 'WORD' => 32, 50 | 'DWORD' => 64, 51 | ]; 52 | 53 | /** 54 | * Read part of data from source and return value in necessary format. 55 | * 56 | * This is abstract method, so each implementation should return it's own 57 | * type of value. 58 | * 59 | * @param AbstractStream $stream Stream from which read. 60 | * @return int|string|null Value type depend by implementation. 61 | */ 62 | abstract public function read(AbstractStream $stream); 63 | 64 | 65 | public function __construct($size, $options = []) 66 | { 67 | if (is_array($size)) { 68 | $this->configure($size); 69 | } else { 70 | $this->setSize($size); 71 | $this->configure($options); 72 | } 73 | } 74 | 75 | /** 76 | * Return final value of size. 77 | * 78 | * If size was set as DataSet path or callback, it will be processed here. 79 | * 80 | * @return int Final value of size. 81 | * @throws ConfigurationException If the value was less than zero. 82 | */ 83 | public function getSize() 84 | { 85 | $size = (int) $this->resolveProperty('size'); 86 | 87 | if ($size < 0) { 88 | throw new ConfigurationException('Field size should not be less 0'); 89 | } 90 | 91 | return $size; 92 | } 93 | 94 | /** 95 | * Process and sets size. 96 | * 97 | * Size can be represented as a string containing on of size key words {@see $sizes}. 98 | * Also you can set path to already parsed value in DataSet. 99 | * 100 | * @param int|string $size Size in bits/bytes or DataSet path. 101 | * @return static For chaining. 102 | */ 103 | public function setSize($size) 104 | { 105 | if (is_string($size) && $parsed = $this->parseSizeWord($size)) { 106 | $this->size = $parsed; 107 | } else { 108 | $this->size = $size; 109 | } 110 | return $this; 111 | } 112 | 113 | /** 114 | * Getter for the value callback. 115 | * 116 | * @return callable 117 | */ 118 | public function getFormatter() 119 | { 120 | return $this->formatter; 121 | } 122 | 123 | /** 124 | * Setter for the value callback. 125 | * 126 | * @param callable $formatter 127 | * @return $this 128 | */ 129 | public function setFormatter($formatter) 130 | { 131 | $this->formatter = $formatter; 132 | return $this; 133 | } 134 | 135 | /** 136 | * @return int 137 | * @since 1.0 138 | */ 139 | public function getEndian() 140 | { 141 | return $this->endian; 142 | } 143 | 144 | /** 145 | * @param int $endian 146 | * @since 1.0 147 | * @return $this 148 | */ 149 | public function setEndian($endian) 150 | { 151 | if (!in_array($endian, [Endian::ENDIAN_BIG, Endian::ENDIAN_LITTLE])) { 152 | throw new ConfigurationException( 153 | sprintf('Endian must be %d for Big endian of %d for Little', Endian::ENDIAN_BIG, Endian::ENDIAN_LITTLE) 154 | ); 155 | } 156 | $this->endian = $endian; 157 | return $this; 158 | } 159 | 160 | /** 161 | * Reads and process value from Stream. 162 | * 163 | * @api 164 | * @param AbstractStream $stream Stream from which read. 165 | * @return mixed The final value. 166 | */ 167 | public function parse(AbstractStream $stream) 168 | { 169 | $value = $this->format($this->read($stream)); 170 | $this->validate($value); 171 | return $value; 172 | } 173 | 174 | /** 175 | * Applies value formatter to read value. 176 | * 177 | * @param int|string|null $value Read value. 178 | * @return mixed Processed value. 179 | */ 180 | private function format($value) 181 | { 182 | if (is_callable($this->formatter)) { 183 | $value = call_user_func($this->formatter, $value, $this->dataSet); 184 | } 185 | return $value; 186 | } 187 | 188 | /** 189 | * Process given string and return appropriate size value. 190 | * 191 | * @param $word 192 | * @return int 193 | */ 194 | private function parseSizeWord($word) 195 | { 196 | $sizeWord = strtoupper(preg_replace('/([a-z])([A-Z])/', '$1_$2', $word)); 197 | if (array_key_exists($sizeWord, $this->sizes)) { 198 | $size = $this->sizes[$sizeWord]; 199 | } else { 200 | $size = 0; 201 | } 202 | return $size; 203 | } 204 | } -------------------------------------------------------------------------------- /src/Zerg/Field/String.php: -------------------------------------------------------------------------------- 1 | getBuffer()->read($this->getSize(), $this->getEndian()); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Zerg/Stream/AbstractStream.php: -------------------------------------------------------------------------------- 1 | buffer; 34 | } 35 | 36 | /** 37 | * Move internal pointer by given amount of bits ahead without reading dta. 38 | * 39 | * @param int $size Amount of bits to be skipped. 40 | */ 41 | public function skip($size) 42 | { 43 | $this->buffer->setPosition($this->buffer->getPosition() + $size); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Zerg/Stream/FileStream.php: -------------------------------------------------------------------------------- 1 | buffer = new BitBuffer($handle); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Zerg/Stream/StringStream.php: -------------------------------------------------------------------------------- 1 | buffer = new BitBuffer($string); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /tests/Zerg/DataSetTest.php: -------------------------------------------------------------------------------- 1 | 'b']); 11 | $dataSet['c'] = 'd'; 12 | $this->assertArrayHasKey('a', $dataSet); 13 | $this->assertArrayHasKey('c', $dataSet); 14 | $this->assertEquals('b', $dataSet['a']); 15 | $this->assertEquals('d', $dataSet['c']); 16 | $this->assertCount(2, $dataSet); 17 | unset($dataSet['a']); 18 | $this->assertArrayNotHasKey('a', $dataSet); 19 | $this->assertCount(1, $dataSet); 20 | } 21 | 22 | public function testIterator() 23 | { 24 | $dataSet = new DataSet([ 25 | 'a' => 'b', 26 | 'c' => 'd', 27 | 'e' => [ 28 | 'f' => 'e' 29 | ] 30 | ]); 31 | 32 | $dataSet->rewind(); 33 | while($dataSet->valid()) { 34 | $this->assertSame($dataSet[$dataSet->key()], $dataSet->current()); 35 | $dataSet->next(); 36 | } 37 | } 38 | 39 | public function testGetData() 40 | { 41 | $dataSet = new DataSet(['a' => 'b']); 42 | $data = $dataSet->getData(); 43 | $this->assertArrayHasKey('a', $data); 44 | $this->assertEquals('b', $data['a']); 45 | $this->assertCount(1, $data); 46 | } 47 | 48 | public function testFlatSetValue() 49 | { 50 | $data = new DataSet(); 51 | $data->setValue('foo', 'bar'); 52 | $this->assertArrayHasKey('foo', $data); 53 | $this->assertEquals('bar', $data['foo']); 54 | $this->assertCount(1, $data); 55 | } 56 | 57 | public function testFlatGetValue() 58 | { 59 | $dataSet = new DataSet(); 60 | $dataSet->setValue('foo', 'bar'); 61 | $this->assertEquals('bar', $dataSet->getValue('foo')); 62 | $dataSet->push('nextLevel'); 63 | $this->assertEquals(null, $dataSet->getValue('foo')); 64 | } 65 | 66 | public function testFlatGetValueByPath() 67 | { 68 | $dataSet = new DataSet(); 69 | $dataSet->setValue('foo', 'bar'); 70 | $this->assertEquals('bar', $dataSet->getValueByPath(['foo'])); 71 | $this->assertEquals(null, $dataSet->getValueByPath(['foo', 'bar'])); 72 | } 73 | 74 | public function testFlatSetValueByPath() 75 | { 76 | $dataSet = new DataSet(['exists' => ['key' => 'value']]); 77 | $dataSet->setValueByPath(['foo'], 'bar'); 78 | $dataSet->setValueByPath('/exists/key', 'newValue'); 79 | $this->assertEquals('bar', $dataSet->getValueByPath(['foo'])); 80 | $this->assertEquals('newValue', $dataSet->getValueByPath(['exists', 'key'])); 81 | } 82 | 83 | public function testNestedSetValue() 84 | { 85 | $dataSet = new DataSet(); 86 | $dataSet->push('level1'); 87 | $dataSet->setValue('foo', 'bar'); 88 | $this->assertEquals([ 89 | 'level1' => [ 90 | 'foo' => 'bar' 91 | ] 92 | ], $dataSet->getData()); 93 | } 94 | 95 | public function testNestedGetValue() 96 | { 97 | $dataSet = new DataSet(); 98 | $dataSet->push('level1'); 99 | $dataSet->setValue('foo', 'bar'); 100 | $this->assertEquals('bar', $dataSet->getValue('foo')); 101 | } 102 | 103 | public function testNestedGetValueByPath() 104 | { 105 | $dataSet = new DataSet(); 106 | $dataSet->push('level1'); 107 | $dataSet->setValue('foo', 'bar'); 108 | $this->assertEquals('bar', $dataSet->getValueByPath(['level1', 'foo'], true)); 109 | } 110 | 111 | public function testNestedSetValueByPath() 112 | { 113 | $dataSet = new DataSet(); 114 | $dataSet->setValueByPath(['level1', 'foo'], 'bar'); 115 | $this->assertEquals('bar', $dataSet->getValueByPath(['level1', 'foo'], true)); 116 | } 117 | 118 | public function paths() 119 | { 120 | return [ 121 | [[], '/a/b/c', ['a', 'b', 'c']], 122 | [[], 'a/b/c', ['a', 'b', 'c']], 123 | [['e', 'f'], './a/b/c', ['e', 'f', 'a', 'b', 'c']], 124 | [['a', 'b', 'c'], './../d/e', ['a', 'b', 'd', 'e']], 125 | [['a', 'b', 'c'], './../../e', ['a', 'e']], 126 | ]; 127 | } 128 | 129 | public function invalidPaths() 130 | { 131 | return [ 132 | [[], '../../a'], 133 | [[], './../a'], 134 | [['e', 'f'], './../f/../../../a'], 135 | [['a', 'b', 'c'], './../../../../a'], 136 | [['a', 'b', 'c'], '/../a'], 137 | ]; 138 | } 139 | 140 | public function pathStrings() 141 | { 142 | return [ 143 | ['/a/b/c', true], 144 | ['a/b/c', true], 145 | ['./a/1/c', true], 146 | ['./../d/4', true], 147 | ['qwe', false], 148 | ['20', false], 149 | ['.qwe', false], 150 | ['asdf.qwe', false], 151 | ['asdf..qwe', false], 152 | ['', false], 153 | ['.', false], 154 | ['..', false], 155 | [' ', false], 156 | ]; 157 | } 158 | 159 | /** 160 | * @dataProvider paths 161 | */ 162 | public function testParsePath($currentPath, $pathString, $path) 163 | { 164 | $dataSet = new DataSet(); 165 | foreach ($currentPath as $part) { 166 | $dataSet->push($part); 167 | } 168 | 169 | $this->assertEquals($path, $dataSet->parsePath($pathString)); 170 | } 171 | 172 | /** 173 | * @dataProvider invalidPaths 174 | * @expectedException \Zerg\Field\ConfigurationException 175 | */ 176 | public function testParsePathException($currentPath, $pathString) 177 | { 178 | $dataSet = new DataSet(); 179 | foreach ($currentPath as $part) { 180 | $dataSet->push($part); 181 | } 182 | 183 | $dataSet->parsePath($pathString); 184 | } 185 | 186 | /** 187 | * @dataProvider pathStrings 188 | */ 189 | public function testIsPath($path, $isPath) 190 | { 191 | $this->assertEquals($isPath, DataSet::isPath($path)); 192 | } 193 | 194 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/ArrTest.php: -------------------------------------------------------------------------------- 1 | 'eof']); 14 | $this->assertEquals('eof', $arr->getUntil()); 15 | $arr->setUntil(function () {return true;}); 16 | $this->assertInstanceOf('\Closure', $arr->getUntil()); 17 | } 18 | 19 | public function testUntilEof() 20 | { 21 | $arr = new Arr(null, ['string', 'byte'], ['until' => 'eof']); 22 | $data = $arr->parse(new StringStream('12345')); 23 | $this->assertEquals(str_split('12345'), $data); 24 | } 25 | 26 | public function testUntilCallback() 27 | { 28 | $arr = new Arr(null, ['string', 'byte'], ['until' => function ($lastValue) { 29 | return (int) $lastValue < 5; 30 | }]); 31 | $data = $arr->parse(new StringStream('12345')); 32 | $this->assertEquals(str_split('12345'), $data); 33 | } 34 | 35 | /** 36 | * @expectedException \OutOfBoundsException 37 | */ 38 | public function testOutException() 39 | { 40 | (new Arr(null, ['string', 'byte'], ['until' => function ($lastValue) { 41 | return (int) $lastValue < 9; 42 | }]))->parse(new StringStream('12345')); 43 | } 44 | 45 | public function testBackLinkField() 46 | { 47 | $arr = new Arr(2, '/firstField'); 48 | $arr->setDataSet(new DataSet(['firstField' => ['string', 8]])); 49 | $data = $arr->parse(new StringStream('12345')); 50 | $this->assertEquals('1', $data[0]); 51 | $this->assertEquals('2', $data[1]); 52 | } 53 | 54 | public function testMassConfig() 55 | { 56 | $conditional1 = new Arr(null, ['int', 8], ['until' => 'eof']); 57 | $conditional2 = new Arr([ 58 | 'field' => ['int', 8], 59 | 'until' => 'eof', 60 | ]); 61 | $this->assertEquals($conditional1, $conditional2); 62 | } 63 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/CollectionTest.php: -------------------------------------------------------------------------------- 1 | ['int', 8], 13 | 'b' => ['string', 10] 14 | ]); 15 | 16 | $this->assertInstanceOf('\\Zerg\\Field\\Int', $collection['a']); 17 | $this->assertInstanceOf('\\Zerg\\Field\\String', $collection['b']); 18 | $this->assertFalse(isset($collection['c'])); 19 | $collection['c'] = new Int(1); 20 | $this->assertInstanceOf('\\Zerg\\Field\\Int', $collection['c']); 21 | unset($collection['a']); 22 | $this->assertFalse(isset($collection['a'])); 23 | } 24 | 25 | public function testIterator() 26 | { 27 | $types = [ 28 | 'a' => '\\Zerg\\Field\\Int', 29 | 'b' => '\\Zerg\\Field\\String', 30 | 'c' => '\\Zerg\\Field\\Collection' 31 | ]; 32 | 33 | $collection = new Collection([ 34 | 'a' => ['int', 8], 35 | 'b' => ['string', 10], 36 | 'c' => [ 37 | 'a' => ['int', 8], 38 | 'b' => ['string', 10], 39 | ] 40 | ]); 41 | 42 | $this->assertCount(3, $collection); 43 | 44 | foreach ($collection as $key => $field) { 45 | $this->assertInstanceOf($types[$key], $field); 46 | } 47 | 48 | $collection->rewind(); 49 | while ($collection->valid()) { 50 | $this->assertInstanceOf($types[$collection->key()], $collection->current()); 51 | $collection->next(); 52 | } 53 | } 54 | 55 | public function testInitFromArray() 56 | { 57 | $collection = new Collection([ 58 | 'a' => ['int', 8], 59 | 'b' => ['string', 10], 60 | 'c' => [ 61 | 'd' => ['int', 8, ['signed' => true]] 62 | ], 63 | 'e' => ['arr', 16, ['string', 10]], 64 | 'f' => ['arr', 5, [ 65 | 'collection', [ 66 | 'fa' => ['int', 8], 67 | 'fc' => [ 68 | ['int', 8], 69 | ['int', 8] 70 | ], 71 | 'fb' => ['string', 10, ['assert' => 'qweqweqweq']] 72 | ] 73 | ]] 74 | ]); 75 | 76 | $this->assertInstanceOf('\\Zerg\\Field\\Int', $collection['a']); 77 | $this->assertInstanceOf('\\Zerg\\Field\\String', $collection['b']); 78 | $this->assertInstanceOf('\\Zerg\\Field\\Int', $collection['c']['d']); 79 | $this->assertInstanceOf('\\Zerg\\Field\\Collection', $collection['c']); 80 | $this->assertEquals(16, $collection['e']->getCount()); 81 | $this->assertInstanceOf('\\Zerg\\Field\\Arr', $collection['f']); 82 | $this->assertEquals(5, $collection['f']->getCount()); 83 | } 84 | 85 | public function testParse() 86 | { 87 | $collection = new Collection([ 88 | 'a' => ['int', 'byte', ['assert' => 49]], 89 | 'b' => ['conditional', '/a', 90 | [ 91 | 0 => ['int', 8], 92 | 49 => ['conditional', '/a', 93 | [ 94 | 49 => ['string', 6] 95 | ] 96 | ] 97 | ], 98 | [ 99 | 'default' => [ 100 | ['int', 8], 101 | ['int', 8] 102 | ] 103 | ] 104 | ], 105 | 'c' => [ 106 | 'collection', 107 | [ 108 | 'd' => ['int', 8, ['signed' => true]], 109 | 'd2' => ['int', 8, ['signed' => true]], 110 | ], 111 | [ 112 | 'assert' => [ 113 | 'd' => 76, 114 | 'd2' => 76 115 | ] 116 | ] 117 | ], 118 | 'e' => ['arr', 16, ['string', 80]], 119 | 'f' => [ 120 | 'arr', 121 | function (Arr $arrayField) { 122 | $value = $arrayField 123 | ->getDataSet() 124 | ->getValueByPath('/a') - 45; 125 | return $value; 126 | }, 127 | [ 128 | 'fa' => ['arr', 5, ['field' => ['string', 16]]], 129 | 'fc' => [ 130 | 'qwe1' => ['int', 8], 131 | 'qwe2' => ['int', 8], 132 | 'qwe3' => [ 133 | ['string', 16], 134 | ['string', 16], 135 | ] 136 | ], 137 | 'fb' => ['string', 16, ['assert' => function ($readString) { 138 | return $readString === 'LL'; 139 | }]] 140 | ] 141 | ], 142 | ]); 143 | 144 | $stream = new StringStream(str_pad('', 1000, '1')); 145 | 146 | $dataSet = $collection->parse($stream); 147 | 148 | $this->assertInternalType('int', $dataSet['a']); 149 | $this->assertInternalType('string', $dataSet['b']); 150 | $this->assertCount(16, $dataSet['e']); 151 | $this->assertCount(4, $dataSet['f']); 152 | $this->assertCount(5, $dataSet['f'][2]['fa']); 153 | $this->assertCount(3, $dataSet['f'][3]['fc']); 154 | $this->assertCount(2, $dataSet['f'][3]['fc']['qwe3']); 155 | $this->assertEquals(2, strlen($dataSet['f'][3]['fc']['qwe3'][1])); 156 | } 157 | 158 | /** 159 | * @expectedException \Zerg\Field\ConfigurationException 160 | * */ 161 | public function testCreationException() 162 | { 163 | new Collection(['foo']); 164 | } 165 | 166 | /** 167 | * @expectedException \PHPUnit_Framework_Error 168 | */ 169 | public function testCreationError() 170 | { 171 | new Collection('foo'); 172 | } 173 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/ConditionalTest.php: -------------------------------------------------------------------------------- 1 | field = new Conditional('/c/d', [ 19 | 1 => [ 20 | 'wqe' => ['int', 8], 21 | 'asd' => ['string', 16] 22 | ], 23 | 5 => ['int', 8], 24 | 10 => ['conditional', '/a', [ 25 | 1 => ['string', 48] 26 | ]] 27 | ], 28 | ['default' => ['int', 8]] 29 | ); 30 | 31 | $this->field->setDataSet(new DataSet([ 32 | 'a' => 1, 33 | 'b' => 5, 34 | 'c' => [ 35 | 'd' => 3, 36 | 'e' => 10, 37 | 'f' => '/c/d' 38 | ] 39 | ])); 40 | 41 | $this->stream = new StringStream('123abcdefghiklm'); 42 | } 43 | 44 | public function testParse() 45 | { 46 | $field = $this->field; 47 | $stream = $this->stream; 48 | 49 | $value = $field->parse($stream); 50 | $this->assertSame(49, $value); 51 | 52 | $field->setKey('/a'); 53 | $value = $field->parse($stream); 54 | $this->assertSame(50, $value['wqe']); 55 | $this->assertSame('3a', $value['asd']); 56 | 57 | $field->setKey('/b'); 58 | $value = $field->parse($stream); 59 | $this->assertSame(ord('b'), $value); 60 | 61 | $field->setKey('/c/e'); 62 | $value = $field->parse($stream); 63 | $this->assertSame('cdefgh', $value); 64 | 65 | $field->setKey('/c/f'); 66 | $value = $field->parse($stream); 67 | $this->assertSame(ord('i'), $value); 68 | } 69 | 70 | /** 71 | * @expectedException \Zerg\Field\InvalidKeyException 72 | * */ 73 | public function testKeyException() 74 | { 75 | $this->field->setKey('/a/y'); 76 | $this->field->setDefault(null); 77 | $this->field->parse($this->stream); 78 | } 79 | 80 | public function testMassConfig() 81 | { 82 | $conditional1 = new Conditional('/some/path', [['int', 8], ['int', 8]], ['assert' => 10, 'default' => ['int', 8]]); 83 | $conditional2 = new Conditional([ 84 | 'key' => '/some/path', 85 | 'fields' => [['int', 8], ['int', 8]], 86 | 'assert' => 10, 87 | 'default' => ['int', 8], 88 | 'signed' => true 89 | ]); 90 | $this->assertEquals($conditional1, $conditional2); 91 | } 92 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/EnumTest.php: -------------------------------------------------------------------------------- 1 | 'right', 13 | 32 => 'wrong', 14 | ], 15 | ['default' => 'default'] 16 | ); 17 | 18 | $stream = new StringStream('123abcdefg'); 19 | 20 | $this->assertEquals('right', $field->read($stream)); 21 | $this->assertEquals('default', $field->read($stream)); 22 | 23 | } 24 | 25 | /** 26 | * @expectedException \Zerg\Field\InvalidKeyException 27 | * */ 28 | public function testKeyException() 29 | { 30 | $field = new Enum(8, [ 31 | 49 => 'right', 32 | 32 => 'wrong', 33 | ]); 34 | 35 | $stream = new StringStream('223abcdefg'); 36 | 37 | $field->read($stream); 38 | } 39 | 40 | public function testAssertion() 41 | { 42 | $field = new Enum(8, [ 43 | 49 => 'right', 44 | 32 => 'wrong', 45 | ], ['assert' => 'right']); 46 | $this->assertTrue($field->validate($field->read(new StringStream('1')))); 47 | } 48 | 49 | /** 50 | * @expectedException \Zerg\Field\AssertException 51 | * */ 52 | public function testAssertionException() 53 | { 54 | (new Enum(8, [ 55 | 49 => 'right', 56 | 32 => 'wrong', 57 | ], ['assert' => 'wrong']))->parse(new StringStream('1')); 58 | } 59 | 60 | public function testMassConfig() 61 | { 62 | $enum1 = new Enum(32, [1, 2], ['assert' => 10, 'signed' => true]); 63 | $enum2 = new Enum([ 64 | 'size' => 32, 65 | 'values' => [1, 2], 66 | 'assert' => 10, 67 | 'signed' => true 68 | ]); 69 | $this->assertEquals($enum1, $enum2); 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/FactoryTest.php: -------------------------------------------------------------------------------- 1 | true]], '\\Zerg\\Field\Int'], 12 | [['string', 1, ['assert' => 'qwe']], '\\Zerg\\Field\String'], 13 | [['enum', 1, [], ['default' => 1]], '\\Zerg\\Field\Enum'], 14 | [['conditional', 1, [], ['default' => []]], '\\Zerg\\Field\conditional'], 15 | [['padding', 1], '\\Zerg\\Field\padding'], 16 | [['collection', []], '\\Zerg\\Field\collection'], 17 | [['arr', 10, ['int', 1]], '\\Zerg\\Field\Arr'], 18 | [['arr', ['field' => ['int', 1], 'count' => 10]], '\\Zerg\\Field\Arr'], 19 | ]; 20 | } 21 | 22 | /** 23 | * @dataProvider types 24 | * */ 25 | public function testCreation($type, $class) 26 | { 27 | $field = Factory::get($type); 28 | $this->assertInstanceOf($class, $field); 29 | } 30 | 31 | /** 32 | * @expectedException \Zerg\Field\ConfigurationException 33 | * */ 34 | public function testCreationException() 35 | { 36 | Factory::get(['foo']); 37 | } 38 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/IntTest.php: -------------------------------------------------------------------------------- 1 | true]); 13 | $this->assertTrue($int->getSigned()); 14 | $int->setSigned(false); 15 | $this->assertFalse($int->getSigned()); 16 | $int->setSigned(true); 17 | $this->assertTrue($int->getSigned()); 18 | } 19 | 20 | public function testRead() 21 | { 22 | $stream = new StringStream("\x03\x80\x80"); 23 | $int = new Int('byte', ['signed' => true]); 24 | $this->assertSame(3, $int->read($stream)); 25 | $this->assertSame(-128, $int->read($stream)); 26 | $int->setSigned(false); 27 | $this->assertSame(128, $int->read($stream)); 28 | } 29 | 30 | public function testReadBits() 31 | { 32 | $stream = new StringStream("\x73\xda\xf4\xdc\0"); 33 | $int = new Int('nibble'); 34 | $values = [7, 3, 13, 10, 15, 4]; 35 | foreach ($values as $expected) { 36 | $this->assertSame($expected, $int->read($stream)); 37 | } 38 | 39 | $this->assertSame(0xdc, (new Int('byte'))->read($stream)); 40 | 41 | $int->setSize('semi_nibble'); 42 | $stream->getBuffer()->setPosition(0); 43 | 44 | $values = [1,3,0,3,3,1,2,2,3,3,1,0]; 45 | foreach ($values as $expected) { 46 | $this->assertSame($expected, $int->read($stream)); 47 | } 48 | } 49 | 50 | /** 51 | * @expectedException \OutOfBoundsException 52 | * */ 53 | public function testOutOfBoundary() 54 | { 55 | $int = new Int('nibble'); 56 | $newStream = new StringStream("\x31"); 57 | $int->read($newStream); 58 | $this->assertSame(1, $int->read($newStream)); 59 | $int->read($newStream); 60 | } 61 | 62 | /** 63 | * @expectedException \Zerg\Field\ConfigurationException 64 | * @dataProvider invalidValues 65 | * */ 66 | public function testInvalidOptionSize($invalidValue) 67 | { 68 | $int = new Int($invalidValue); 69 | $int->getSize(); 70 | } 71 | 72 | 73 | /** 74 | * @expectedException \LengthException 75 | * */ 76 | public function testLargeIntException() 77 | { 78 | $int = new Int(65); 79 | $int->read(new StringStream('foo')); 80 | } 81 | 82 | public function invalidValues() 83 | { 84 | return [ 85 | [-1], 86 | ['/foo/bar'] 87 | ]; 88 | } 89 | 90 | public function testAssertion() 91 | { 92 | $int = new Int(8, ['assert' => 49]); 93 | $this->assertTrue($int->validate(49)); 94 | } 95 | 96 | /** 97 | * @expectedException \Zerg\Field\AssertException 98 | * */ 99 | public function testAssertionException() 100 | { 101 | (new Int(8, ['assert' => 50]))->parse(new StringStream('1')); 102 | } 103 | 104 | /** 105 | * @expectedException \Zerg\Field\AssertException 106 | * */ 107 | public function testCallbackAssertionException() 108 | { 109 | (new Int(8, ['assert' => function ($val, Int $field) { 110 | return $val !== 49; 111 | }]))->parse(new StringStream('1')); 112 | } 113 | 114 | public function testEndian() 115 | { 116 | $field = new Int('byte', ['endian' => Endian::ENDIAN_BIG]); 117 | $this->assertEquals(Endian::ENDIAN_BIG, $field->getEndian()); 118 | $this->assertEquals(Endian::ENDIAN_LITTLE, $field->setEndian(Endian::ENDIAN_LITTLE)->getEndian()); 119 | } 120 | 121 | 122 | /** 123 | * @expectedException \Zerg\Field\ConfigurationException 124 | * */ 125 | public function testEndianException() 126 | { 127 | $field = new Int('byte', ['endian' => 'little']); 128 | } 129 | 130 | public function testMassConfig() 131 | { 132 | $int1 = new Int(32, ['assert' => 10, 'signed' => true, 'endian' => Endian::ENDIAN_BIG]); 133 | $int2 = new Int([ 134 | 'size' => 32, 135 | 'assert' => 10, 136 | 'signed' => true, 137 | 'endian' => Endian::ENDIAN_BIG 138 | ]); 139 | $this->assertEquals($int1, $int2); 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/PaddingTest.php: -------------------------------------------------------------------------------- 1 | read($stream); 32 | $padding->parse($stream); 33 | 34 | $this->assertEquals($result, $int2->read($stream)); 35 | } 36 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/ScalarTest.php: -------------------------------------------------------------------------------- 1 | true, 17 | 'formatter' => $callback 18 | ]); 19 | 20 | $this->assertSame(8, $field->getSize()); 21 | $this->assertSame($callback, $field->getFormatter()); 22 | $this->assertTrue($field->getSigned()); 23 | } 24 | 25 | public function testValueCallback() 26 | { 27 | $stream = new StringStream('123abcdefgqwertyahnytjssdadfkjhb'); 28 | $field = new Int('byte', [ 29 | 'formatter' => function($value) { 30 | return $value - 2 * 8; 31 | } 32 | ]); 33 | 34 | $this->assertEquals(33, $field->parse($stream)); 35 | } 36 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/SizeableTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(2, $field->getSize()); 13 | 14 | $field->setSize('short'); 15 | $this->assertEquals(16, $field->getSize()); 16 | } 17 | 18 | public function testConditionalSize() 19 | { 20 | $field = new Int('semi_nibble'); 21 | 22 | $dataSet = new DataSet([ 23 | 'a' => [ 24 | 'b' => 4 25 | ], 26 | 'c' => 8, 27 | 'd' => [ 28 | 'g' => '/a/b', 29 | 'e' => [ 30 | 'f' => 16 31 | ] 32 | ] 33 | ]); 34 | 35 | $field->setDataSet($dataSet); 36 | 37 | $field->setSize('/c'); 38 | $this->assertEquals(8, $field->getSize()); 39 | 40 | $field->setSize('/d/e/f'); 41 | $this->assertEquals(16, $field->getSize()); 42 | 43 | $field->setSize('/d/g'); 44 | $this->assertEquals(4, $field->getSize()); 45 | 46 | } 47 | 48 | public function testSizeCallback() 49 | { 50 | $field = new Int(function() { 51 | return 6; 52 | }); 53 | 54 | $this->assertEquals(6, $field->getSize()); 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /tests/Zerg/Field/StringTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('ab', $string->read($stream)); 15 | } 16 | 17 | public function testAssertion() 18 | { 19 | $string = new String(8, ['assert' => '1']); 20 | $this->assertTrue($string->validate('1')); 21 | } 22 | 23 | /** 24 | * @expectedException \Zerg\Field\AssertException 25 | * */ 26 | public function testAssertionException() 27 | { 28 | (new String(8, ['assert' => '2']))->parse(new StringStream('1')); 29 | } 30 | 31 | public function testMassConfig() 32 | { 33 | $formatter = function ($value) { 34 | return $value . ';'; 35 | }; 36 | $string1 = new String(30, ['assert' => 'qwer', 'endian' => Endian::ENDIAN_BIG, 'formatter' => $formatter]); 37 | $string2 = new String([ 38 | 'size' => 30, 39 | 'assert' => 'qwer', 40 | 'endian' => Endian::ENDIAN_BIG, 41 | 'formatter' => $formatter 42 | ]); 43 | $this->assertEquals($string1, $string2); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /tests/Zerg/Stream/FileStreamTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('1', $stream->getBuffer()->read(8)); 11 | $stream->skip(16); 12 | $this->assertEquals('a', $stream->getBuffer()->read(8)); 13 | $stream->skip(8); 14 | $this->assertEquals('c', $stream->getBuffer()->read(8)); 15 | $this->assertEquals('d', $stream->getBuffer()->read(8)); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/Zerg/Stream/StringStreamTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('1', $stream->getBuffer()->read(8)); 14 | $stream->skip(16); 15 | $this->assertEquals('a', $stream->getBuffer()->read(8)); 16 | $stream->skip(8); 17 | $this->assertEquals('c', $stream->getBuffer()->read(8)); 18 | $this->assertEquals('d', $stream->getBuffer()->read(8)); 19 | $stream->getBuffer()->read(200 * 8); 20 | } 21 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('Zerg\\', __DIR__.'/Zerg'); -------------------------------------------------------------------------------- /tests/data: -------------------------------------------------------------------------------- 1 | 123abcdefg --------------------------------------------------------------------------------