From a2ac4438707fe0b870eae60f3706201a46ca86c2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 4 Jul 2016 01:43:23 +0200 Subject: [PATCH] First commit (master) --- .gitignore | 1 + LICENSE | 18 +++ README.md | 27 ++++ composer.json | 22 +++ example.php | 6 + lib/danog/RightPack/ParserException.php | 19 +++ lib/danog/RightPack/RightPacker.php | 192 ++++++++++++++++++++++++ 7 files changed, 285 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 example.php create mode 100644 lib/danog/RightPack/ParserException.php create mode 100644 lib/danog/RightPack/RightPacker.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c0be628 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +The MIT License (MIT) + +Copyright (c) 2016 Daniil Gentili + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..61644cf --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# rightpack class + +Licensed under MIT. + +PHP's pack() and unpack(), done the right way. + +This library was created to help me develop a [client for the mtproto protocol](https://github.com/danog/MadelineProto). + +The format syntax is exactly the one used in python's struct (https://docs.python.org/2/library/struct.html) + +For now custom byte size is not fully supported, as well as p and P formats. + +## Installation + +Install using composer: +``` +composer require danog/rightpack +``` + +# Usage + +``` +require('vendor/autoload.php'); +$rightpack = new \danog\RightPack\RightPacker(); +``` + +[Daniil Gentili](http://daniil.it) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..cdae94f --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "danog/rightpack", + "type": "library", + "description": "PHP's pack() and unpack(), done the right way.", + "license": "MIT", + "homepage": "https://daniil.it/rightpack", + "keywords": ["pack", "unpack", "byte", "bytes", "python", "struct"], + "authors": [ + { + "name": "danog", + "email": "daniil@daniil.it" + } + ], + "require": { + "php": ">=5.6.0" + }, + "autoload": { + "psr-0": { + "danog\\RightPack\\": "lib/" + } + } +} diff --git a/example.php b/example.php new file mode 100644 index 0000000..db240c7 --- /dev/null +++ b/example.php @@ -0,0 +1,6 @@ +pack("cc", "a", "s")); diff --git a/lib/danog/RightPack/ParserException.php b/lib/danog/RightPack/ParserException.php new file mode 100644 index 0000000..bcb3b98 --- /dev/null +++ b/lib/danog/RightPack/ParserException.php @@ -0,0 +1,19 @@ + + * @license MIT license +*/ + +/* Just an exception class */ +class ParserException extends \Exception +{ +} diff --git a/lib/danog/RightPack/RightPacker.php b/lib/danog/RightPack/RightPacker.php new file mode 100644 index 0000000..32a95d2 --- /dev/null +++ b/lib/danog/RightPack/RightPacker.php @@ -0,0 +1,192 @@ + + * @license MIT license +*/ +class RightPacker { + /** + * Constructor + * + * Sets modifiers and gets endianness + * + */ + public function __construct(){ + $this->BIG_ENDIAN = (pack('L', 1) === pack('N', 1)); + $this->MODIFIERS = [ + "<" => ["BIG_ENDIAN" => false], + ">" => ["BIG_ENDIAN" => true], + "!" => ["BIG_ENDIAN" => true], + "@" => ["BIG_ENDIAN" => $this->BIG_ENDIAN], + "=" => ["BIG_ENDIAN" => $this->BIG_ENDIAN] + ]; + $this->FORMATS = [ + // These formats need to be modified after/before encoding/decoding. + "p" => "p", // “Pascal string”, meaning a short variable-length string stored in a fixed number of bytes, given by the count. The first byte stored is the length of the string, or 255, whichever is smaller. The bytes of the string follow. If the string passed in to pack() is too long (longer than the count minus 1), only the leading count-1 bytes of the string are stored. If the string is shorter than count-1, it is padded with null bytes so that exactly count bytes in all are used. Note that for unpack(), the 'p' format character consumes count bytes, but that the string returned can never contain more than 255 characters. + "P" => "P", // integer or long integer, depending on the size needed to hold a pointer when it has been cast to an integer type. A NULL pointer will always be returned as the Python integer 0. When packing pointer-sized values, Python integer or long integer objects may be used. For example, the Alpha and Merced processors use 64-bit pointer values, meaning a Python long integer will be used to hold the pointer; other platforms use 32-bit pointers and will use a Python integer. + + // These formats have automatical byte size, this must be fixed. + "i" => "i", // should be 4 (32 bit) + "I" => "I", // should be 4 (32 bit) + "f" => "f", // should be 4 (32 bit) + "d" => "d", // should be 8 (64 bit) + + // These formats should work exactly as in python's struct (same byte size, etc). + "c" => "a", + "?" => "c", + "x" => "x", + "b" => "c", + "B" => "C", + "h" => "s", + "H" => "S", + "l" => "l", + "L" => "L", + "q" => "q", + "Q" => "Q", + "s" => "a", + ]; + + } + + /** + * Pack + * + * Packs data into bytes + * + * @param $format Format string + * @param ...$data Parameters to encode + * @return Encoded data + */ + public function pack($format, ...$data) { + $result = null; // Data to return + $packcommand = $this->parseformat($format, $this->array_total_strlen($data), $this->array_each_strlen($data)); // Get pack parameters + + foreach ($packcommand as $key => $command) { + $curresult = pack($this->FORMATS[$command["format"]].$command["count"], $data[$key]); // Pack current char + if(isset($command["modifiers"]["BIG_ENDIAN"]) && ((!$this->BIG_ENDIAN && $command["modifiers"]["BIG_ENDIAN"]) || ($this->BIG_ENDIAN && !$command["modifiers"]["BIG_ENDIAN"]))) $curresult = strrev($curresult); // Reverse if wrong endianness + $result .= $curresult; + } + return $result; + } + /** + * Unpack + * + * Unpacks data into bytes + * + * @param $format Format string + * @param $data Data to decode + * @return Decoded data + */ + public function unpack($format, $data) { + $result = []; // Data to return + $packcommand = $this->parseformat($format, strlen($data)); // Get unpack parameters + foreach ($packcommand as $key => $command) { + if(isset($command["modifiers"]["BIG_ENDIAN"]) && ((!$this->BIG_ENDIAN && $command["modifiers"]["BIG_ENDIAN"]) || ($this->BIG_ENDIAN && !$command["modifiers"]["BIG_ENDIAN"]))) $data[$key] = strrev($data[$key]); // Reverse if wrong endianness + $result[$key] = unpack($this->FORMATS[$command["format"]].$command["count"], $data[$key])[1]; // Pack current char + switch ($command["format"]) { + case '?': + if ($result[$key] == 0) $result[$key] = false; else $result[$key] = true; + break; + default: + break; + } + } + return $result; + } + + /** + * Parse format + * + * Parses format string. + * + * @throws ParserException if format string is too long or there aren't enough parameters or if an unkown format or modifier is supplied. + * @param $format Format string to parse + * @param $count Number of chars in all objects to encode/decode + * @param $arraycount (Optional) Array containing the number of chars contained in each element of the array to pack + * @return Array with format and modifiers for each object to encode/decode + */ + public function parseformat($format, $count, $arraycount = null) { + $formatcharcount = 0; // Current element to decode/encodeù + $charcount = 0; // Current char + + $result = []; // Array with the results + foreach (str_split($format) as $offset => $currentformatchar) { // Current format char + if(!isset($result[$formatcharcount]) || !is_array($result[$formatcharcount])) { + $result[$formatcharcount] = []; // Create array for current element + } + + $result[$formatcharcount]["count"] = 0; // Set the count of the objects to decode for the current format char to 0 + + if(isset($this->MODIFIERS[$currentformatchar])) { // If current format char is a modifier + $result[$formatcharcount]["modifiers"] = $this->MODIFIERS[$currentformatchar]; // Set the modifiers for the current format char + } else if(is_int($currentformatchar) && ($currentformatchar > 0 || $currentformatchar <= 9)) { + $result[$formatcharcount]["count"] .= $currentformatchar; // Set the count for the current format char + } else if($currentformatchar == "*") { + $result[$formatcharcount]["count"] = $count - $formatcharcount; // Set the count for the current format char + } else if(isset($this->FORMATS[$currentformatchar])) { + if(!isset($result[$formatcharcount]["count"]) || $result[$formatcharcount]["count"] == 0 || $result[$formatcharcount]["count"] == null) { + $result[$formatcharcount]["count"] = 1; // Set count to 1 if something's wrong. + } + $result[$formatcharcount]["format"] = $currentformatchar; // Set format + $charcount += $result[$formatcharcount]["count"]; + + if($arraycount !== null) { + if($formatcharcount + 1 > count($arraycount)) { + throw new ParserException("Format string too long or not enough parameters at offset ".$offset."."); + } + if($result[$formatcharcount]["count"] > $arraycount[$formatcharcount]) { + throw new ParserException("Format string too long for offset ".$offset."."); + } + } + if($charcount > $count) { + throw new ParserException("Format string too long or not enough chars (total char count is bigger than provided char count)."); + } + $formatcharcount++; // Increase element count + + + } else throw new ParserException("Unkown format or modifier supplied (".$currentformatchar." at offset ".$offset.")."); + } + if($charcount != $count) { + throw new ParserException("Too many parameters or not enough format chars were specified."); + } + return $result; + } + /** + * Array each strlen + * + * Get length of each array element. + * + * @param $array Array to parse + * @return Array with lengths + **/ + public function array_each_strlen($array) { + foreach ($array as $key => &$value) { + $value = strlen($array[$key]); + } + return $array; + } + /** + * Array total strlen + * + * Get total length of every array element. + * + * @param $array Array to parse + * @return Integer with the total length + **/ + public function array_total_strlen($array) { + $count = 0; + foreach ($array as $value) { + $count += strlen($value); + } + return $count; + } +}