├── 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 |
--------------------------------------------------------------------------------