├── tests ├── _files │ ├── testMissingAnnounce.torrent │ ├── testMissingInfo.torrent │ ├── valid.torrent │ ├── large_files.torrent │ ├── large_file.img.torrent │ ├── file_with_private_set_to_0.torrent │ └── file_with_private_set_to_1.torrent ├── _extra_files │ └── extra.torrent ├── EncoderTest.php ├── DecoderTest.php └── TorrentTest.php ├── phpstan.neon ├── .gitignore ├── phpunit.xml.dist ├── LICENSE ├── src ├── EncoderInterface.php ├── Encoder.php ├── DecoderInterface.php ├── Decoder.php └── Torrent.php ├── .github └── workflows │ └── default.yml ├── ChangeLog.md ├── composer.json ├── README.md └── composer.lock /tests/_files/testMissingAnnounce.torrent: -------------------------------------------------------------------------------- 1 | d3:foo3:bare -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | checkMissingIterableValueType: false -------------------------------------------------------------------------------- /tests/_files/testMissingInfo.torrent: -------------------------------------------------------------------------------- 1 | d8:announce15:http://tracker/e -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vendor 3 | composer.phar 4 | phpunit.xml 5 | PHP_BitTorrent-*.tgz 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /tests/_files/valid.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christeredvartsen/php-bittorrent/HEAD/tests/_files/valid.torrent -------------------------------------------------------------------------------- /tests/_extra_files/extra.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christeredvartsen/php-bittorrent/HEAD/tests/_extra_files/extra.torrent -------------------------------------------------------------------------------- /tests/_files/large_files.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christeredvartsen/php-bittorrent/HEAD/tests/_files/large_files.torrent -------------------------------------------------------------------------------- /tests/_files/large_file.img.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christeredvartsen/php-bittorrent/HEAD/tests/_files/large_file.img.torrent -------------------------------------------------------------------------------- /tests/_files/file_with_private_set_to_0.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christeredvartsen/php-bittorrent/HEAD/tests/_files/file_with_private_set_to_0.torrent -------------------------------------------------------------------------------- /tests/_files/file_with_private_set_to_1.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christeredvartsen/php-bittorrent/HEAD/tests/_files/file_with_private_set_to_1.torrent -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2020, Christer Edvartsen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | * The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/EncoderInterface.php: -------------------------------------------------------------------------------- 1 | =7.2" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^8.5", 22 | "phploc/phploc": "^5.0", 23 | "phpstan/phpstan": "^0.12" 24 | }, 25 | "support": { 26 | "source": "https://github.com/christeredvartsen/php-bittorrent", 27 | "issues": "https://github.com/christeredvartsen/php-bittorrent/issues" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "BitTorrent\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "BitTorrent\\": "tests/" 37 | } 38 | }, 39 | "scripts": { 40 | "clean:build": "rm -rf build", 41 | "qa:lint": "for file in `git ls-files '*php'`; do php -l $file; done", 42 | "qa:phpstan": "vendor/bin/phpstan analyse src -l max --no-progress", 43 | "qa:phploc": [ 44 | "rm -rf build/artifacts/phploc", 45 | "mkdir -p build/artifacts/phploc", 46 | "vendor/bin/phploc --log-xml build/artifacts/phploc/phploc.xml src" 47 | ], 48 | "qa": [ 49 | "@qa:lint", 50 | "@qa:phploc", 51 | "@qa:phpstan" 52 | ], 53 | "test:phpunit": "vendor/bin/phpunit --verbose", 54 | "test:phpunit:coverage": [ 55 | "rm -rf build/artifacts/phpunit/coverage", 56 | "mkdir -p build/artifacts/phpunit/coverage", 57 | "vendor/bin/phpunit --verbose --coverage-text --coverage-html build/artifacts/phpunit/coverage" 58 | ], 59 | "test": [ 60 | "@test:phpunit" 61 | ], 62 | "ci": [ 63 | "@qa", 64 | "@test" 65 | ] 66 | } 67 | } -------------------------------------------------------------------------------- /src/Encoder.php: -------------------------------------------------------------------------------- 1 | false, 15 | ]; 16 | 17 | /** 18 | * Class constructor 19 | * 20 | * @param array $params Parameters for the encoder 21 | */ 22 | public function __construct(array $params = []) { 23 | $this->params = array_replace($this->params, $params); 24 | } 25 | 26 | public function encode($var) : string { 27 | if (is_int($var)) { 28 | return $this->encodeInteger($var); 29 | } else if (is_string($var)) { 30 | return $this->encodeString($var); 31 | } else if (is_array($var)) { 32 | $size = count($var); 33 | 34 | if (!$size && $this->params['encodeEmptyArrayAsDictionary']) { 35 | return $this->encodeDictionary($var); 36 | } 37 | 38 | for ($i = 0; $i < $size; $i++) { 39 | if (!isset($var[$i])) { 40 | return $this->encodeDictionary($var); 41 | } 42 | } 43 | 44 | return $this->encodeList($var); 45 | } 46 | 47 | throw new InvalidArgumentException(sprintf('Variables of type %s can not be encoded.', gettype($var))); 48 | } 49 | 50 | public function encodeInteger(int $integer) : string { 51 | return sprintf('i%de', $integer); 52 | } 53 | 54 | public function encodeString(string $string) : string { 55 | return sprintf('%d:%s', strlen($string), $string); 56 | } 57 | 58 | public function encodeList(array $list) : string { 59 | $encodedList = array_map(function($value) : string { 60 | return $this->encode($value); 61 | }, $list); 62 | 63 | return sprintf('l%se', implode('', $encodedList)); 64 | } 65 | 66 | public function encodeDictionary(array $dictionary) : string { 67 | ksort($dictionary); 68 | 69 | $encodedDictionary = ''; 70 | 71 | foreach ($dictionary as $key => $value) { 72 | $encodedDictionary .= $this->encodeString((string) $key) . $this->encode($value); 73 | } 74 | 75 | return sprintf('d%se', $encodedDictionary); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/DecoderInterface.php: -------------------------------------------------------------------------------- 1 | encodeString('Some string')); // string(14) "11:Some string" 24 | var_dump($encoder->encodeInteger(42)); // string(4) "i42e" 25 | var_dump($encoder->encodeList([1, 2, 3]); // string(11) "li1ei2ei3ee" 26 | var_dump($encoder->encodeDictionary(['foo' => 'bar', 'bar' => 'foo']); // string(22) "d3:foo3:bar3:bar3:fooe" 27 | ``` 28 | 29 | There is also a convenience method simply called `encode` in the `BitTorrent\Encoder` class that can be used to encode all encodable variables (integers, strings and arrays). 30 | 31 | ### Decode BitTorrent encoded data 32 | 33 | ```php 34 | decodeString('11:Some string')); // string(11) "Some string" 40 | var_dump($decoder->decodeInteger('i42e')); // int(42) 41 | var_dump($decoder->decodeList('li1ei2ei3ee'); // array(3) { [0]=> int(1) [1]=> int(2) [2]=> int(3) } 42 | var_dump($decoder->decodeDictionary('d3:foo3:bar3:bar3:fooe'); // array(2) { ["foo"]=> string(3) "bar" ["bar"]=> string(3) "foo" } 43 | ``` 44 | 45 | There is also a convenience method called `decode` that can decode any BitTorrent encoded data. 46 | 47 | ### Decode torrent files 48 | 49 | The decoder class also has a method for decoding a torrent file (which is an encoded dictionary): 50 | 51 | ```php 52 | decodeFile('/path/to/file.torrent'); 57 | ``` 58 | 59 | ### Create new torrent files and open existing ones 60 | 61 | The `BitTorrent\Torrent` class represents a torrent file and can be used to create torrent files. 62 | 63 | ```php 64 | withComment('Some comment'); 69 | 70 | $torrent->save('/save/to/path/file.torrent'); 71 | ``` 72 | 73 | The class can also load a torrent file: 74 | 75 | ```php 76 | withAnnounce('http://tracker/announce.php') // Override announce in original file 81 | ->withComment('Some comment'); // Override commend in original file 82 | 83 | $torrent->save('/save/to/path/file.torrent'); // Save to a new file 84 | ``` 85 | 86 | ## License 87 | Licensed under the MIT License. 88 | 89 | See [LICENSE](LICENSE) file. 90 | -------------------------------------------------------------------------------- /tests/EncoderTest.php: -------------------------------------------------------------------------------- 1 | encoder = new Encoder(); 16 | } 17 | 18 | public function getEncodeIntegerData() : array { 19 | return [ 20 | [-1, 'i-1e'], 21 | [0, 'i0e'], 22 | [1, 'i1e'], 23 | ]; 24 | } 25 | 26 | /** 27 | * @dataProvider getEncodeIntegerData 28 | * @covers ::encodeInteger 29 | */ 30 | public function testEncodeInteger(int $value, string $encoded) : void { 31 | $this->assertSame($encoded, $this->encoder->encodeInteger($value)); 32 | } 33 | 34 | public function getEncodeStringData() : array { 35 | return [ 36 | ['spam', '4:spam'], 37 | ['foobar', '6:foobar'], 38 | ['foo:bar', '7:foo:bar'], 39 | ]; 40 | } 41 | 42 | /** 43 | * @dataProvider getEncodeStringData 44 | * @covers ::encodeString 45 | */ 46 | public function testEncodeString(string $value, string $encoded) : void { 47 | $this->assertSame($encoded, $this->encoder->encodeString($value)); 48 | } 49 | 50 | public function getEncodeListData() : array { 51 | return [ 52 | [['spam', 1, [1]], 'l4:spami1eli1eee'], 53 | ]; 54 | } 55 | 56 | /** 57 | * @dataProvider getEncodeListData 58 | * @covers ::encodeList 59 | */ 60 | public function testEncodeList(array $value, string $encoded) : void { 61 | $this->assertSame($encoded, $this->encoder->encodeList($value)); 62 | } 63 | 64 | public function getEncodeDictionaryData() : array { 65 | return [ 66 | [['1' => 'foo', 'foo' => 'bar', 'list' => [1, 2, 3]], 'd3:foo3:bar4:listli1ei2ei3ee1:13:fooe'], 67 | [['foo' => 'bar', 'spam' => 'eggs'], 'd3:foo3:bar4:spam4:eggse'], 68 | [['spam' => 'eggs', 'foo' => 'bar'], 'd3:foo3:bar4:spam4:eggse'], 69 | ]; 70 | } 71 | 72 | /** 73 | * @dataProvider getEncodeDictionaryData 74 | * @covers ::encodeDictionary 75 | */ 76 | public function testEncodeDictionary(array $value, string $encoded) : void { 77 | $this->assertSame($encoded, $this->encoder->encodeDictionary($value)); 78 | } 79 | 80 | public function getEncodeData() : array { 81 | return [ 82 | [1, 'i1e'], 83 | ['spam', '4:spam'], 84 | [[1, 2, 3], 'li1ei2ei3ee'], 85 | [['foo' => 'bar', 'spam' => 'sucks'], 'd3:foo3:bar4:spam5:suckse'], 86 | ]; 87 | } 88 | 89 | /** 90 | * @dataProvider getEncodeData 91 | * @covers ::encode 92 | */ 93 | public function testEncodeUsingGenericMethod($value, string $encoded) : void { 94 | $this->assertSame($encoded, $this->encoder->encode($value)); 95 | } 96 | 97 | /** 98 | * @covers ::encode 99 | */ 100 | public function testEncodeNonSupportedType() : void { 101 | $this->expectException(InvalidArgumentException::class); 102 | $this->expectExceptionMessage('Variables of type object can not be encoded.'); 103 | $this->encoder->encode(new stdClass()); 104 | } 105 | 106 | /** 107 | * @covers ::__construct 108 | * @covers ::encode 109 | */ 110 | public function testCanEncodeEmptyArraysAsDictionaries() : void { 111 | $encoder = new Encoder(); 112 | $this->assertSame('le', $encoder->encode([])); 113 | 114 | $encoder = new Encoder([ 115 | 'encodeEmptyArrayAsDictionary' => true, 116 | ]); 117 | $this->assertSame('de', $encoder->encode([])); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Decoder.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder ?? new Encoder(); 19 | } 20 | 21 | public function decodeFile(string $path, bool $strict = false) : array { 22 | if (!is_readable($path)) { 23 | throw new InvalidArgumentException(sprintf('File %s does not exist or can not be read.', $path)); 24 | } 25 | 26 | /** @var string */ 27 | $contents = file_get_contents($path, true); 28 | 29 | return $this->decodeFileContents($contents, $strict); 30 | } 31 | 32 | public function decodeFileContents(string $contents, bool $strict = false) : array { 33 | $dictionary = $this->decodeDictionary($contents); 34 | 35 | if ($strict) { 36 | if (!isset($dictionary['announce']) || !is_string($dictionary['announce']) && !empty($dictionary['announce'])) { 37 | throw new InvalidArgumentException('Missing or empty "announce" key.'); 38 | } else if (!isset($dictionary['info']) || !is_array($dictionary['info']) && !empty($dictionary['info'])) { 39 | throw new InvalidArgumentException('Missing or empty "info" key.'); 40 | } 41 | } 42 | 43 | return $dictionary; 44 | } 45 | 46 | public function decode(string $string) { 47 | if ('i' === $string[0]) { 48 | return $this->decodeInteger($string); 49 | } else if ('l' === $string[0]) { 50 | return $this->decodeList($string); 51 | } else if ('d' === $string[0]) { 52 | return $this->decodeDictionary($string); 53 | } else if (preg_match('/^\d+:/', $string)) { 54 | return $this->decodeString($string); 55 | } 56 | 57 | throw new InvalidArgumentException('Parameter is not correctly encoded.'); 58 | } 59 | 60 | public function decodeInteger(string $integer) : int { 61 | if ('i' !== $integer[0] || (!$ePos = strpos($integer, 'e'))) { 62 | throw new InvalidArgumentException('Invalid integer. Integers must start wth "i" and end with "e".'); 63 | } 64 | 65 | $integer = substr($integer, 1, ($ePos - 1)); 66 | $len = strlen($integer); 67 | 68 | if (('0' === $integer[0] && $len > 1) || ('-' === $integer[0] && '0' === $integer[1]) || !is_numeric($integer)) { 69 | throw new InvalidArgumentException('Invalid integer value.'); 70 | } 71 | 72 | return (int) $integer; 73 | } 74 | 75 | public function decodeString(string $string) : string { 76 | $stringParts = explode(':', $string, 2); 77 | 78 | if (2 !== count($stringParts)) { 79 | throw new InvalidArgumentException('Invalid string. Strings consist of two parts separated by ":".'); 80 | } 81 | 82 | $length = (int) $stringParts[0]; 83 | $lengthLen = strlen((string) $length); 84 | 85 | if (($lengthLen + 1 + $length) > strlen($string)) { 86 | throw new InvalidArgumentException('The length of the string does not match the prefix of the encoded data.'); 87 | } 88 | 89 | return substr($string, ($lengthLen + 1), $length); 90 | } 91 | 92 | public function decodeList(string $list) : array { 93 | if ('l' !== $list[0]) { 94 | throw new InvalidArgumentException('Parameter is not an encoded list.'); 95 | } 96 | 97 | $ret = []; 98 | 99 | $length = strlen($list); 100 | $i = 1; 101 | 102 | while ($i < $length) { 103 | if ('e' === $list[$i]) { 104 | break; 105 | } 106 | 107 | $part = substr($list, $i); 108 | $decodedPart = $this->decode($part); 109 | $ret[] = $decodedPart; 110 | $i += strlen($this->encoder->encode($decodedPart)); 111 | } 112 | 113 | return $ret; 114 | } 115 | 116 | public function decodeDictionary(string $dictionary) : array { 117 | if ('d' !== $dictionary[0]) { 118 | throw new InvalidArgumentException('Parameter is not an encoded dictionary.'); 119 | } 120 | 121 | $length = strlen($dictionary); 122 | $ret = []; 123 | $i = 1; 124 | 125 | while ($i < $length) { 126 | if ('e' === $dictionary[$i]) { 127 | break; 128 | } 129 | 130 | $keyPart = substr($dictionary, $i); 131 | $key = $this->decodeString($keyPart); 132 | $keyPartLength = strlen($this->encoder->encodeString($key)); 133 | 134 | $valuePart = substr($dictionary, ($i + $keyPartLength)); 135 | $value = $this->decode($valuePart); 136 | $valuePartLength = strlen($this->encoder->encode($value)); 137 | 138 | $ret[$key] = $value; 139 | $i += ($keyPartLength + $valuePartLength); 140 | } 141 | 142 | return $ret; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/DecoderTest.php: -------------------------------------------------------------------------------- 1 | decoder = new Decoder(); 15 | } 16 | 17 | public function getDecodeIntegerData() : array { 18 | return [ 19 | ['i1e', 1], 20 | ['i-1e', -1], 21 | ['i0e', 0], 22 | ]; 23 | } 24 | 25 | /** 26 | * @dataProvider getDecodeIntegerData 27 | * @covers ::decodeInteger 28 | */ 29 | public function testDecoderInteger(string $encoded, int $value) : void { 30 | $this->assertEquals($value, $this->decoder->decodeInteger($encoded)); 31 | } 32 | 33 | public function getDecodeInvalidIntegerData() : array { 34 | return [ 35 | ['i01e'], 36 | ['i-01e'], 37 | ['ifoobare'], 38 | ]; 39 | } 40 | 41 | /** 42 | * @dataProvider getDecodeInvalidIntegerData 43 | * @covers ::decodeInteger 44 | */ 45 | public function testDecodeInvalidInteger(string $value) : void { 46 | $this->expectException(InvalidArgumentException::class); 47 | $this->expectExceptionMessage('Invalid integer value.'); 48 | $this->decoder->decodeInteger($value); 49 | } 50 | 51 | /** 52 | * @covers ::decodeInteger 53 | */ 54 | public function testDecodeStringAsInteger() : void { 55 | $this->expectException(InvalidArgumentException::class); 56 | $this->expectExceptionMessage('Invalid integer. Integers must start wth "i" and end with "e".'); 57 | $this->decoder->decodeInteger('4:spam'); 58 | } 59 | 60 | /** 61 | * @covers ::decodeInteger 62 | */ 63 | public function testDecodePartialInteger() : void { 64 | $this->expectException(InvalidArgumentException::class); 65 | $this->expectExceptionMessage('Invalid integer. Integers must start wth "i" and end with "e".'); 66 | $this->decoder->decodeInteger('i10'); 67 | } 68 | 69 | public function getDecodeStringData() : array { 70 | return [ 71 | ['4:spam', 'spam'], 72 | ['11:test string', 'test string'], 73 | ['3:foobar', 'foo'], 74 | ]; 75 | } 76 | 77 | /** 78 | * @dataProvider getDecodeStringData 79 | * @covers ::decodeString 80 | */ 81 | public function testDecodeString(string $encoded, string $value) : void { 82 | $this->assertSame($value, $this->decoder->decodeString($encoded)); 83 | } 84 | 85 | /** 86 | * @covers ::decodeString 87 | */ 88 | public function testDecodeInvalidString() : void { 89 | $this->expectException(InvalidArgumentException::class); 90 | $this->expectExceptionMessage('Invalid string. Strings consist of two parts separated by ":".'); 91 | $this->decoder->decodeString('4spam'); 92 | } 93 | 94 | /** 95 | * @covers ::decodeString 96 | */ 97 | public function testDecodeStringWithInvalidLength() : void { 98 | $this->expectException(InvalidArgumentException::class); 99 | $this->expectExceptionMessage('The length of the string does not match the prefix of the encoded data.'); 100 | $this->decoder->decodeString('6:spam'); 101 | } 102 | 103 | public function getDecodeListData() : array { 104 | return [ 105 | ['li1ei2ei3ee', [1, 2, 3]], 106 | ]; 107 | } 108 | 109 | /** 110 | * @dataProvider getDecodeListData 111 | * @covers ::decodeList 112 | */ 113 | public function testDecodeList(string $encoded, array $value) : void { 114 | $this->assertEquals($value, $this->decoder->decodeList($encoded)); 115 | } 116 | 117 | /** 118 | * @covers ::decodeList 119 | */ 120 | public function testDecodeInvalidList() : void { 121 | $this->expectException(InvalidArgumentException::class); 122 | $this->expectExceptionMessage('Parameter is not an encoded list.'); 123 | $this->decoder->decodeList('4:spam'); 124 | } 125 | 126 | public function getDecodeDictionaryData() : array { 127 | return [ 128 | ['d3:foo3:bar4:spam4:eggse', ['foo' => 'bar', 'spam' => 'eggs']], 129 | ]; 130 | } 131 | 132 | /** 133 | * @dataProvider getDecodeDictionaryData 134 | * @covers ::decodeDictionary 135 | */ 136 | public function testDecodeDictionary(string $encoded, array $value) : void { 137 | $this->assertSame($value, $this->decoder->decodeDictionary($encoded)); 138 | } 139 | 140 | /** 141 | * @covers ::decodeDictionary 142 | */ 143 | public function testDecodeInvalidDictionary() : void { 144 | $this->expectException(InvalidArgumentException::class); 145 | $this->expectExceptionMessage('Parameter is not an encoded dictionary.'); 146 | $this->decoder->decodeDictionary('4:spam'); 147 | } 148 | 149 | public function getGenericDecodeData() : array { 150 | return [ 151 | ['i1e', 1], 152 | ['4:spam', 'spam'], 153 | ['li1ei2ei3ee', [1, 2, 3]], 154 | ['d3:foo3:bare', ['foo' => 'bar']], 155 | ]; 156 | } 157 | 158 | /** 159 | * @dataProvider getGenericDecodeData 160 | * @covers ::__construct 161 | * @covers ::decode 162 | */ 163 | public function testGenericDecode(string $encoded, $value) : void { 164 | $this->assertEquals($value, $this->decoder->decode($encoded)); 165 | } 166 | 167 | /** 168 | * @covers ::decode 169 | */ 170 | public function testGenericDecodeWithInvalidData() : void { 171 | $this->expectException(InvalidArgumentException::class); 172 | $this->expectExceptionMessage('Parameter is not correctly encoded.'); 173 | $this->decoder->decode('foo'); 174 | } 175 | 176 | /** 177 | * @covers ::decodeFile 178 | * @covers ::decodeFileContents 179 | */ 180 | public function testDecodeTorrentFileStrictWithMissingAnnounce() : void { 181 | $this->expectException(InvalidArgumentException::class); 182 | $this->expectExceptionMessage('Missing or empty "announce" key.'); 183 | $this->decoder->decodeFile(__DIR__ . '/_files/testMissingAnnounce.torrent', true); 184 | } 185 | 186 | /** 187 | * @covers ::decodeFile 188 | * @covers ::decodeFileContents 189 | */ 190 | public function testDecodeTorrentFileStrictWithMissingInfo() : void { 191 | $this->expectException(InvalidArgumentException::class); 192 | $this->expectExceptionMessage('Missing or empty "info" key.'); 193 | $this->decoder->decodeFile(__DIR__ . '/_files/testMissingInfo.torrent', true); 194 | } 195 | 196 | /** 197 | * @covers ::decodeFile 198 | */ 199 | public function testDecodeNonReadableFile() : void { 200 | $this->expectException(InvalidArgumentException::class); 201 | $this->expectExceptionMessageRegExp('/^File .*nonExistingFile does not exist or can not be read.$/'); 202 | $this->decoder->decodeFile(__DIR__ . '/nonExistingFile'); 203 | } 204 | 205 | /** 206 | * @covers ::decodeFile 207 | * @covers ::decodeFileContents 208 | */ 209 | public function testDecodeFileWithStrictChecksEnabled() : void { 210 | $list = $this->decoder->decodeFile(__DIR__ . '/_files/valid.torrent', true); 211 | 212 | $this->assertIsArray($list); 213 | $this->assertArrayHasKey('announce', $list); 214 | $this->assertSame('http://trackerurl', $list['announce']); 215 | $this->assertArrayHasKey('comment', $list); 216 | $this->assertSame('This is a comment', $list['comment']); 217 | $this->assertArrayHasKey('creation date', $list); 218 | $this->assertEquals(1323713688, $list['creation date']); 219 | $this->assertArrayHasKey('info', $list); 220 | $this->assertIsArray($list['info']); 221 | $this->assertArrayHasKey('files', $list['info']); 222 | $this->assertSame(5, count($list['info']['files'])); 223 | $this->assertArrayHasKey('name', $list['info']); 224 | $this->assertSame('PHP', $list['info']['name']); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Torrent.php: -------------------------------------------------------------------------------- 1 | announceUrl = $announceUrl; 82 | } 83 | 84 | if (null === $encoder) { 85 | $encoder = new Encoder(); 86 | } 87 | 88 | $this->encoder = $encoder; 89 | } 90 | 91 | /** 92 | * Set piece length exponent 93 | * 94 | * @param int $pieceLengthExp 95 | * @return self 96 | */ 97 | public function withPieceLengthExp(int $pieceLengthExp) : self { 98 | $new = clone $this; 99 | $new->pieceLengthExp = $pieceLengthExp; 100 | 101 | return $new; 102 | } 103 | 104 | /** 105 | * Get the piece length exponent 106 | * 107 | * @return int 108 | */ 109 | public function getPieceLengthExp() : int { 110 | return $this->pieceLengthExp; 111 | } 112 | 113 | /** 114 | * Set the announce URL 115 | * 116 | * @param string $announceUrl 117 | * @return self 118 | */ 119 | public function withAnnounceUrl(string $announceUrl) : self { 120 | $new = clone $this; 121 | $new->announceUrl = $announceUrl; 122 | 123 | return $new; 124 | } 125 | 126 | /** 127 | * Get the announce URL 128 | * 129 | * @return ?string 130 | */ 131 | public function getAnnounceUrl() : ?string { 132 | return $this->announceUrl; 133 | } 134 | 135 | /** 136 | * Set the internal encoder 137 | * 138 | * @param EncoderInterface $encoder 139 | * @return self 140 | */ 141 | public function withEncoder(EncoderInterface $encoder) : self { 142 | $new = clone $this; 143 | $new->encoder = $encoder; 144 | 145 | return $new; 146 | } 147 | 148 | /** 149 | * Get the encoder 150 | * 151 | * @return EncoderInterface 152 | */ 153 | public function getEncoder() : EncoderInterface { 154 | return $this->encoder; 155 | } 156 | 157 | /** 158 | * Set announce URL list 159 | * 160 | * @param string[] $announceList 161 | * @return self 162 | */ 163 | public function withAnnounceList(array $announceList) : self { 164 | $new = clone $this; 165 | $new->announceList = $announceList; 166 | 167 | return $new; 168 | } 169 | 170 | /** 171 | * Get the announce list 172 | * 173 | * @return string[] 174 | */ 175 | public function getAnnounceList() : ?array { 176 | return $this->announceList; 177 | } 178 | 179 | /** 180 | * Set a comment 181 | * 182 | * @param string $comment 183 | * @return self 184 | */ 185 | public function withComment(string $comment) : self { 186 | $new = clone $this; 187 | $new->comment = $comment; 188 | 189 | return $new; 190 | } 191 | 192 | /** 193 | * Get the comment 194 | * 195 | * @return ?string 196 | */ 197 | public function getComment() : ?string { 198 | return $this->comment; 199 | } 200 | 201 | /** 202 | * Set the created by field 203 | * 204 | * @param string $createdBy 205 | * @return self 206 | */ 207 | public function withCreatedBy(string $createdBy) : self { 208 | $new = clone $this; 209 | $new->createdBy = $createdBy; 210 | 211 | return $new; 212 | } 213 | 214 | /** 215 | * Get the created by string 216 | * 217 | * @return ?string 218 | */ 219 | public function getCreatedBy() : ?string { 220 | return $this->createdBy; 221 | } 222 | 223 | /** 224 | * Set the created at field 225 | * 226 | * @param int $createdAt 227 | * @return self 228 | */ 229 | public function withCreatedAt(int $createdAt) : self { 230 | $new = clone $this; 231 | $new->createdAt = $createdAt; 232 | 233 | return $new; 234 | } 235 | 236 | /** 237 | * Get the created at field 238 | * 239 | * @return ?int 240 | */ 241 | public function getCreatedAt() : ?int { 242 | return $this->createdAt; 243 | } 244 | 245 | /** 246 | * Set additional info 247 | * 248 | * @param array $info 249 | * @return self 250 | */ 251 | public function withInfo(array $info) : self { 252 | $new = clone $this; 253 | $new->info = $info; 254 | 255 | return $new; 256 | } 257 | 258 | /** 259 | * Get additional info 260 | * 261 | * @return ?array 262 | */ 263 | public function getInfo() : ?array { 264 | return $this->info; 265 | } 266 | 267 | /** 268 | * Set extra metadata 269 | * 270 | * @param array $extra 271 | * @return self 272 | */ 273 | public function withExtraMeta(array $extra) : self { 274 | $new = clone $this; 275 | $new->extraMeta = $extra; 276 | 277 | return $new; 278 | } 279 | 280 | /** 281 | * Get extra metadata 282 | * 283 | * @return ?array 284 | */ 285 | public function getExtraMeta() : ?array { 286 | return $this->extraMeta; 287 | } 288 | 289 | /** 290 | * Save the current torrent object to the specified filename 291 | * 292 | * This method will save the current object to a file. If the file specified exists it will be 293 | * overwritten. 294 | * 295 | * @param string $path The path to save the file to 296 | * @throws RuntimeException 297 | * @return bool Returns true on success or false on failure 298 | */ 299 | public function save(string $path) : bool { 300 | if (!is_writable($path) && !is_writable(dirname($path))) { 301 | throw new InvalidArgumentException(sprintf('Could not open file "%s" for writing.', $path)); 302 | } 303 | 304 | if (null === $announceUrl = $this->getAnnounceUrl()) { 305 | throw new RuntimeException('Announce URL is missing'); 306 | } 307 | 308 | $torrent = [ 309 | 'announce' => $announceUrl, 310 | 'creation date' => $this->getCreatedAt() ?: time(), 311 | 'info' => $this->getInfoPart(), 312 | ]; 313 | 314 | if (null !== $announceList = $this->getAnnounceList()) { 315 | $torrent['announce-list'] = $announceList; 316 | } 317 | 318 | if (null !== $comment = $this->getComment()) { 319 | $torrent['comment'] = $comment; 320 | } 321 | 322 | if (null !== $createdBy = $this->getCreatedBy()) { 323 | $torrent['created by'] = $createdBy; 324 | } 325 | 326 | if (null !== ($extra = $this->getExtraMeta()) && is_array($extra)) { 327 | foreach ($extra as $extraKey => $extraValue) { 328 | if (array_key_exists($extraKey, $torrent)) { 329 | throw new RuntimeException(sprintf('Duplicate key in extra meta info. "%s" already exists.', $extraKey)); 330 | } 331 | 332 | $torrent[$extraKey] = $extraValue; 333 | } 334 | } 335 | 336 | return false !== file_put_contents($path, $this->encoder->encodeDictionary($torrent)); 337 | } 338 | 339 | /** 340 | * Get file list 341 | * 342 | * @return array 343 | */ 344 | public function getFileList() : array { 345 | $info = $this->getInfoPart(); 346 | 347 | if (isset($info['length'])) { 348 | return [$info['name']]; 349 | } 350 | 351 | return $info['files']; 352 | } 353 | 354 | /** 355 | * Get file size 356 | * 357 | * @return int 358 | */ 359 | public function getSize() : int { 360 | $info = $this->getInfoPart(); 361 | 362 | if (isset($info['length'])) { 363 | return $info['length']; 364 | } 365 | 366 | return (int) array_sum(array_map(function(array $file) : int { 367 | return $file['length']; 368 | }, $this->getFileList())); 369 | } 370 | 371 | /** 372 | * Get the name from the info part 373 | * 374 | * @return string 375 | */ 376 | public function getName() : string { 377 | return $this->getInfoPart()['name']; 378 | } 379 | 380 | /** 381 | * Get sha1 hash from the encoded info part 382 | * 383 | * @param bool $raw Whether or not to return the hash in raw format 384 | * @return string 385 | */ 386 | public function getHash(bool $raw = false) : string { 387 | return sha1( 388 | $this->encoder->encodeDictionary($this->getInfoPart()), 389 | $raw 390 | ); 391 | } 392 | 393 | /** 394 | * Get the encoded hash 395 | * 396 | * @return string 397 | */ 398 | public function getEncodedHash() : string { 399 | return urlencode($this->getHash(true)); 400 | } 401 | 402 | /** 403 | * Get the info part of the torrent 404 | * 405 | * @return array 406 | */ 407 | private function getInfoPart() : array { 408 | $info = $this->getInfo(); 409 | 410 | if (null === $info) { 411 | throw new RuntimeException('The info part of the torrent is not set.'); 412 | } 413 | 414 | return $info; 415 | } 416 | 417 | /** 418 | * Check if the torrent is private 419 | * 420 | * @return bool 421 | */ 422 | public function isPrivate() : bool { 423 | $info = $this->getInfoPart(); 424 | 425 | return (isset($info['private']) && 1 === $info['private']); 426 | } 427 | 428 | /** 429 | * Create torrent instance from a string 430 | * 431 | * @param string $contents 432 | * @param DecoderInterface $decoder 433 | * @param bool $strict 434 | * @return self 435 | */ 436 | static public function createFromString(string $contents, DecoderInterface $decoder, bool $strict = false) : self { 437 | return static::createFromDictionary($decoder->decodeFileContents($contents, $strict)); 438 | } 439 | 440 | /** 441 | * Create torrent instance from a torrent file 442 | * 443 | * @param string $path 444 | * @param DecoderInterface $decoder 445 | * @param bool $strict 446 | * @return self 447 | */ 448 | static public function createFromTorrentFile(string $path, DecoderInterface $decoder, bool $strict = false) : self { 449 | return static::createFromDictionary($decoder->decodeFile($path, $strict)); 450 | } 451 | 452 | /** 453 | * Create torrent instance from a dictionary 454 | * 455 | * @param array $dictionary 456 | * @return self 457 | */ 458 | static public function createFromDictionary(array $dictionary) : self { 459 | $torrent = new self(); 460 | 461 | if (isset($dictionary['announce'])) { 462 | $torrent = $torrent->withAnnounceUrl($dictionary['announce']); 463 | unset($dictionary['announce']); 464 | } 465 | 466 | if (isset($dictionary['announce-list'])) { 467 | $torrent = $torrent->withAnnounceList($dictionary['announce-list']); 468 | unset($dictionary['announce-list']); 469 | } 470 | 471 | if (isset($dictionary['comment'])) { 472 | $torrent = $torrent->withComment($dictionary['comment']); 473 | unset($dictionary['comment']); 474 | } 475 | 476 | if (isset($dictionary['created by'])) { 477 | $torrent = $torrent->withCreatedBy($dictionary['created by']); 478 | unset($dictionary['created by']); 479 | } 480 | 481 | if (isset($dictionary['creation date'])) { 482 | $torrent = $torrent->withCreatedAt($dictionary['creation date']); 483 | unset($dictionary['creation date']); 484 | } 485 | 486 | if (isset($dictionary['info'])) { 487 | $torrent = $torrent->withInfo($dictionary['info']); 488 | unset($dictionary['info']); 489 | } 490 | 491 | if (count($dictionary) > 0) { 492 | $torrent = $torrent->withExtraMeta($dictionary); 493 | } 494 | 495 | return $torrent; 496 | } 497 | 498 | /** 499 | * Create torrent instance from local path 500 | * 501 | * @param string $path 502 | * @param string $announceUrl 503 | * @throws RuntimeException 504 | * @return self 505 | */ 506 | static public function createFromPath(string $path, string $announceUrl = null) : self { 507 | $torrent = new self($announceUrl); 508 | $files = []; 509 | 510 | $absolutePath = realpath($path); 511 | $pathIsFile = false; 512 | 513 | if (false !== $absolutePath && is_file($absolutePath)) { 514 | $pathIsFile = true; 515 | $files[] = [ 516 | 'filename' => basename($absolutePath), 517 | 'filesize' => filesize($absolutePath), 518 | ]; 519 | } else if (false !== $absolutePath && is_dir($absolutePath)) { 520 | $iterator = new RecursiveIteratorIterator( 521 | new RecursiveDirectoryIterator($absolutePath, RecursiveDirectoryIterator::SKIP_DOTS) 522 | ); 523 | 524 | foreach ($iterator as $entry) { 525 | $files[] = [ 526 | 'filename' => str_replace($absolutePath . DIRECTORY_SEPARATOR, '', (string) $entry), 527 | 'filesize' => $entry->getSize(), 528 | ]; 529 | } 530 | } else { 531 | throw new InvalidArgumentException(sprintf('Invalid path: %s', $path)); 532 | } 533 | 534 | $info = [ 535 | 'piece length' => pow(2, $torrent->getPieceLengthExp()) 536 | ]; 537 | 538 | $pieces = []; 539 | 540 | if ($pathIsFile) { 541 | $filePath = dirname($absolutePath); 542 | $info['name'] = $files[0]['filename']; 543 | $info['length'] = $files[0]['filesize']; 544 | $position = 0; 545 | $fp = fopen($p = $filePath . DIRECTORY_SEPARATOR . $files[0]['filename'], 'rb'); 546 | 547 | if (false === $fp) { 548 | throw new RuntimeException(sprintf('Failed to open file: %s', $p)); 549 | } 550 | 551 | while ($position < $info['length']) { 552 | /** @var string */ 553 | $part = fread($fp, min($info['piece length'], $info['length'] - $position)); 554 | $pieces[] = sha1($part, true); 555 | 556 | $position += $info['piece length']; 557 | 558 | if ($position > $info['length']) { 559 | $position = $info['length']; 560 | } 561 | } 562 | 563 | fclose($fp); 564 | 565 | $pieces = implode('', $pieces); 566 | } else { 567 | $info['name'] = basename($absolutePath); 568 | 569 | usort($files, function(array $a, array $b) : int { 570 | return strcmp($a['filename'], $b['filename']); 571 | }); 572 | 573 | $part = ''; 574 | $done = 0; 575 | 576 | // Loop through all the files in the $files array to generate the pieces and the other 577 | // stuff in the info part of the torrent. Note that two files may be part of the same 578 | // piece since btmakemetafile uses cyclic buffers 579 | foreach ($files as $file) { 580 | $filename = $file['filename']; 581 | $filesize = $file['filesize']; 582 | 583 | $info['files'][] = [ 584 | 'length' => $filesize, 585 | 'path' => explode(DIRECTORY_SEPARATOR, $filename) 586 | ]; 587 | 588 | $position = 0; 589 | $fp = fopen($p = $absolutePath . DIRECTORY_SEPARATOR . $filename, 'rb'); 590 | 591 | if (false === $fp) { 592 | throw new RuntimeException(sprintf('Failed to open file: %s', $p)); 593 | } 594 | 595 | while ($position < $filesize) { 596 | $bytes = min(($filesize - $position), ($info['piece length'] - $done)); 597 | $part .= fread($fp, $bytes); 598 | 599 | $done += $bytes; 600 | $position += $bytes; 601 | 602 | if ($done === $info['piece length']) { 603 | $pieces[] = sha1($part, true); 604 | $part = ''; 605 | $done = 0; 606 | } 607 | } 608 | 609 | fclose($fp); 610 | } 611 | 612 | if ($done > 0) { 613 | $pieces[] = sha1($part, true); 614 | } 615 | 616 | $pieces = implode('', $pieces); 617 | } 618 | 619 | $info['pieces'] = $pieces; 620 | ksort($info); 621 | 622 | return $torrent->withInfo($info); 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /tests/TorrentTest.php: -------------------------------------------------------------------------------- 1 | torrent = new Torrent(); 16 | } 17 | 18 | /** 19 | * @covers ::__construct 20 | * @covers ::getEncoder 21 | */ 22 | public function testConstructor() : void { 23 | $torrent = new Torrent('http://sometracker'); 24 | $this->assertSame('http://sometracker', $torrent->getAnnounceUrl()); 25 | $this->assertInstanceOf(EncoderInterface::class, $torrent->getEncoder()); 26 | } 27 | 28 | /** 29 | * @covers ::withEncoder 30 | * @covers ::getEncoder 31 | */ 32 | public function testSetAndGetEncoder() : void { 33 | $encoder = $this->createMock(EncoderInterface::class); 34 | $torrent = (new Torrent('http://sometracker'))->withEncoder($encoder); 35 | $this->assertInstanceOf(EncoderInterface::class, $torrent->getEncoder()); 36 | } 37 | 38 | public function getDataForSettersAndGetters() : array { 39 | return [ 40 | 'Announce URL' => [ 41 | 'getter' => 'getAnnounceUrl', 42 | 'mutator' => 'withAnnounceUrl', 43 | 'value' => 'http://tracker', 44 | 'initial' => null, 45 | ], 46 | 'Piece length exponent' => [ 47 | 'getter' => 'getPieceLengthExp', 48 | 'mutator' => 'withPieceLengthExp', 49 | 'value' => 6, 50 | 'initial' => 18, 51 | ], 52 | 'Comment' => [ 53 | 'getter' => 'getComment', 54 | 'mutator' => 'withComment', 55 | 'value' => 'some comment', 56 | 'initial' => null, 57 | ], 58 | 'Announce list' => [ 59 | 'getter' => 'getAnnounceList', 60 | 'mutator' => 'withAnnounceList', 61 | 'value' => ['http://sometracker'], 62 | 'initial' => [], 63 | ], 64 | 'Created by' => [ 65 | 'getter' => 'getCreatedBy', 66 | 'mutator' => 'withCreatedBy', 67 | 'value' => 'Some creator', 68 | 'initial' => 'PHP BitTorrent', 69 | ], 70 | 'Created at' => [ 71 | 'getter' => 'getCreatedAt', 72 | 'mutator' => 'withCreatedAt', 73 | 'value' => 1567451302, 74 | 'initial' => null, 75 | ], 76 | 'Info' => [ 77 | 'getter' => 'getInfo', 78 | 'mutator' => 'withInfo', 79 | 'value' => ['length' => 123, 'name' => 'some name'], 80 | 'initial' => null, 81 | ], 82 | 'Extra meta' => [ 83 | 'getter' => 'getExtraMeta', 84 | 'mutator' => 'withExtraMeta', 85 | 'value' => ['foo' => 'bar'], 86 | 'initial' => null, 87 | ], 88 | ]; 89 | } 90 | 91 | /** 92 | * @dataProvider getDataForSettersAndGetters 93 | * @covers ::withAnnounceUrl 94 | * @covers ::withPieceLengthExp 95 | * @covers ::withComment 96 | * @covers ::withAnnounceList 97 | * @covers ::withCreatedBy 98 | * @covers ::withCreatedAt 99 | * @covers ::withInfo 100 | * @covers ::withExtraMeta 101 | * 102 | * @covers ::getAnnounceUrl 103 | * @covers ::getPieceLengthExp 104 | * @covers ::getComment 105 | * @covers ::getAnnounceList 106 | * @covers ::getCreatedBy 107 | * @covers ::getCreatedAt 108 | * @covers ::getInfo 109 | * @covers ::getExtraMeta 110 | */ 111 | public function testSettersAndGetters(string $getter, string $mutator, $value, $initial) : void { 112 | $this->assertSame($initial, $this->torrent->$getter(), 'Incorrect initial value'); 113 | $torrent = $this->torrent->$mutator($value); 114 | $this->assertSame($initial, $this->torrent->$getter(), 'Incorrect value after setting'); 115 | $this->assertSame($value, $torrent->$getter(), 'Incorrect value in mutation'); 116 | } 117 | 118 | /** 119 | * @covers ::getName 120 | */ 121 | public function testGetNameWithNoInfoBlockAdded() : void { 122 | $this->expectException(RuntimeException::class); 123 | $this->expectExceptionMessage('The info part of the torrent is not set.'); 124 | $this->torrent->getName(); 125 | } 126 | 127 | /** 128 | * @covers ::getSize 129 | */ 130 | public function testGetSizeWithNoInfoBlockAdded() : void { 131 | $this->expectException(RuntimeException::class); 132 | $this->expectExceptionMessage('The info part of the torrent is not set.'); 133 | $this->torrent->getSize(); 134 | } 135 | 136 | /** 137 | * @covers ::getFileList 138 | */ 139 | public function testGetFileListWithNoInfoBlockAdded() : void { 140 | $this->expectException(RuntimeException::class); 141 | $this->expectExceptionMessage('The info part of the torrent is not set.'); 142 | $this->torrent->getFileList(); 143 | } 144 | 145 | /** 146 | * @covers ::withInfo 147 | * @covers ::getName 148 | */ 149 | public function testGetName() : void { 150 | $this->assertSame('Some name', $this->torrent->withInfo($info = ['name' => 'Some name'])->getName()); 151 | } 152 | 153 | /** 154 | * @covers ::withInfo 155 | * @covers ::getSize 156 | */ 157 | public function testGetSizeWhenLengthIsPresentInTheInfoBlock() : void { 158 | $this->assertSame(123, $this->torrent->withInfo(['length' => 123])->getSize()); 159 | } 160 | 161 | /** 162 | * @covers ::withInfo 163 | * @covers ::getFileList 164 | */ 165 | public function testGetFileListWhenInfoBlockOnlyContainsOneFile() : void { 166 | $fileList = $this->torrent->withInfo(['length' => 123, 'name' => 'some_filename'])->getFileList(); 167 | $this->assertCount(1, $fileList); 168 | $this->assertSame('some_filename', $fileList[0]); 169 | } 170 | 171 | /** 172 | * @covers ::withInfo 173 | * @covers ::getFileList 174 | */ 175 | public function testGetFileList() : void { 176 | $files = [ 177 | ['length' => 12, 'path' => ['path', 'file.php']], 178 | ['length' => 32, 'path' => ['path2', 'file2.php']], 179 | ['length' => 123, 'path' => ['file.php']], 180 | ]; 181 | $this->assertSame($files, $this->torrent->withInfo(['files' => $files])->getFileList()); 182 | } 183 | 184 | /** 185 | * @covers ::withInfo 186 | * @covers ::getSize 187 | */ 188 | public function testGetSizeWhenInfoBlockHasSeveralFiles() : void { 189 | $files = [ 190 | ['length' => 12, 'path' => ['path', 'file.php']], 191 | ['length' => 32, 'path' => ['path2', 'file2.php']], 192 | ['length' => 123, 'path' => ['file.php']], 193 | ]; 194 | $this->assertEquals(167, $this->torrent->withInfo(['files' => $files])->getSize()); 195 | } 196 | 197 | /** 198 | * @covers ::createFromTorrentFile 199 | * @covers ::createFromDictionary 200 | * @covers ::getAnnounceUrl 201 | * @covers ::getComment 202 | * @covers ::getCreatedBy 203 | * @covers ::getCreatedAt 204 | * @covers ::getSize 205 | * @covers ::getFileList 206 | */ 207 | public function testCreateFromTorrentFile() : void { 208 | $torrent = Torrent::createFromTorrentFile(__DIR__ . '/_files/valid.torrent', new Decoder()); 209 | 210 | $this->assertSame('http://trackerurl', $torrent->getAnnounceUrl()); 211 | $this->assertSame('This is a comment', $torrent->getComment()); 212 | $this->assertSame('PHP BitTorrent', $torrent->getCreatedBy()); 213 | $this->assertSame(1323713688, $torrent->getCreatedAt()); 214 | $this->assertEquals(30243, $torrent->getSize()); 215 | $this->assertSame(5, count($torrent->getFileList())); 216 | } 217 | 218 | /** 219 | * @covers ::createFromString 220 | * @covers ::createFromDictionary 221 | * @covers ::getAnnounceUrl 222 | * @covers ::getComment 223 | * @covers ::getCreatedBy 224 | * @covers ::getCreatedAt 225 | * @covers ::getSize 226 | * @covers ::getFileList 227 | */ 228 | public function testCreateFromTorrentFileString() : void { 229 | $torrent = Torrent::createFromString(file_get_contents(__DIR__ . '/_files/valid.torrent'), new Decoder()); 230 | 231 | $this->assertSame('http://trackerurl', $torrent->getAnnounceUrl()); 232 | $this->assertSame('This is a comment', $torrent->getComment()); 233 | $this->assertSame('PHP BitTorrent', $torrent->getCreatedBy()); 234 | $this->assertSame(1323713688, $torrent->getCreatedAt()); 235 | $this->assertEquals(30243, $torrent->getSize()); 236 | $this->assertSame(5, count($torrent->getFileList())); 237 | } 238 | 239 | /** 240 | * @covers ::createFromTorrentFile 241 | * @covers ::createFromDictionary 242 | * @covers ::getAnnounceUrl 243 | * @covers ::getFileList 244 | */ 245 | public function testCreateFromTorrentFileWithLists() : void { 246 | $torrent = Torrent::createFromTorrentFile(__DIR__ . '/_extra_files/extra.torrent', new Decoder()); 247 | $announceList = [ 248 | [ 249 | 'http://tracker/', 250 | 'http://tracker2/', 251 | 'http://tracker3/', 252 | ] 253 | ]; 254 | 255 | $this->assertSame('http://tracker/', $torrent->getAnnounceUrl()); 256 | $this->assertEquals($announceList, $torrent->getAnnounceList()); 257 | $this->assertSame(1, count($torrent->getFileList())); 258 | } 259 | 260 | /** 261 | * @covers ::createFromTorrentFile 262 | * @covers ::getExtraMeta 263 | */ 264 | public function testCreateFromTorrentFileWithExtra() : void { 265 | $torrent = Torrent::createFromTorrentFile(__DIR__ . '/_extra_files/extra.torrent', new Decoder()); 266 | $webSeeds = [ 267 | 'url-list' => 268 | [ 269 | 'http://seed/', 270 | 'http://seed2/', 271 | 'http://seed3/', 272 | ] 273 | ]; 274 | 275 | $this->assertEquals($webSeeds, $torrent->getExtraMeta()); 276 | } 277 | 278 | /** 279 | * @covers ::createFromPath 280 | * @covers ::getAnnounceUrl 281 | * @covers ::getName 282 | * @covers ::getSize 283 | * @covers ::getFileList 284 | */ 285 | public function testCreateFromPathWhenUsingADirectoryAsArgument() : void { 286 | $path = __DIR__ . '/_files'; 287 | $trackerUrl = 'http://trackerurl'; 288 | $torrent = Torrent::createFromPath($path, $trackerUrl); 289 | 290 | $this->assertSame($trackerUrl, $torrent->getAnnounceUrl()); 291 | $this->assertSame('_files', $torrent->getName()); 292 | $this->assertEquals(902910, $torrent->getSize()); 293 | $this->assertSame(7, count($torrent->getFileList())); 294 | } 295 | 296 | /** 297 | * @covers ::createFromPath 298 | * @covers ::getAnnounceUrl 299 | * @covers ::getName 300 | * @covers ::getSize 301 | * @covers ::getFileList 302 | */ 303 | public function testCreateFromPathWhenUsingAFileAsArgument() : void { 304 | $path = __DIR__ . '/_files/valid.torrent'; 305 | $trackerUrl = 'http://trackerurl'; 306 | $torrent = Torrent::createFromPath($path, $trackerUrl); 307 | 308 | $this->assertSame($trackerUrl, $torrent->getAnnounceUrl()); 309 | $this->assertSame('valid.torrent', $torrent->getName()); 310 | $this->assertSame(440, $torrent->getSize()); 311 | $this->assertSame(1, count($torrent->getFileList())); 312 | } 313 | 314 | /** 315 | * @covers ::createFromPath 316 | * @covers ::withComment 317 | * @covers ::withCreatedBy 318 | * @covers ::withAnnounceList 319 | * @covers ::save 320 | * @covers ::createFromTorrentFile 321 | * @covers ::getAnnounceUrl 322 | * @covers ::getComment 323 | * @covers ::getCreatedBy 324 | * @covers ::getName 325 | * @covers ::getSize 326 | * @covers ::getFileList 327 | * @covers ::getAnnounceList 328 | * @covers ::getInfoPart 329 | */ 330 | public function testSaveTorrent() : void { 331 | $path = __DIR__ . '/_files'; 332 | $announce = 'http://tracker/'; 333 | $announceList = [['http://tracker2'], ['http://tracker3']]; 334 | $comment = 'Some comment'; 335 | $createdBy = 'PHPUnit'; 336 | $target = tempnam(sys_get_temp_dir(), 'PHP\BitTorrent'); 337 | 338 | if (!$target) { 339 | $this->fail('Could not create file: ' . $target); 340 | } 341 | 342 | $torrent = Torrent::createFromPath($path, $announce) 343 | ->withComment($comment) 344 | ->withCreatedBy($createdBy) 345 | ->withAnnounceList($announceList); 346 | 347 | $this->assertTrue($torrent->save($target)); 348 | 349 | $torrent = Torrent::createFromTorrentFile($target, new Decoder()); 350 | 351 | $this->assertSame($announce, $torrent->getAnnounceUrl()); 352 | $this->assertSame($comment, $torrent->getComment()); 353 | $this->assertSame($createdBy, $torrent->getCreatedBy()); 354 | $this->assertSame('_files', $torrent->getName()); 355 | $this->assertEquals(902910, $torrent->getSize()); 356 | $this->assertSame(7, count($torrent->getFileList())); 357 | $this->assertSame($announceList, $torrent->getAnnounceList()); 358 | 359 | unlink($target); 360 | } 361 | 362 | /** 363 | * @covers ::createFromPath 364 | * @covers ::withExtraMeta 365 | * @covers ::save 366 | * @covers ::createFromTorrentFile 367 | * @covers ::getExtraMeta 368 | */ 369 | public function testSaveWithExtra() : void { 370 | $path = __DIR__ . '/_files'; 371 | $announce = 'http://tracker/'; 372 | $target = tempnam(sys_get_temp_dir(), 'PHP\BitTorrent'); 373 | 374 | if (!$target) { 375 | $this->fail('Could not create file: ' . $target); 376 | } 377 | 378 | $extra = [ 379 | 'url-list' => [ 380 | 'http://seed/', 381 | 'http://seed2/', 382 | 'http://seed3/', 383 | ], 384 | ]; 385 | 386 | $torrent = Torrent::createFromPath($path, $announce)->withExtraMeta($extra); 387 | $torrent->save($target); 388 | 389 | $torrent = Torrent::createFromTorrentFile($target, new Decoder()); 390 | 391 | $this->assertEquals($extra, $torrent->getExtraMeta()); 392 | 393 | unlink($target); 394 | } 395 | 396 | /** 397 | * @covers ::createFromPath 398 | * @covers ::withExtraMeta 399 | * @covers ::save 400 | */ 401 | public function testSaveWithInvalidExtra() : void { 402 | $path = __DIR__ . '/_files'; 403 | $announce = 'http://tracker/'; 404 | $target = tempnam(sys_get_temp_dir(), 'PHP\BitTorrent'); 405 | 406 | if (!$target) { 407 | $this->fail('Could not create file: ' . $target); 408 | } 409 | 410 | $extra = ['announce' => 'http://extratracker']; 411 | 412 | $torrent = Torrent::createFromPath($path, $announce) 413 | ->withExtraMeta($extra); 414 | 415 | $this->expectException(RuntimeException::class); 416 | $this->expectExceptionMessage('Duplicate key in extra meta info'); 417 | $torrent->save($target); 418 | } 419 | 420 | /** 421 | * @covers ::save 422 | */ 423 | public function testSaveWithNoAnnounce() : void { 424 | $this->expectException(RuntimeException::class); 425 | $this->expectExceptionMessage('Announce URL is missing'); 426 | $this->torrent->save(tempnam(sys_get_temp_dir(), 'PHP\BitTorrent')); 427 | } 428 | 429 | /** 430 | * @covers ::withAnnounceUrl 431 | * @covers ::save 432 | * @covers ::getInfoPart 433 | */ 434 | public function testSaveWithNoInfoBlock() : void { 435 | $torrent = $this->torrent->withAnnounceUrl('http://tracker'); 436 | $this->expectException(RuntimeException::class); 437 | $this->expectExceptionMessage('The info part of the torrent is not set.'); 438 | $torrent->save(tempnam(sys_get_temp_dir(), 'PHP\BitTorrent')); 439 | } 440 | 441 | /** 442 | * @covers ::createFromPath 443 | * @covers ::save 444 | */ 445 | public function testSaveToUnwritableFile() : void { 446 | $torrent = Torrent::createFromPath(__FILE__, 'http://tracker/'); 447 | $this->expectException(InvalidArgumentException::class); 448 | $this->expectExceptionMessage('Could not open file'); 449 | $torrent->save(uniqid() . DIRECTORY_SEPARATOR . uniqid()); 450 | } 451 | 452 | /** 453 | * @covers ::createFromTorrentFile 454 | */ 455 | public function testCreateFromTorrentFileWithUnexistingTorrentFile() : void { 456 | $this->expectException(InvalidArgumentException::class); 457 | $this->expectExceptionMessage('foobar does not exist'); 458 | Torrent::createFromTorrentFile('foobar', new Decoder()); 459 | } 460 | 461 | /** 462 | * @covers ::createFromPath 463 | */ 464 | public function testCreateFromPathWithInvalidPath() : void { 465 | $this->expectException(InvalidArgumentException::class); 466 | $this->expectExceptionMessage('Invalid path: foobar'); 467 | Torrent::createFromPath('foobar', 'http://trackerurl'); 468 | } 469 | 470 | /** 471 | * @covers ::getHash 472 | */ 473 | public function testThrowsExceptionWhenTryingToGenerateHashWithEmptyTorrentFile() : void { 474 | $this->expectException(RuntimeException::class); 475 | $this->expectExceptionMessage('The info part of the torrent is not set.'); 476 | $this->torrent->getHash(); 477 | } 478 | 479 | /** 480 | * @covers ::getEncodedHash 481 | */ 482 | public function testThrowsExceptionWhenTryingToGenerateEncodedHashWithEmptyTorrentFile() : void { 483 | $this->expectException(RuntimeException::class); 484 | $this->expectExceptionMessage('The info part of the torrent is not set.'); 485 | $this->torrent->getEncodedHash(); 486 | } 487 | 488 | /** 489 | * @covers ::getHash 490 | */ 491 | public function testGetHash() : void { 492 | $this->assertSame( 493 | 'c717bfd30211a5e36c94cabaab3b3ca0dc89f91a', 494 | Torrent::createFromTorrentFile(__DIR__ . '/_files/valid.torrent', new Decoder())->getHash() 495 | ); 496 | } 497 | 498 | /** 499 | * @covers ::getEncodedHash 500 | * @covers ::getHash 501 | */ 502 | public function testGetEncodedHash() : void { 503 | $this->assertSame( 504 | '%C7%17%BF%D3%02%11%A5%E3l%94%CA%BA%AB%3B%3C%A0%DC%89%F9%1A', 505 | Torrent::createFromTorrentFile(__DIR__ . '/_files/valid.torrent', new Decoder())->getEncodedHash() 506 | ); 507 | } 508 | 509 | /** 510 | * @covers ::getSize 511 | */ 512 | public function testGetSizeWithLargeValues() : void { 513 | $decoder = new Decoder(); 514 | $this->assertSame(6442450944, Torrent::createFromTorrentFile(__DIR__ . '/_files/large_files.torrent', $decoder)->getSize()); 515 | $this->assertSame(5368709120, Torrent::createFromTorrentFile(__DIR__ . '/_files/large_file.img.torrent', $decoder)->getSize()); 516 | } 517 | 518 | /** 519 | * @covers ::isPrivate 520 | */ 521 | public function testIsPrivateWhenFlagDoesNotExist() : void { 522 | $this->assertFalse(Torrent::createFromTorrentFile(__DIR__ . '/_files/large_files.torrent', new Decoder())->isPrivate()); 523 | } 524 | 525 | /** 526 | * @covers ::isPrivate 527 | */ 528 | public function testIsPrivateWhenItExistsAndIs1() : void { 529 | $this->assertTrue(Torrent::createFromTorrentFile(__DIR__ . '/_files/file_with_private_set_to_1.torrent', new Decoder())->isPrivate()); 530 | } 531 | 532 | /** 533 | * @covers ::isPrivate 534 | */ 535 | public function testIsPrivateWhenItExistsAndIsNot1() : void { 536 | $this->assertFalse(Torrent::createFromTorrentFile(__DIR__ . '/_files/file_with_private_set_to_0.torrent', new Decoder())->isPrivate()); 537 | } 538 | } 539 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "829c2bf2725619d4ec477f545139ffcc", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "doctrine/instantiator", 12 | "version": "1.3.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/doctrine/instantiator.git", 16 | "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", 21 | "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1" 26 | }, 27 | "require-dev": { 28 | "doctrine/coding-standard": "^6.0", 29 | "ext-pdo": "*", 30 | "ext-phar": "*", 31 | "phpbench/phpbench": "^0.13", 32 | "phpstan/phpstan-phpunit": "^0.11", 33 | "phpstan/phpstan-shim": "^0.11", 34 | "phpunit/phpunit": "^7.0" 35 | }, 36 | "type": "library", 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "1.2.x-dev" 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 45 | } 46 | }, 47 | "notification-url": "https://packagist.org/downloads/", 48 | "license": [ 49 | "MIT" 50 | ], 51 | "authors": [ 52 | { 53 | "name": "Marco Pivetta", 54 | "email": "ocramius@gmail.com", 55 | "homepage": "http://ocramius.github.com/" 56 | } 57 | ], 58 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 59 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 60 | "keywords": [ 61 | "constructor", 62 | "instantiate" 63 | ], 64 | "time": "2019-10-21T16:45:58+00:00" 65 | }, 66 | { 67 | "name": "myclabs/deep-copy", 68 | "version": "1.9.5", 69 | "source": { 70 | "type": "git", 71 | "url": "https://github.com/myclabs/DeepCopy.git", 72 | "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" 73 | }, 74 | "dist": { 75 | "type": "zip", 76 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", 77 | "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", 78 | "shasum": "" 79 | }, 80 | "require": { 81 | "php": "^7.1" 82 | }, 83 | "replace": { 84 | "myclabs/deep-copy": "self.version" 85 | }, 86 | "require-dev": { 87 | "doctrine/collections": "^1.0", 88 | "doctrine/common": "^2.6", 89 | "phpunit/phpunit": "^7.1" 90 | }, 91 | "type": "library", 92 | "autoload": { 93 | "psr-4": { 94 | "DeepCopy\\": "src/DeepCopy/" 95 | }, 96 | "files": [ 97 | "src/DeepCopy/deep_copy.php" 98 | ] 99 | }, 100 | "notification-url": "https://packagist.org/downloads/", 101 | "license": [ 102 | "MIT" 103 | ], 104 | "description": "Create deep copies (clones) of your objects", 105 | "keywords": [ 106 | "clone", 107 | "copy", 108 | "duplicate", 109 | "object", 110 | "object graph" 111 | ], 112 | "time": "2020-01-17T21:11:47+00:00" 113 | }, 114 | { 115 | "name": "phar-io/manifest", 116 | "version": "1.0.3", 117 | "source": { 118 | "type": "git", 119 | "url": "https://github.com/phar-io/manifest.git", 120 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" 121 | }, 122 | "dist": { 123 | "type": "zip", 124 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 125 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 126 | "shasum": "" 127 | }, 128 | "require": { 129 | "ext-dom": "*", 130 | "ext-phar": "*", 131 | "phar-io/version": "^2.0", 132 | "php": "^5.6 || ^7.0" 133 | }, 134 | "type": "library", 135 | "extra": { 136 | "branch-alias": { 137 | "dev-master": "1.0.x-dev" 138 | } 139 | }, 140 | "autoload": { 141 | "classmap": [ 142 | "src/" 143 | ] 144 | }, 145 | "notification-url": "https://packagist.org/downloads/", 146 | "license": [ 147 | "BSD-3-Clause" 148 | ], 149 | "authors": [ 150 | { 151 | "name": "Arne Blankerts", 152 | "email": "arne@blankerts.de", 153 | "role": "Developer" 154 | }, 155 | { 156 | "name": "Sebastian Heuer", 157 | "email": "sebastian@phpeople.de", 158 | "role": "Developer" 159 | }, 160 | { 161 | "name": "Sebastian Bergmann", 162 | "email": "sebastian@phpunit.de", 163 | "role": "Developer" 164 | } 165 | ], 166 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 167 | "time": "2018-07-08T19:23:20+00:00" 168 | }, 169 | { 170 | "name": "phar-io/version", 171 | "version": "2.0.1", 172 | "source": { 173 | "type": "git", 174 | "url": "https://github.com/phar-io/version.git", 175 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" 176 | }, 177 | "dist": { 178 | "type": "zip", 179 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", 180 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", 181 | "shasum": "" 182 | }, 183 | "require": { 184 | "php": "^5.6 || ^7.0" 185 | }, 186 | "type": "library", 187 | "autoload": { 188 | "classmap": [ 189 | "src/" 190 | ] 191 | }, 192 | "notification-url": "https://packagist.org/downloads/", 193 | "license": [ 194 | "BSD-3-Clause" 195 | ], 196 | "authors": [ 197 | { 198 | "name": "Arne Blankerts", 199 | "email": "arne@blankerts.de", 200 | "role": "Developer" 201 | }, 202 | { 203 | "name": "Sebastian Heuer", 204 | "email": "sebastian@phpeople.de", 205 | "role": "Developer" 206 | }, 207 | { 208 | "name": "Sebastian Bergmann", 209 | "email": "sebastian@phpunit.de", 210 | "role": "Developer" 211 | } 212 | ], 213 | "description": "Library for handling version information and constraints", 214 | "time": "2018-07-08T19:19:57+00:00" 215 | }, 216 | { 217 | "name": "phpdocumentor/reflection-common", 218 | "version": "2.0.0", 219 | "source": { 220 | "type": "git", 221 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 222 | "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" 223 | }, 224 | "dist": { 225 | "type": "zip", 226 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", 227 | "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", 228 | "shasum": "" 229 | }, 230 | "require": { 231 | "php": ">=7.1" 232 | }, 233 | "require-dev": { 234 | "phpunit/phpunit": "~6" 235 | }, 236 | "type": "library", 237 | "extra": { 238 | "branch-alias": { 239 | "dev-master": "2.x-dev" 240 | } 241 | }, 242 | "autoload": { 243 | "psr-4": { 244 | "phpDocumentor\\Reflection\\": "src/" 245 | } 246 | }, 247 | "notification-url": "https://packagist.org/downloads/", 248 | "license": [ 249 | "MIT" 250 | ], 251 | "authors": [ 252 | { 253 | "name": "Jaap van Otterdijk", 254 | "email": "opensource@ijaap.nl" 255 | } 256 | ], 257 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 258 | "homepage": "http://www.phpdoc.org", 259 | "keywords": [ 260 | "FQSEN", 261 | "phpDocumentor", 262 | "phpdoc", 263 | "reflection", 264 | "static analysis" 265 | ], 266 | "time": "2018-08-07T13:53:10+00:00" 267 | }, 268 | { 269 | "name": "phpdocumentor/reflection-docblock", 270 | "version": "4.3.4", 271 | "source": { 272 | "type": "git", 273 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 274 | "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" 275 | }, 276 | "dist": { 277 | "type": "zip", 278 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", 279 | "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", 280 | "shasum": "" 281 | }, 282 | "require": { 283 | "php": "^7.0", 284 | "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", 285 | "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", 286 | "webmozart/assert": "^1.0" 287 | }, 288 | "require-dev": { 289 | "doctrine/instantiator": "^1.0.5", 290 | "mockery/mockery": "^1.0", 291 | "phpdocumentor/type-resolver": "0.4.*", 292 | "phpunit/phpunit": "^6.4" 293 | }, 294 | "type": "library", 295 | "extra": { 296 | "branch-alias": { 297 | "dev-master": "4.x-dev" 298 | } 299 | }, 300 | "autoload": { 301 | "psr-4": { 302 | "phpDocumentor\\Reflection\\": [ 303 | "src/" 304 | ] 305 | } 306 | }, 307 | "notification-url": "https://packagist.org/downloads/", 308 | "license": [ 309 | "MIT" 310 | ], 311 | "authors": [ 312 | { 313 | "name": "Mike van Riel", 314 | "email": "me@mikevanriel.com" 315 | } 316 | ], 317 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 318 | "time": "2019-12-28T18:55:12+00:00" 319 | }, 320 | { 321 | "name": "phpdocumentor/type-resolver", 322 | "version": "1.0.1", 323 | "source": { 324 | "type": "git", 325 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 326 | "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" 327 | }, 328 | "dist": { 329 | "type": "zip", 330 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", 331 | "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", 332 | "shasum": "" 333 | }, 334 | "require": { 335 | "php": "^7.1", 336 | "phpdocumentor/reflection-common": "^2.0" 337 | }, 338 | "require-dev": { 339 | "ext-tokenizer": "^7.1", 340 | "mockery/mockery": "~1", 341 | "phpunit/phpunit": "^7.0" 342 | }, 343 | "type": "library", 344 | "extra": { 345 | "branch-alias": { 346 | "dev-master": "1.x-dev" 347 | } 348 | }, 349 | "autoload": { 350 | "psr-4": { 351 | "phpDocumentor\\Reflection\\": "src" 352 | } 353 | }, 354 | "notification-url": "https://packagist.org/downloads/", 355 | "license": [ 356 | "MIT" 357 | ], 358 | "authors": [ 359 | { 360 | "name": "Mike van Riel", 361 | "email": "me@mikevanriel.com" 362 | } 363 | ], 364 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 365 | "time": "2019-08-22T18:11:29+00:00" 366 | }, 367 | { 368 | "name": "phploc/phploc", 369 | "version": "5.0.0", 370 | "source": { 371 | "type": "git", 372 | "url": "https://github.com/sebastianbergmann/phploc.git", 373 | "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884" 374 | }, 375 | "dist": { 376 | "type": "zip", 377 | "url": "https://api.github.com/repos/sebastianbergmann/phploc/zipball/5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884", 378 | "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884", 379 | "shasum": "" 380 | }, 381 | "require": { 382 | "php": "^7.2", 383 | "sebastian/finder-facade": "^1.1", 384 | "sebastian/version": "^2.0", 385 | "symfony/console": "^4.0" 386 | }, 387 | "bin": [ 388 | "phploc" 389 | ], 390 | "type": "library", 391 | "extra": { 392 | "branch-alias": { 393 | "dev-master": "5.0-dev" 394 | } 395 | }, 396 | "autoload": { 397 | "classmap": [ 398 | "src/" 399 | ] 400 | }, 401 | "notification-url": "https://packagist.org/downloads/", 402 | "license": [ 403 | "BSD-3-Clause" 404 | ], 405 | "authors": [ 406 | { 407 | "name": "Sebastian Bergmann", 408 | "email": "sebastian@phpunit.de", 409 | "role": "lead" 410 | } 411 | ], 412 | "description": "A tool for quickly measuring the size of a PHP project.", 413 | "homepage": "https://github.com/sebastianbergmann/phploc", 414 | "time": "2019-03-16T10:41:19+00:00" 415 | }, 416 | { 417 | "name": "phpspec/prophecy", 418 | "version": "v1.10.2", 419 | "source": { 420 | "type": "git", 421 | "url": "https://github.com/phpspec/prophecy.git", 422 | "reference": "b4400efc9d206e83138e2bb97ed7f5b14b831cd9" 423 | }, 424 | "dist": { 425 | "type": "zip", 426 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b4400efc9d206e83138e2bb97ed7f5b14b831cd9", 427 | "reference": "b4400efc9d206e83138e2bb97ed7f5b14b831cd9", 428 | "shasum": "" 429 | }, 430 | "require": { 431 | "doctrine/instantiator": "^1.0.2", 432 | "php": "^5.3|^7.0", 433 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 434 | "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", 435 | "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" 436 | }, 437 | "require-dev": { 438 | "phpspec/phpspec": "^2.5 || ^3.2", 439 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 440 | }, 441 | "type": "library", 442 | "extra": { 443 | "branch-alias": { 444 | "dev-master": "1.10.x-dev" 445 | } 446 | }, 447 | "autoload": { 448 | "psr-4": { 449 | "Prophecy\\": "src/Prophecy" 450 | } 451 | }, 452 | "notification-url": "https://packagist.org/downloads/", 453 | "license": [ 454 | "MIT" 455 | ], 456 | "authors": [ 457 | { 458 | "name": "Konstantin Kudryashov", 459 | "email": "ever.zet@gmail.com", 460 | "homepage": "http://everzet.com" 461 | }, 462 | { 463 | "name": "Marcello Duarte", 464 | "email": "marcello.duarte@gmail.com" 465 | } 466 | ], 467 | "description": "Highly opinionated mocking framework for PHP 5.3+", 468 | "homepage": "https://github.com/phpspec/prophecy", 469 | "keywords": [ 470 | "Double", 471 | "Dummy", 472 | "fake", 473 | "mock", 474 | "spy", 475 | "stub" 476 | ], 477 | "time": "2020-01-20T15:57:02+00:00" 478 | }, 479 | { 480 | "name": "phpstan/phpstan", 481 | "version": "0.12.7", 482 | "source": { 483 | "type": "git", 484 | "url": "https://github.com/phpstan/phpstan.git", 485 | "reference": "07fa7958027fd98c567099bbcda5d6a0f2ec5197" 486 | }, 487 | "dist": { 488 | "type": "zip", 489 | "url": "https://api.github.com/repos/phpstan/phpstan/zipball/07fa7958027fd98c567099bbcda5d6a0f2ec5197", 490 | "reference": "07fa7958027fd98c567099bbcda5d6a0f2ec5197", 491 | "shasum": "" 492 | }, 493 | "require": { 494 | "php": "^7.1" 495 | }, 496 | "bin": [ 497 | "phpstan", 498 | "phpstan.phar" 499 | ], 500 | "type": "library", 501 | "extra": { 502 | "branch-alias": { 503 | "dev-master": "0.12-dev" 504 | } 505 | }, 506 | "autoload": { 507 | "files": [ 508 | "bootstrap.php" 509 | ] 510 | }, 511 | "notification-url": "https://packagist.org/downloads/", 512 | "license": [ 513 | "MIT" 514 | ], 515 | "description": "PHPStan - PHP Static Analysis Tool", 516 | "time": "2020-01-20T21:59:06+00:00" 517 | }, 518 | { 519 | "name": "phpunit/php-code-coverage", 520 | "version": "7.0.10", 521 | "source": { 522 | "type": "git", 523 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 524 | "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" 525 | }, 526 | "dist": { 527 | "type": "zip", 528 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", 529 | "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", 530 | "shasum": "" 531 | }, 532 | "require": { 533 | "ext-dom": "*", 534 | "ext-xmlwriter": "*", 535 | "php": "^7.2", 536 | "phpunit/php-file-iterator": "^2.0.2", 537 | "phpunit/php-text-template": "^1.2.1", 538 | "phpunit/php-token-stream": "^3.1.1", 539 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 540 | "sebastian/environment": "^4.2.2", 541 | "sebastian/version": "^2.0.1", 542 | "theseer/tokenizer": "^1.1.3" 543 | }, 544 | "require-dev": { 545 | "phpunit/phpunit": "^8.2.2" 546 | }, 547 | "suggest": { 548 | "ext-xdebug": "^2.7.2" 549 | }, 550 | "type": "library", 551 | "extra": { 552 | "branch-alias": { 553 | "dev-master": "7.0-dev" 554 | } 555 | }, 556 | "autoload": { 557 | "classmap": [ 558 | "src/" 559 | ] 560 | }, 561 | "notification-url": "https://packagist.org/downloads/", 562 | "license": [ 563 | "BSD-3-Clause" 564 | ], 565 | "authors": [ 566 | { 567 | "name": "Sebastian Bergmann", 568 | "email": "sebastian@phpunit.de", 569 | "role": "lead" 570 | } 571 | ], 572 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 573 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 574 | "keywords": [ 575 | "coverage", 576 | "testing", 577 | "xunit" 578 | ], 579 | "time": "2019-11-20T13:55:58+00:00" 580 | }, 581 | { 582 | "name": "phpunit/php-file-iterator", 583 | "version": "2.0.2", 584 | "source": { 585 | "type": "git", 586 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 587 | "reference": "050bedf145a257b1ff02746c31894800e5122946" 588 | }, 589 | "dist": { 590 | "type": "zip", 591 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", 592 | "reference": "050bedf145a257b1ff02746c31894800e5122946", 593 | "shasum": "" 594 | }, 595 | "require": { 596 | "php": "^7.1" 597 | }, 598 | "require-dev": { 599 | "phpunit/phpunit": "^7.1" 600 | }, 601 | "type": "library", 602 | "extra": { 603 | "branch-alias": { 604 | "dev-master": "2.0.x-dev" 605 | } 606 | }, 607 | "autoload": { 608 | "classmap": [ 609 | "src/" 610 | ] 611 | }, 612 | "notification-url": "https://packagist.org/downloads/", 613 | "license": [ 614 | "BSD-3-Clause" 615 | ], 616 | "authors": [ 617 | { 618 | "name": "Sebastian Bergmann", 619 | "email": "sebastian@phpunit.de", 620 | "role": "lead" 621 | } 622 | ], 623 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 624 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 625 | "keywords": [ 626 | "filesystem", 627 | "iterator" 628 | ], 629 | "time": "2018-09-13T20:33:42+00:00" 630 | }, 631 | { 632 | "name": "phpunit/php-text-template", 633 | "version": "1.2.1", 634 | "source": { 635 | "type": "git", 636 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 637 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 638 | }, 639 | "dist": { 640 | "type": "zip", 641 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 642 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 643 | "shasum": "" 644 | }, 645 | "require": { 646 | "php": ">=5.3.3" 647 | }, 648 | "type": "library", 649 | "autoload": { 650 | "classmap": [ 651 | "src/" 652 | ] 653 | }, 654 | "notification-url": "https://packagist.org/downloads/", 655 | "license": [ 656 | "BSD-3-Clause" 657 | ], 658 | "authors": [ 659 | { 660 | "name": "Sebastian Bergmann", 661 | "email": "sebastian@phpunit.de", 662 | "role": "lead" 663 | } 664 | ], 665 | "description": "Simple template engine.", 666 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 667 | "keywords": [ 668 | "template" 669 | ], 670 | "time": "2015-06-21T13:50:34+00:00" 671 | }, 672 | { 673 | "name": "phpunit/php-timer", 674 | "version": "2.1.2", 675 | "source": { 676 | "type": "git", 677 | "url": "https://github.com/sebastianbergmann/php-timer.git", 678 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" 679 | }, 680 | "dist": { 681 | "type": "zip", 682 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", 683 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", 684 | "shasum": "" 685 | }, 686 | "require": { 687 | "php": "^7.1" 688 | }, 689 | "require-dev": { 690 | "phpunit/phpunit": "^7.0" 691 | }, 692 | "type": "library", 693 | "extra": { 694 | "branch-alias": { 695 | "dev-master": "2.1-dev" 696 | } 697 | }, 698 | "autoload": { 699 | "classmap": [ 700 | "src/" 701 | ] 702 | }, 703 | "notification-url": "https://packagist.org/downloads/", 704 | "license": [ 705 | "BSD-3-Clause" 706 | ], 707 | "authors": [ 708 | { 709 | "name": "Sebastian Bergmann", 710 | "email": "sebastian@phpunit.de", 711 | "role": "lead" 712 | } 713 | ], 714 | "description": "Utility class for timing", 715 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 716 | "keywords": [ 717 | "timer" 718 | ], 719 | "time": "2019-06-07T04:22:29+00:00" 720 | }, 721 | { 722 | "name": "phpunit/php-token-stream", 723 | "version": "3.1.1", 724 | "source": { 725 | "type": "git", 726 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 727 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" 728 | }, 729 | "dist": { 730 | "type": "zip", 731 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", 732 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", 733 | "shasum": "" 734 | }, 735 | "require": { 736 | "ext-tokenizer": "*", 737 | "php": "^7.1" 738 | }, 739 | "require-dev": { 740 | "phpunit/phpunit": "^7.0" 741 | }, 742 | "type": "library", 743 | "extra": { 744 | "branch-alias": { 745 | "dev-master": "3.1-dev" 746 | } 747 | }, 748 | "autoload": { 749 | "classmap": [ 750 | "src/" 751 | ] 752 | }, 753 | "notification-url": "https://packagist.org/downloads/", 754 | "license": [ 755 | "BSD-3-Clause" 756 | ], 757 | "authors": [ 758 | { 759 | "name": "Sebastian Bergmann", 760 | "email": "sebastian@phpunit.de" 761 | } 762 | ], 763 | "description": "Wrapper around PHP's tokenizer extension.", 764 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 765 | "keywords": [ 766 | "tokenizer" 767 | ], 768 | "time": "2019-09-17T06:23:10+00:00" 769 | }, 770 | { 771 | "name": "phpunit/phpunit", 772 | "version": "8.5.2", 773 | "source": { 774 | "type": "git", 775 | "url": "https://github.com/sebastianbergmann/phpunit.git", 776 | "reference": "018b6ac3c8ab20916db85fa91bf6465acb64d1e0" 777 | }, 778 | "dist": { 779 | "type": "zip", 780 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/018b6ac3c8ab20916db85fa91bf6465acb64d1e0", 781 | "reference": "018b6ac3c8ab20916db85fa91bf6465acb64d1e0", 782 | "shasum": "" 783 | }, 784 | "require": { 785 | "doctrine/instantiator": "^1.2.0", 786 | "ext-dom": "*", 787 | "ext-json": "*", 788 | "ext-libxml": "*", 789 | "ext-mbstring": "*", 790 | "ext-xml": "*", 791 | "ext-xmlwriter": "*", 792 | "myclabs/deep-copy": "^1.9.1", 793 | "phar-io/manifest": "^1.0.3", 794 | "phar-io/version": "^2.0.1", 795 | "php": "^7.2", 796 | "phpspec/prophecy": "^1.8.1", 797 | "phpunit/php-code-coverage": "^7.0.7", 798 | "phpunit/php-file-iterator": "^2.0.2", 799 | "phpunit/php-text-template": "^1.2.1", 800 | "phpunit/php-timer": "^2.1.2", 801 | "sebastian/comparator": "^3.0.2", 802 | "sebastian/diff": "^3.0.2", 803 | "sebastian/environment": "^4.2.2", 804 | "sebastian/exporter": "^3.1.1", 805 | "sebastian/global-state": "^3.0.0", 806 | "sebastian/object-enumerator": "^3.0.3", 807 | "sebastian/resource-operations": "^2.0.1", 808 | "sebastian/type": "^1.1.3", 809 | "sebastian/version": "^2.0.1" 810 | }, 811 | "require-dev": { 812 | "ext-pdo": "*" 813 | }, 814 | "suggest": { 815 | "ext-soap": "*", 816 | "ext-xdebug": "*", 817 | "phpunit/php-invoker": "^2.0.0" 818 | }, 819 | "bin": [ 820 | "phpunit" 821 | ], 822 | "type": "library", 823 | "extra": { 824 | "branch-alias": { 825 | "dev-master": "8.5-dev" 826 | } 827 | }, 828 | "autoload": { 829 | "classmap": [ 830 | "src/" 831 | ] 832 | }, 833 | "notification-url": "https://packagist.org/downloads/", 834 | "license": [ 835 | "BSD-3-Clause" 836 | ], 837 | "authors": [ 838 | { 839 | "name": "Sebastian Bergmann", 840 | "email": "sebastian@phpunit.de", 841 | "role": "lead" 842 | } 843 | ], 844 | "description": "The PHP Unit Testing framework.", 845 | "homepage": "https://phpunit.de/", 846 | "keywords": [ 847 | "phpunit", 848 | "testing", 849 | "xunit" 850 | ], 851 | "time": "2020-01-08T08:49:49+00:00" 852 | }, 853 | { 854 | "name": "psr/container", 855 | "version": "1.0.0", 856 | "source": { 857 | "type": "git", 858 | "url": "https://github.com/php-fig/container.git", 859 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" 860 | }, 861 | "dist": { 862 | "type": "zip", 863 | "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 864 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 865 | "shasum": "" 866 | }, 867 | "require": { 868 | "php": ">=5.3.0" 869 | }, 870 | "type": "library", 871 | "extra": { 872 | "branch-alias": { 873 | "dev-master": "1.0.x-dev" 874 | } 875 | }, 876 | "autoload": { 877 | "psr-4": { 878 | "Psr\\Container\\": "src/" 879 | } 880 | }, 881 | "notification-url": "https://packagist.org/downloads/", 882 | "license": [ 883 | "MIT" 884 | ], 885 | "authors": [ 886 | { 887 | "name": "PHP-FIG", 888 | "homepage": "http://www.php-fig.org/" 889 | } 890 | ], 891 | "description": "Common Container Interface (PHP FIG PSR-11)", 892 | "homepage": "https://github.com/php-fig/container", 893 | "keywords": [ 894 | "PSR-11", 895 | "container", 896 | "container-interface", 897 | "container-interop", 898 | "psr" 899 | ], 900 | "time": "2017-02-14T16:28:37+00:00" 901 | }, 902 | { 903 | "name": "sebastian/code-unit-reverse-lookup", 904 | "version": "1.0.1", 905 | "source": { 906 | "type": "git", 907 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 908 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 909 | }, 910 | "dist": { 911 | "type": "zip", 912 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 913 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 914 | "shasum": "" 915 | }, 916 | "require": { 917 | "php": "^5.6 || ^7.0" 918 | }, 919 | "require-dev": { 920 | "phpunit/phpunit": "^5.7 || ^6.0" 921 | }, 922 | "type": "library", 923 | "extra": { 924 | "branch-alias": { 925 | "dev-master": "1.0.x-dev" 926 | } 927 | }, 928 | "autoload": { 929 | "classmap": [ 930 | "src/" 931 | ] 932 | }, 933 | "notification-url": "https://packagist.org/downloads/", 934 | "license": [ 935 | "BSD-3-Clause" 936 | ], 937 | "authors": [ 938 | { 939 | "name": "Sebastian Bergmann", 940 | "email": "sebastian@phpunit.de" 941 | } 942 | ], 943 | "description": "Looks up which function or method a line of code belongs to", 944 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 945 | "time": "2017-03-04T06:30:41+00:00" 946 | }, 947 | { 948 | "name": "sebastian/comparator", 949 | "version": "3.0.2", 950 | "source": { 951 | "type": "git", 952 | "url": "https://github.com/sebastianbergmann/comparator.git", 953 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" 954 | }, 955 | "dist": { 956 | "type": "zip", 957 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 958 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 959 | "shasum": "" 960 | }, 961 | "require": { 962 | "php": "^7.1", 963 | "sebastian/diff": "^3.0", 964 | "sebastian/exporter": "^3.1" 965 | }, 966 | "require-dev": { 967 | "phpunit/phpunit": "^7.1" 968 | }, 969 | "type": "library", 970 | "extra": { 971 | "branch-alias": { 972 | "dev-master": "3.0-dev" 973 | } 974 | }, 975 | "autoload": { 976 | "classmap": [ 977 | "src/" 978 | ] 979 | }, 980 | "notification-url": "https://packagist.org/downloads/", 981 | "license": [ 982 | "BSD-3-Clause" 983 | ], 984 | "authors": [ 985 | { 986 | "name": "Jeff Welch", 987 | "email": "whatthejeff@gmail.com" 988 | }, 989 | { 990 | "name": "Volker Dusch", 991 | "email": "github@wallbash.com" 992 | }, 993 | { 994 | "name": "Bernhard Schussek", 995 | "email": "bschussek@2bepublished.at" 996 | }, 997 | { 998 | "name": "Sebastian Bergmann", 999 | "email": "sebastian@phpunit.de" 1000 | } 1001 | ], 1002 | "description": "Provides the functionality to compare PHP values for equality", 1003 | "homepage": "https://github.com/sebastianbergmann/comparator", 1004 | "keywords": [ 1005 | "comparator", 1006 | "compare", 1007 | "equality" 1008 | ], 1009 | "time": "2018-07-12T15:12:46+00:00" 1010 | }, 1011 | { 1012 | "name": "sebastian/diff", 1013 | "version": "3.0.2", 1014 | "source": { 1015 | "type": "git", 1016 | "url": "https://github.com/sebastianbergmann/diff.git", 1017 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" 1018 | }, 1019 | "dist": { 1020 | "type": "zip", 1021 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 1022 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 1023 | "shasum": "" 1024 | }, 1025 | "require": { 1026 | "php": "^7.1" 1027 | }, 1028 | "require-dev": { 1029 | "phpunit/phpunit": "^7.5 || ^8.0", 1030 | "symfony/process": "^2 || ^3.3 || ^4" 1031 | }, 1032 | "type": "library", 1033 | "extra": { 1034 | "branch-alias": { 1035 | "dev-master": "3.0-dev" 1036 | } 1037 | }, 1038 | "autoload": { 1039 | "classmap": [ 1040 | "src/" 1041 | ] 1042 | }, 1043 | "notification-url": "https://packagist.org/downloads/", 1044 | "license": [ 1045 | "BSD-3-Clause" 1046 | ], 1047 | "authors": [ 1048 | { 1049 | "name": "Kore Nordmann", 1050 | "email": "mail@kore-nordmann.de" 1051 | }, 1052 | { 1053 | "name": "Sebastian Bergmann", 1054 | "email": "sebastian@phpunit.de" 1055 | } 1056 | ], 1057 | "description": "Diff implementation", 1058 | "homepage": "https://github.com/sebastianbergmann/diff", 1059 | "keywords": [ 1060 | "diff", 1061 | "udiff", 1062 | "unidiff", 1063 | "unified diff" 1064 | ], 1065 | "time": "2019-02-04T06:01:07+00:00" 1066 | }, 1067 | { 1068 | "name": "sebastian/environment", 1069 | "version": "4.2.3", 1070 | "source": { 1071 | "type": "git", 1072 | "url": "https://github.com/sebastianbergmann/environment.git", 1073 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" 1074 | }, 1075 | "dist": { 1076 | "type": "zip", 1077 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 1078 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 1079 | "shasum": "" 1080 | }, 1081 | "require": { 1082 | "php": "^7.1" 1083 | }, 1084 | "require-dev": { 1085 | "phpunit/phpunit": "^7.5" 1086 | }, 1087 | "suggest": { 1088 | "ext-posix": "*" 1089 | }, 1090 | "type": "library", 1091 | "extra": { 1092 | "branch-alias": { 1093 | "dev-master": "4.2-dev" 1094 | } 1095 | }, 1096 | "autoload": { 1097 | "classmap": [ 1098 | "src/" 1099 | ] 1100 | }, 1101 | "notification-url": "https://packagist.org/downloads/", 1102 | "license": [ 1103 | "BSD-3-Clause" 1104 | ], 1105 | "authors": [ 1106 | { 1107 | "name": "Sebastian Bergmann", 1108 | "email": "sebastian@phpunit.de" 1109 | } 1110 | ], 1111 | "description": "Provides functionality to handle HHVM/PHP environments", 1112 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1113 | "keywords": [ 1114 | "Xdebug", 1115 | "environment", 1116 | "hhvm" 1117 | ], 1118 | "time": "2019-11-20T08:46:58+00:00" 1119 | }, 1120 | { 1121 | "name": "sebastian/exporter", 1122 | "version": "3.1.2", 1123 | "source": { 1124 | "type": "git", 1125 | "url": "https://github.com/sebastianbergmann/exporter.git", 1126 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" 1127 | }, 1128 | "dist": { 1129 | "type": "zip", 1130 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", 1131 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", 1132 | "shasum": "" 1133 | }, 1134 | "require": { 1135 | "php": "^7.0", 1136 | "sebastian/recursion-context": "^3.0" 1137 | }, 1138 | "require-dev": { 1139 | "ext-mbstring": "*", 1140 | "phpunit/phpunit": "^6.0" 1141 | }, 1142 | "type": "library", 1143 | "extra": { 1144 | "branch-alias": { 1145 | "dev-master": "3.1.x-dev" 1146 | } 1147 | }, 1148 | "autoload": { 1149 | "classmap": [ 1150 | "src/" 1151 | ] 1152 | }, 1153 | "notification-url": "https://packagist.org/downloads/", 1154 | "license": [ 1155 | "BSD-3-Clause" 1156 | ], 1157 | "authors": [ 1158 | { 1159 | "name": "Sebastian Bergmann", 1160 | "email": "sebastian@phpunit.de" 1161 | }, 1162 | { 1163 | "name": "Jeff Welch", 1164 | "email": "whatthejeff@gmail.com" 1165 | }, 1166 | { 1167 | "name": "Volker Dusch", 1168 | "email": "github@wallbash.com" 1169 | }, 1170 | { 1171 | "name": "Adam Harvey", 1172 | "email": "aharvey@php.net" 1173 | }, 1174 | { 1175 | "name": "Bernhard Schussek", 1176 | "email": "bschussek@gmail.com" 1177 | } 1178 | ], 1179 | "description": "Provides the functionality to export PHP variables for visualization", 1180 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1181 | "keywords": [ 1182 | "export", 1183 | "exporter" 1184 | ], 1185 | "time": "2019-09-14T09:02:43+00:00" 1186 | }, 1187 | { 1188 | "name": "sebastian/finder-facade", 1189 | "version": "1.2.3", 1190 | "source": { 1191 | "type": "git", 1192 | "url": "https://github.com/sebastianbergmann/finder-facade.git", 1193 | "reference": "167c45d131f7fc3d159f56f191a0a22228765e16" 1194 | }, 1195 | "dist": { 1196 | "type": "zip", 1197 | "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/167c45d131f7fc3d159f56f191a0a22228765e16", 1198 | "reference": "167c45d131f7fc3d159f56f191a0a22228765e16", 1199 | "shasum": "" 1200 | }, 1201 | "require": { 1202 | "php": "^7.1", 1203 | "symfony/finder": "^2.3|^3.0|^4.0|^5.0", 1204 | "theseer/fdomdocument": "^1.6" 1205 | }, 1206 | "type": "library", 1207 | "extra": { 1208 | "branch-alias": [] 1209 | }, 1210 | "autoload": { 1211 | "classmap": [ 1212 | "src/" 1213 | ] 1214 | }, 1215 | "notification-url": "https://packagist.org/downloads/", 1216 | "license": [ 1217 | "BSD-3-Clause" 1218 | ], 1219 | "authors": [ 1220 | { 1221 | "name": "Sebastian Bergmann", 1222 | "email": "sebastian@phpunit.de", 1223 | "role": "lead" 1224 | } 1225 | ], 1226 | "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", 1227 | "homepage": "https://github.com/sebastianbergmann/finder-facade", 1228 | "time": "2020-01-16T08:08:45+00:00" 1229 | }, 1230 | { 1231 | "name": "sebastian/global-state", 1232 | "version": "3.0.0", 1233 | "source": { 1234 | "type": "git", 1235 | "url": "https://github.com/sebastianbergmann/global-state.git", 1236 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" 1237 | }, 1238 | "dist": { 1239 | "type": "zip", 1240 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1241 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1242 | "shasum": "" 1243 | }, 1244 | "require": { 1245 | "php": "^7.2", 1246 | "sebastian/object-reflector": "^1.1.1", 1247 | "sebastian/recursion-context": "^3.0" 1248 | }, 1249 | "require-dev": { 1250 | "ext-dom": "*", 1251 | "phpunit/phpunit": "^8.0" 1252 | }, 1253 | "suggest": { 1254 | "ext-uopz": "*" 1255 | }, 1256 | "type": "library", 1257 | "extra": { 1258 | "branch-alias": { 1259 | "dev-master": "3.0-dev" 1260 | } 1261 | }, 1262 | "autoload": { 1263 | "classmap": [ 1264 | "src/" 1265 | ] 1266 | }, 1267 | "notification-url": "https://packagist.org/downloads/", 1268 | "license": [ 1269 | "BSD-3-Clause" 1270 | ], 1271 | "authors": [ 1272 | { 1273 | "name": "Sebastian Bergmann", 1274 | "email": "sebastian@phpunit.de" 1275 | } 1276 | ], 1277 | "description": "Snapshotting of global state", 1278 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1279 | "keywords": [ 1280 | "global state" 1281 | ], 1282 | "time": "2019-02-01T05:30:01+00:00" 1283 | }, 1284 | { 1285 | "name": "sebastian/object-enumerator", 1286 | "version": "3.0.3", 1287 | "source": { 1288 | "type": "git", 1289 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1290 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 1291 | }, 1292 | "dist": { 1293 | "type": "zip", 1294 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1295 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1296 | "shasum": "" 1297 | }, 1298 | "require": { 1299 | "php": "^7.0", 1300 | "sebastian/object-reflector": "^1.1.1", 1301 | "sebastian/recursion-context": "^3.0" 1302 | }, 1303 | "require-dev": { 1304 | "phpunit/phpunit": "^6.0" 1305 | }, 1306 | "type": "library", 1307 | "extra": { 1308 | "branch-alias": { 1309 | "dev-master": "3.0.x-dev" 1310 | } 1311 | }, 1312 | "autoload": { 1313 | "classmap": [ 1314 | "src/" 1315 | ] 1316 | }, 1317 | "notification-url": "https://packagist.org/downloads/", 1318 | "license": [ 1319 | "BSD-3-Clause" 1320 | ], 1321 | "authors": [ 1322 | { 1323 | "name": "Sebastian Bergmann", 1324 | "email": "sebastian@phpunit.de" 1325 | } 1326 | ], 1327 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1328 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1329 | "time": "2017-08-03T12:35:26+00:00" 1330 | }, 1331 | { 1332 | "name": "sebastian/object-reflector", 1333 | "version": "1.1.1", 1334 | "source": { 1335 | "type": "git", 1336 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1337 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 1338 | }, 1339 | "dist": { 1340 | "type": "zip", 1341 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 1342 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 1343 | "shasum": "" 1344 | }, 1345 | "require": { 1346 | "php": "^7.0" 1347 | }, 1348 | "require-dev": { 1349 | "phpunit/phpunit": "^6.0" 1350 | }, 1351 | "type": "library", 1352 | "extra": { 1353 | "branch-alias": { 1354 | "dev-master": "1.1-dev" 1355 | } 1356 | }, 1357 | "autoload": { 1358 | "classmap": [ 1359 | "src/" 1360 | ] 1361 | }, 1362 | "notification-url": "https://packagist.org/downloads/", 1363 | "license": [ 1364 | "BSD-3-Clause" 1365 | ], 1366 | "authors": [ 1367 | { 1368 | "name": "Sebastian Bergmann", 1369 | "email": "sebastian@phpunit.de" 1370 | } 1371 | ], 1372 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1373 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1374 | "time": "2017-03-29T09:07:27+00:00" 1375 | }, 1376 | { 1377 | "name": "sebastian/recursion-context", 1378 | "version": "3.0.0", 1379 | "source": { 1380 | "type": "git", 1381 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1382 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 1383 | }, 1384 | "dist": { 1385 | "type": "zip", 1386 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1387 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1388 | "shasum": "" 1389 | }, 1390 | "require": { 1391 | "php": "^7.0" 1392 | }, 1393 | "require-dev": { 1394 | "phpunit/phpunit": "^6.0" 1395 | }, 1396 | "type": "library", 1397 | "extra": { 1398 | "branch-alias": { 1399 | "dev-master": "3.0.x-dev" 1400 | } 1401 | }, 1402 | "autoload": { 1403 | "classmap": [ 1404 | "src/" 1405 | ] 1406 | }, 1407 | "notification-url": "https://packagist.org/downloads/", 1408 | "license": [ 1409 | "BSD-3-Clause" 1410 | ], 1411 | "authors": [ 1412 | { 1413 | "name": "Jeff Welch", 1414 | "email": "whatthejeff@gmail.com" 1415 | }, 1416 | { 1417 | "name": "Sebastian Bergmann", 1418 | "email": "sebastian@phpunit.de" 1419 | }, 1420 | { 1421 | "name": "Adam Harvey", 1422 | "email": "aharvey@php.net" 1423 | } 1424 | ], 1425 | "description": "Provides functionality to recursively process PHP variables", 1426 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1427 | "time": "2017-03-03T06:23:57+00:00" 1428 | }, 1429 | { 1430 | "name": "sebastian/resource-operations", 1431 | "version": "2.0.1", 1432 | "source": { 1433 | "type": "git", 1434 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1435 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" 1436 | }, 1437 | "dist": { 1438 | "type": "zip", 1439 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1440 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1441 | "shasum": "" 1442 | }, 1443 | "require": { 1444 | "php": "^7.1" 1445 | }, 1446 | "type": "library", 1447 | "extra": { 1448 | "branch-alias": { 1449 | "dev-master": "2.0-dev" 1450 | } 1451 | }, 1452 | "autoload": { 1453 | "classmap": [ 1454 | "src/" 1455 | ] 1456 | }, 1457 | "notification-url": "https://packagist.org/downloads/", 1458 | "license": [ 1459 | "BSD-3-Clause" 1460 | ], 1461 | "authors": [ 1462 | { 1463 | "name": "Sebastian Bergmann", 1464 | "email": "sebastian@phpunit.de" 1465 | } 1466 | ], 1467 | "description": "Provides a list of PHP built-in functions that operate on resources", 1468 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1469 | "time": "2018-10-04T04:07:39+00:00" 1470 | }, 1471 | { 1472 | "name": "sebastian/type", 1473 | "version": "1.1.3", 1474 | "source": { 1475 | "type": "git", 1476 | "url": "https://github.com/sebastianbergmann/type.git", 1477 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" 1478 | }, 1479 | "dist": { 1480 | "type": "zip", 1481 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", 1482 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", 1483 | "shasum": "" 1484 | }, 1485 | "require": { 1486 | "php": "^7.2" 1487 | }, 1488 | "require-dev": { 1489 | "phpunit/phpunit": "^8.2" 1490 | }, 1491 | "type": "library", 1492 | "extra": { 1493 | "branch-alias": { 1494 | "dev-master": "1.1-dev" 1495 | } 1496 | }, 1497 | "autoload": { 1498 | "classmap": [ 1499 | "src/" 1500 | ] 1501 | }, 1502 | "notification-url": "https://packagist.org/downloads/", 1503 | "license": [ 1504 | "BSD-3-Clause" 1505 | ], 1506 | "authors": [ 1507 | { 1508 | "name": "Sebastian Bergmann", 1509 | "email": "sebastian@phpunit.de", 1510 | "role": "lead" 1511 | } 1512 | ], 1513 | "description": "Collection of value objects that represent the types of the PHP type system", 1514 | "homepage": "https://github.com/sebastianbergmann/type", 1515 | "time": "2019-07-02T08:10:15+00:00" 1516 | }, 1517 | { 1518 | "name": "sebastian/version", 1519 | "version": "2.0.1", 1520 | "source": { 1521 | "type": "git", 1522 | "url": "https://github.com/sebastianbergmann/version.git", 1523 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1524 | }, 1525 | "dist": { 1526 | "type": "zip", 1527 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1528 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1529 | "shasum": "" 1530 | }, 1531 | "require": { 1532 | "php": ">=5.6" 1533 | }, 1534 | "type": "library", 1535 | "extra": { 1536 | "branch-alias": { 1537 | "dev-master": "2.0.x-dev" 1538 | } 1539 | }, 1540 | "autoload": { 1541 | "classmap": [ 1542 | "src/" 1543 | ] 1544 | }, 1545 | "notification-url": "https://packagist.org/downloads/", 1546 | "license": [ 1547 | "BSD-3-Clause" 1548 | ], 1549 | "authors": [ 1550 | { 1551 | "name": "Sebastian Bergmann", 1552 | "email": "sebastian@phpunit.de", 1553 | "role": "lead" 1554 | } 1555 | ], 1556 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1557 | "homepage": "https://github.com/sebastianbergmann/version", 1558 | "time": "2016-10-03T07:35:21+00:00" 1559 | }, 1560 | { 1561 | "name": "symfony/console", 1562 | "version": "v4.4.3", 1563 | "source": { 1564 | "type": "git", 1565 | "url": "https://github.com/symfony/console.git", 1566 | "reference": "e9ee09d087e2c88eaf6e5fc0f5c574f64d100e4f" 1567 | }, 1568 | "dist": { 1569 | "type": "zip", 1570 | "url": "https://api.github.com/repos/symfony/console/zipball/e9ee09d087e2c88eaf6e5fc0f5c574f64d100e4f", 1571 | "reference": "e9ee09d087e2c88eaf6e5fc0f5c574f64d100e4f", 1572 | "shasum": "" 1573 | }, 1574 | "require": { 1575 | "php": "^7.1.3", 1576 | "symfony/polyfill-mbstring": "~1.0", 1577 | "symfony/polyfill-php73": "^1.8", 1578 | "symfony/service-contracts": "^1.1|^2" 1579 | }, 1580 | "conflict": { 1581 | "symfony/dependency-injection": "<3.4", 1582 | "symfony/event-dispatcher": "<4.3|>=5", 1583 | "symfony/lock": "<4.4", 1584 | "symfony/process": "<3.3" 1585 | }, 1586 | "provide": { 1587 | "psr/log-implementation": "1.0" 1588 | }, 1589 | "require-dev": { 1590 | "psr/log": "~1.0", 1591 | "symfony/config": "^3.4|^4.0|^5.0", 1592 | "symfony/dependency-injection": "^3.4|^4.0|^5.0", 1593 | "symfony/event-dispatcher": "^4.3", 1594 | "symfony/lock": "^4.4|^5.0", 1595 | "symfony/process": "^3.4|^4.0|^5.0", 1596 | "symfony/var-dumper": "^4.3|^5.0" 1597 | }, 1598 | "suggest": { 1599 | "psr/log": "For using the console logger", 1600 | "symfony/event-dispatcher": "", 1601 | "symfony/lock": "", 1602 | "symfony/process": "" 1603 | }, 1604 | "type": "library", 1605 | "extra": { 1606 | "branch-alias": { 1607 | "dev-master": "4.4-dev" 1608 | } 1609 | }, 1610 | "autoload": { 1611 | "psr-4": { 1612 | "Symfony\\Component\\Console\\": "" 1613 | }, 1614 | "exclude-from-classmap": [ 1615 | "/Tests/" 1616 | ] 1617 | }, 1618 | "notification-url": "https://packagist.org/downloads/", 1619 | "license": [ 1620 | "MIT" 1621 | ], 1622 | "authors": [ 1623 | { 1624 | "name": "Fabien Potencier", 1625 | "email": "fabien@symfony.com" 1626 | }, 1627 | { 1628 | "name": "Symfony Community", 1629 | "homepage": "https://symfony.com/contributors" 1630 | } 1631 | ], 1632 | "description": "Symfony Console Component", 1633 | "homepage": "https://symfony.com", 1634 | "time": "2020-01-10T21:54:01+00:00" 1635 | }, 1636 | { 1637 | "name": "symfony/finder", 1638 | "version": "v5.0.3", 1639 | "source": { 1640 | "type": "git", 1641 | "url": "https://github.com/symfony/finder.git", 1642 | "reference": "4176e7cb846fe08f32518b7e0ed8462e2db8d9bb" 1643 | }, 1644 | "dist": { 1645 | "type": "zip", 1646 | "url": "https://api.github.com/repos/symfony/finder/zipball/4176e7cb846fe08f32518b7e0ed8462e2db8d9bb", 1647 | "reference": "4176e7cb846fe08f32518b7e0ed8462e2db8d9bb", 1648 | "shasum": "" 1649 | }, 1650 | "require": { 1651 | "php": "^7.2.5" 1652 | }, 1653 | "type": "library", 1654 | "extra": { 1655 | "branch-alias": { 1656 | "dev-master": "5.0-dev" 1657 | } 1658 | }, 1659 | "autoload": { 1660 | "psr-4": { 1661 | "Symfony\\Component\\Finder\\": "" 1662 | }, 1663 | "exclude-from-classmap": [ 1664 | "/Tests/" 1665 | ] 1666 | }, 1667 | "notification-url": "https://packagist.org/downloads/", 1668 | "license": [ 1669 | "MIT" 1670 | ], 1671 | "authors": [ 1672 | { 1673 | "name": "Fabien Potencier", 1674 | "email": "fabien@symfony.com" 1675 | }, 1676 | { 1677 | "name": "Symfony Community", 1678 | "homepage": "https://symfony.com/contributors" 1679 | } 1680 | ], 1681 | "description": "Symfony Finder Component", 1682 | "homepage": "https://symfony.com", 1683 | "time": "2020-01-04T14:08:26+00:00" 1684 | }, 1685 | { 1686 | "name": "symfony/polyfill-ctype", 1687 | "version": "v1.13.1", 1688 | "source": { 1689 | "type": "git", 1690 | "url": "https://github.com/symfony/polyfill-ctype.git", 1691 | "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" 1692 | }, 1693 | "dist": { 1694 | "type": "zip", 1695 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 1696 | "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 1697 | "shasum": "" 1698 | }, 1699 | "require": { 1700 | "php": ">=5.3.3" 1701 | }, 1702 | "suggest": { 1703 | "ext-ctype": "For best performance" 1704 | }, 1705 | "type": "library", 1706 | "extra": { 1707 | "branch-alias": { 1708 | "dev-master": "1.13-dev" 1709 | } 1710 | }, 1711 | "autoload": { 1712 | "psr-4": { 1713 | "Symfony\\Polyfill\\Ctype\\": "" 1714 | }, 1715 | "files": [ 1716 | "bootstrap.php" 1717 | ] 1718 | }, 1719 | "notification-url": "https://packagist.org/downloads/", 1720 | "license": [ 1721 | "MIT" 1722 | ], 1723 | "authors": [ 1724 | { 1725 | "name": "Gert de Pagter", 1726 | "email": "BackEndTea@gmail.com" 1727 | }, 1728 | { 1729 | "name": "Symfony Community", 1730 | "homepage": "https://symfony.com/contributors" 1731 | } 1732 | ], 1733 | "description": "Symfony polyfill for ctype functions", 1734 | "homepage": "https://symfony.com", 1735 | "keywords": [ 1736 | "compatibility", 1737 | "ctype", 1738 | "polyfill", 1739 | "portable" 1740 | ], 1741 | "time": "2019-11-27T13:56:44+00:00" 1742 | }, 1743 | { 1744 | "name": "symfony/polyfill-mbstring", 1745 | "version": "v1.13.1", 1746 | "source": { 1747 | "type": "git", 1748 | "url": "https://github.com/symfony/polyfill-mbstring.git", 1749 | "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" 1750 | }, 1751 | "dist": { 1752 | "type": "zip", 1753 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", 1754 | "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", 1755 | "shasum": "" 1756 | }, 1757 | "require": { 1758 | "php": ">=5.3.3" 1759 | }, 1760 | "suggest": { 1761 | "ext-mbstring": "For best performance" 1762 | }, 1763 | "type": "library", 1764 | "extra": { 1765 | "branch-alias": { 1766 | "dev-master": "1.13-dev" 1767 | } 1768 | }, 1769 | "autoload": { 1770 | "psr-4": { 1771 | "Symfony\\Polyfill\\Mbstring\\": "" 1772 | }, 1773 | "files": [ 1774 | "bootstrap.php" 1775 | ] 1776 | }, 1777 | "notification-url": "https://packagist.org/downloads/", 1778 | "license": [ 1779 | "MIT" 1780 | ], 1781 | "authors": [ 1782 | { 1783 | "name": "Nicolas Grekas", 1784 | "email": "p@tchwork.com" 1785 | }, 1786 | { 1787 | "name": "Symfony Community", 1788 | "homepage": "https://symfony.com/contributors" 1789 | } 1790 | ], 1791 | "description": "Symfony polyfill for the Mbstring extension", 1792 | "homepage": "https://symfony.com", 1793 | "keywords": [ 1794 | "compatibility", 1795 | "mbstring", 1796 | "polyfill", 1797 | "portable", 1798 | "shim" 1799 | ], 1800 | "time": "2019-11-27T14:18:11+00:00" 1801 | }, 1802 | { 1803 | "name": "symfony/polyfill-php73", 1804 | "version": "v1.13.1", 1805 | "source": { 1806 | "type": "git", 1807 | "url": "https://github.com/symfony/polyfill-php73.git", 1808 | "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f" 1809 | }, 1810 | "dist": { 1811 | "type": "zip", 1812 | "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f", 1813 | "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f", 1814 | "shasum": "" 1815 | }, 1816 | "require": { 1817 | "php": ">=5.3.3" 1818 | }, 1819 | "type": "library", 1820 | "extra": { 1821 | "branch-alias": { 1822 | "dev-master": "1.13-dev" 1823 | } 1824 | }, 1825 | "autoload": { 1826 | "psr-4": { 1827 | "Symfony\\Polyfill\\Php73\\": "" 1828 | }, 1829 | "files": [ 1830 | "bootstrap.php" 1831 | ], 1832 | "classmap": [ 1833 | "Resources/stubs" 1834 | ] 1835 | }, 1836 | "notification-url": "https://packagist.org/downloads/", 1837 | "license": [ 1838 | "MIT" 1839 | ], 1840 | "authors": [ 1841 | { 1842 | "name": "Nicolas Grekas", 1843 | "email": "p@tchwork.com" 1844 | }, 1845 | { 1846 | "name": "Symfony Community", 1847 | "homepage": "https://symfony.com/contributors" 1848 | } 1849 | ], 1850 | "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", 1851 | "homepage": "https://symfony.com", 1852 | "keywords": [ 1853 | "compatibility", 1854 | "polyfill", 1855 | "portable", 1856 | "shim" 1857 | ], 1858 | "time": "2019-11-27T16:25:15+00:00" 1859 | }, 1860 | { 1861 | "name": "symfony/service-contracts", 1862 | "version": "v2.0.1", 1863 | "source": { 1864 | "type": "git", 1865 | "url": "https://github.com/symfony/service-contracts.git", 1866 | "reference": "144c5e51266b281231e947b51223ba14acf1a749" 1867 | }, 1868 | "dist": { 1869 | "type": "zip", 1870 | "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749", 1871 | "reference": "144c5e51266b281231e947b51223ba14acf1a749", 1872 | "shasum": "" 1873 | }, 1874 | "require": { 1875 | "php": "^7.2.5", 1876 | "psr/container": "^1.0" 1877 | }, 1878 | "suggest": { 1879 | "symfony/service-implementation": "" 1880 | }, 1881 | "type": "library", 1882 | "extra": { 1883 | "branch-alias": { 1884 | "dev-master": "2.0-dev" 1885 | } 1886 | }, 1887 | "autoload": { 1888 | "psr-4": { 1889 | "Symfony\\Contracts\\Service\\": "" 1890 | } 1891 | }, 1892 | "notification-url": "https://packagist.org/downloads/", 1893 | "license": [ 1894 | "MIT" 1895 | ], 1896 | "authors": [ 1897 | { 1898 | "name": "Nicolas Grekas", 1899 | "email": "p@tchwork.com" 1900 | }, 1901 | { 1902 | "name": "Symfony Community", 1903 | "homepage": "https://symfony.com/contributors" 1904 | } 1905 | ], 1906 | "description": "Generic abstractions related to writing services", 1907 | "homepage": "https://symfony.com", 1908 | "keywords": [ 1909 | "abstractions", 1910 | "contracts", 1911 | "decoupling", 1912 | "interfaces", 1913 | "interoperability", 1914 | "standards" 1915 | ], 1916 | "time": "2019-11-18T17:27:11+00:00" 1917 | }, 1918 | { 1919 | "name": "theseer/fdomdocument", 1920 | "version": "1.6.6", 1921 | "source": { 1922 | "type": "git", 1923 | "url": "https://github.com/theseer/fDOMDocument.git", 1924 | "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca" 1925 | }, 1926 | "dist": { 1927 | "type": "zip", 1928 | "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca", 1929 | "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca", 1930 | "shasum": "" 1931 | }, 1932 | "require": { 1933 | "ext-dom": "*", 1934 | "lib-libxml": "*", 1935 | "php": ">=5.3.3" 1936 | }, 1937 | "type": "library", 1938 | "autoload": { 1939 | "classmap": [ 1940 | "src/" 1941 | ] 1942 | }, 1943 | "notification-url": "https://packagist.org/downloads/", 1944 | "license": [ 1945 | "BSD-3-Clause" 1946 | ], 1947 | "authors": [ 1948 | { 1949 | "name": "Arne Blankerts", 1950 | "email": "arne@blankerts.de", 1951 | "role": "lead" 1952 | } 1953 | ], 1954 | "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", 1955 | "homepage": "https://github.com/theseer/fDOMDocument", 1956 | "time": "2017-06-30T11:53:12+00:00" 1957 | }, 1958 | { 1959 | "name": "theseer/tokenizer", 1960 | "version": "1.1.3", 1961 | "source": { 1962 | "type": "git", 1963 | "url": "https://github.com/theseer/tokenizer.git", 1964 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" 1965 | }, 1966 | "dist": { 1967 | "type": "zip", 1968 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1969 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1970 | "shasum": "" 1971 | }, 1972 | "require": { 1973 | "ext-dom": "*", 1974 | "ext-tokenizer": "*", 1975 | "ext-xmlwriter": "*", 1976 | "php": "^7.0" 1977 | }, 1978 | "type": "library", 1979 | "autoload": { 1980 | "classmap": [ 1981 | "src/" 1982 | ] 1983 | }, 1984 | "notification-url": "https://packagist.org/downloads/", 1985 | "license": [ 1986 | "BSD-3-Clause" 1987 | ], 1988 | "authors": [ 1989 | { 1990 | "name": "Arne Blankerts", 1991 | "role": "Developer", 1992 | "email": "arne@blankerts.de" 1993 | } 1994 | ], 1995 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1996 | "time": "2019-06-13T22:48:21+00:00" 1997 | }, 1998 | { 1999 | "name": "webmozart/assert", 2000 | "version": "1.6.0", 2001 | "source": { 2002 | "type": "git", 2003 | "url": "https://github.com/webmozart/assert.git", 2004 | "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" 2005 | }, 2006 | "dist": { 2007 | "type": "zip", 2008 | "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", 2009 | "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", 2010 | "shasum": "" 2011 | }, 2012 | "require": { 2013 | "php": "^5.3.3 || ^7.0", 2014 | "symfony/polyfill-ctype": "^1.8" 2015 | }, 2016 | "conflict": { 2017 | "vimeo/psalm": "<3.6.0" 2018 | }, 2019 | "require-dev": { 2020 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 2021 | }, 2022 | "type": "library", 2023 | "autoload": { 2024 | "psr-4": { 2025 | "Webmozart\\Assert\\": "src/" 2026 | } 2027 | }, 2028 | "notification-url": "https://packagist.org/downloads/", 2029 | "license": [ 2030 | "MIT" 2031 | ], 2032 | "authors": [ 2033 | { 2034 | "name": "Bernhard Schussek", 2035 | "email": "bschussek@gmail.com" 2036 | } 2037 | ], 2038 | "description": "Assertions to validate method input/output with nice error messages.", 2039 | "keywords": [ 2040 | "assert", 2041 | "check", 2042 | "validate" 2043 | ], 2044 | "time": "2019-11-24T13:36:37+00:00" 2045 | } 2046 | ], 2047 | "aliases": [], 2048 | "minimum-stability": "stable", 2049 | "stability-flags": [], 2050 | "prefer-stable": false, 2051 | "prefer-lowest": false, 2052 | "platform": { 2053 | "php": ">=7.2" 2054 | }, 2055 | "platform-dev": [] 2056 | } 2057 | --------------------------------------------------------------------------------