Skip to content

Commit 644c463

Browse files
committed
Merge branch 'stable' into ext-encoding
2 parents 5d6a088 + 51b8d6a commit 644c463

12 files changed

Lines changed: 264 additions & 15 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
id: composer-cache
2828
uses: actions/cache@v4
2929
with:
30-
path: "~/.composer/cache"
30+
path: "~/.cache/composer"
3131
key: "php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}"
3232
restore-keys: "php-${{ matrix.php }}-composer-"
3333

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,102 @@
22
![CI](https://github.com/pmmp/NBT/workflows/CI/badge.svg)
33

44
PHP library for working with the NBT (Named Binary Tag) data storage format, as designed by Mojang.
5+
6+
## Examples
7+
8+
The library provides two NBT serializers: `BigEndianNbtSerializer` (typically suited for Minecraft Java) and `LittleEndianNbtSerializer` (typically used by Bedrock for storage).
9+
10+
Note: Bedrock network NBT (which uses varints in some places) is not implemented here. See [BedrockProtocol](https://github.com/pmmp/BedrockProtocol) for that.
11+
12+
Note: `TAG_LongArray` is not supported because Bedrock doesn't support it, so it's not clear how NBT trees intended for serialization by Bedrock would be restricted from using it.
13+
14+
### Reading data
15+
```php
16+
use pocketmine\nbt\LittleEndianNbtSerializer;
17+
use pocketmine\nbt\NbtDataException;
18+
use pocketmine\nbt\NoSuchTagException;
19+
use pocketmine\nbt\UnexpectedTagTypeException;
20+
use pocketmine\nbt\tag\StringTag;
21+
22+
$serializer = new LittleEndianNbtSerializer();
23+
$optionalStartOffset = 0;
24+
$optionalMaxDepth = 0; //unlimited by default
25+
$treeRoot = $serializer->read($yourInputBytes, $optionalStartOffset, $optionalMaxDepth);
26+
27+
try{
28+
//If you expect a TAG_Compound root (the most common case)
29+
$data = $treeRoot->mustGetCompoundTag());
30+
}catch(NbtDataException $e){
31+
var_dump("root isn't a TAG_Compound");
32+
}
33+
//For other, less common cases where the root tag isn't a compound
34+
var_dump($treeRoot->getTag());
35+
36+
var_dump($treeRoot->getName()); //typically empty
37+
38+
try{
39+
var_dump($data->getString("hello"));
40+
}catch(UnexpectedTagTypeException $e){
41+
var_dump("not a TAG_String");
42+
}catch(NoSuchTagException $e){
43+
var_dump("no such tag called \"hello\"");
44+
}
45+
46+
try{
47+
$nestedCompound = $data->getCompoundTag("nestedCompound");
48+
}catch(UnexpectedTagTypeException $e){
49+
var_dump("not a TAG_Compound");
50+
}
51+
if($nestedCompound === null){
52+
//For legacy BC reasons, this works differently than the primitive type getters like getString() getInt() etc
53+
var_dump("no such nested tag called \"nestedCompound\"");
54+
}
55+
56+
try{
57+
$nestedList = $data->getListTag("listOfStrings", StringTag::class);
58+
}catch(UnexpectedTagTypeException $e){
59+
var_dump("not a list of strings");
60+
}
61+
if($nestedList === null){
62+
//For legacy BC reasons, getListTag() returns NULL if the tag doesn't exist
63+
var_dump("no such nested tag called \"listOfStrings\"");
64+
}
65+
var_dump($nestedList->getValue()); //StringTag[]
66+
```
67+
68+
### Writing data
69+
70+
```php
71+
use pocketmine\nbt\LittleEndianNbtSerializer;
72+
use pocketmine\nbt\TreeRoot;
73+
use pocketmine\nbt\tag\CompoundTag;
74+
use pocketmine\nbt\tag\IntTag;
75+
use pocketmine\nbt\tag\ListTag;
76+
use pocketmine\nbt\tag\StringTag;
77+
78+
$compound = CompoundTag::create()
79+
->setByte("byte", 1)
80+
->setInt("int", 2)
81+
->setTag("list", new ListTag([
82+
new StringTag("item1"),
83+
new StringTag("item2")
84+
]))
85+
->setTag("compound", CompoundTag::create()
86+
->setByte("nestedByte", 1)
87+
);
88+
89+
//empty lists infer their type from the first value added
90+
$list = new ListTag();
91+
$list->push(new StringTag("hello")); //list is now ListTag<StringTag>
92+
try{
93+
$list->push(new IntTag(1));
94+
}catch(\TypeError $e){
95+
var_dump("can't push an int into a string list");
96+
}
97+
98+
$serializer = new LittleEndianNbtSerializer();
99+
$bytes = $serializer->write($treeRoot);
100+
101+
//or if you have a Tag instance
102+
$bytes = $serializer->write(new TreeRoot($data, "optionalRootName"));
103+
```

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
"ext-encoding": "~1.0.0"
99
},
1010
"require-dev": {
11-
"phpstan/phpstan": "2.1.0",
11+
"phpstan/phpstan": "2.1.27",
1212
"phpunit/phpunit": "^9.5",
1313
"phpstan/extension-installer": "^1.0",
14-
"phpstan/phpstan-strict-rules": "^2.0"
14+
"phpstan/phpstan-strict-rules": "^2.0",
15+
"phpstan/phpstan-phpunit": "^2.0"
1516
},
1617
"license": "LGPL-3.0",
1718
"autoload": {

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
includes:
2+
- tests/phpstan/configs/expected-test-errors.neon
23
- tests/phpstan/configs/impossible-mixed.neon
34
- tests/phpstan/configs/phpstan-bugs.neon
45

@@ -7,3 +8,4 @@ parameters:
78
treatPhpDocTypesAsCertain: false
89
paths:
910
- src
11+
- tests

src/tag/CompoundTag.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,26 @@ public function getTag(string $name) : ?Tag{
9090
/**
9191
* Returns the ListTag with the specified name, or null if it does not exist. Triggers an exception if a tag exists
9292
* with that name and the tag is not a ListTag.
93+
*
94+
* @phpstan-template TValue of Tag
95+
* @phpstan-param class-string<TValue> $tagClass
96+
* @phpstan-return ListTag<TValue>|null
97+
*
98+
* @throws UnexpectedTagTypeException
9399
*/
94-
public function getListTag(string $name) : ?ListTag{
100+
public function getListTag(string $name, string $tagClass = Tag::class) : ?ListTag{
95101
$tag = $this->getTag($name);
96-
if($tag !== null && !($tag instanceof ListTag)){
97-
throw new UnexpectedTagTypeException("Expected a tag of type " . ListTag::class . ", got " . get_class($tag));
102+
if($tag !== null){
103+
if(!$tag instanceof ListTag){
104+
throw new UnexpectedTagTypeException("Expected a tag of type " . ListTag::class . ", got " . get_class($tag));
105+
}
106+
$casted = $tag->cast($tagClass);
107+
if($casted === null){
108+
throw new UnexpectedTagTypeException("Unable to cast list to ListTag<$tagClass>");
109+
}
110+
return $casted;
98111
}
99-
return $tag;
112+
return null;
100113
}
101114

102115
/**

src/tag/ListTag.php

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
use function str_repeat;
4343

4444
/**
45-
* @phpstan-implements \IteratorAggregate<int, Tag>
45+
* @phpstan-template TValue of Tag = Tag
46+
* @phpstan-implements \IteratorAggregate<int, TValue>
4647
*/
4748
final class ListTag extends Tag implements \Countable, \IteratorAggregate{
4849
use NoDynamicFieldsTrait;
@@ -51,12 +52,13 @@ final class ListTag extends Tag implements \Countable, \IteratorAggregate{
5152
private $tagType;
5253
/**
5354
* @var Tag[]
54-
* @phpstan-var list<Tag>
55+
* @phpstan-var list<TValue>
5556
*/
5657
private $value = [];
5758

5859
/**
5960
* @param Tag[] $value
61+
* @phpstan-param TValue[] $value
6062
*/
6163
public function __construct(array $value = [], int $tagType = NBT::TAG_End){
6264
self::restrictArgCount(__METHOD__, func_num_args(), 2);
@@ -68,7 +70,7 @@ public function __construct(array $value = [], int $tagType = NBT::TAG_End){
6870

6971
/**
7072
* @return Tag[]
71-
* @phpstan-return list<Tag>
73+
* @phpstan-return list<TValue>
7274
*/
7375
public function getValue() : array{
7476
return $this->value;
@@ -83,6 +85,31 @@ public function getAllValues() : array{
8385
return array_map(fn(Tag $t) => $t->getValue(), $this->value);
8486
}
8587

88+
/**
89+
* @phpstan-template TTarget of Tag
90+
* @phpstan-param class-string<TTarget> $tagClass
91+
* @phpstan-this-out self<TTarget> $this
92+
*/
93+
private function checkTagClass(string $tagClass) : bool{
94+
return count($this->value) === 0 || $this->first() instanceof $tagClass;
95+
}
96+
97+
/**
98+
* Returns $this if the tag values are of type $tagClass, null otherwise.
99+
* The returned value will have the proper PHPStan generic types set if it matches.
100+
*
101+
* If the list is empty, the cast will always succeed, as empty lists infer their
102+
* type from the first value inserted.
103+
*
104+
* @phpstan-template TTarget of Tag
105+
* @phpstan-param class-string<TTarget> $tagClass
106+
*
107+
* @phpstan-return self<TTarget>|null
108+
*/
109+
public function cast(string $tagClass) : ?self{
110+
return $this->checkTagClass($tagClass) ? $this : null;
111+
}
112+
86113
public function count() : int{
87114
return count($this->value);
88115
}
@@ -93,6 +120,10 @@ public function getCount() : int{
93120

94121
/**
95122
* Appends the specified tag to the end of the list.
123+
*
124+
* @phpstan-template TNewValue of TValue
125+
* @phpstan-param TNewValue $tag
126+
* @phpstan-this-out self<TNewValue>
96127
*/
97128
public function push(Tag $tag) : void{
98129
$this->checkTagType($tag);
@@ -101,6 +132,7 @@ public function push(Tag $tag) : void{
101132

102133
/**
103134
* Removes the last tag from the list and returns it.
135+
* @phpstan-return TValue
104136
*/
105137
public function pop() : Tag{
106138
if(count($this->value) === 0){
@@ -111,6 +143,10 @@ public function pop() : Tag{
111143

112144
/**
113145
* Adds the specified tag to the start of the list.
146+
*
147+
* @phpstan-template TNewValue of TValue
148+
* @phpstan-param TNewValue $tag
149+
* @phpstan-this-out self<TNewValue>
114150
*/
115151
public function unshift(Tag $tag) : void{
116152
$this->checkTagType($tag);
@@ -119,6 +155,7 @@ public function unshift(Tag $tag) : void{
119155

120156
/**
121157
* Removes the first tag from the list and returns it.
158+
* @phpstan-return TValue
122159
*/
123160
public function shift() : Tag{
124161
if(count($this->value) === 0){
@@ -131,6 +168,10 @@ public function shift() : Tag{
131168
* Inserts a tag into the list between existing tags, at the specified offset. Later values in the list are moved up
132169
* by 1 position.
133170
*
171+
* @phpstan-template TNewValue of TValue
172+
* @phpstan-param TNewValue $tag
173+
* @phpstan-this-out self<TNewValue>
174+
*
134175
* @return void
135176
* @throws \OutOfRangeException if the offset is not within the bounds of the list
136177
*/
@@ -158,6 +199,8 @@ public function remove(int $offset) : void{
158199
/**
159200
* Returns the tag at the specified offset.
160201
*
202+
* @phpstan-return TValue
203+
*
161204
* @throws \OutOfRangeException if the offset is not within the bounds of the list
162205
*/
163206
public function get(int $offset) : Tag{
@@ -169,6 +212,7 @@ public function get(int $offset) : Tag{
169212

170213
/**
171214
* Returns the element in the first position of the list, without removing it.
215+
* @phpstan-return TValue
172216
*/
173217
public function first() : Tag{
174218
if(count($this->value) === 0){
@@ -179,6 +223,7 @@ public function first() : Tag{
179223

180224
/**
181225
* Returns the element in the last position in the list (the end), without removing it.
226+
* @phpstan-return TValue
182227
*/
183228
public function last() : Tag{
184229
if(count($this->value) === 0){
@@ -190,6 +235,10 @@ public function last() : Tag{
190235
/**
191236
* Overwrites the tag at the specified offset.
192237
*
238+
* @phpstan-template TNewValue of TValue
239+
* @phpstan-param TNewValue $tag
240+
* @phpstan-this-out self<TNewValue>
241+
*
193242
* @throws \OutOfRangeException if the offset is not within the bounds of the list
194243
*/
195244
public function set(int $offset, Tag $tag) : void{
@@ -230,6 +279,7 @@ public function getTagType() : int{
230279
}
231280

232281
/**
282+
* @deprecated
233283
* Sets the type of tag that can be added to this list. If TAG_End is used, the type will be auto-detected from the
234284
* first tag added to the list.
235285
*
@@ -307,7 +357,7 @@ protected function makeCopy(){
307357

308358
/**
309359
* @return \Generator|Tag[]
310-
* @phpstan-return \Generator<int, Tag, void, void>
360+
* @phpstan-return \Generator<int, TValue, void, void>
311361
*/
312362
public function getIterator() : \Generator{
313363
yield from $this->value;

src/tag/Tag.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ abstract protected function stringifyValue(int $indentation) : string;
5757
* Used for cloning tags in tags that have children.
5858
*
5959
* @throws \RuntimeException if a recursive dependency was detected
60+
* @return static
6061
*/
6162
public function safeClone() : Tag{
6263
if($this->cloning){
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Expression "clone \$tag" on a separate line does not do anything\.$#'
5+
identifier: expr.resultUnused
6+
count: 1
7+
path: ../../phpunit/tag/CompoundTagTest.php
8+
9+
-
10+
message: '#^Expression "clone \$tag" on a separate line does not do anything\.$#'
11+
identifier: expr.resultUnused
12+
count: 1
13+
path: ../../phpunit/tag/ListTagTest.php

tests/phpstan/configs/phpstan-bugs.neon

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,37 @@ parameters:
1313
path: ../../../src/tag/IntArrayTag.php
1414

1515
-
16-
message: '#^Property pocketmine\\nbt\\tag\\ListTag\:\:\$value \(list\<pocketmine\\nbt\\tag\\Tag\>\) does not accept non\-empty\-array\<int\<0, max\>, pocketmine\\nbt\\tag\\Tag\>\.$#'
16+
message: '#^Property pocketmine\\nbt\\tag\\ListTag\<TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\>\:\:\$value \(list\<TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\>\) does not accept non\-empty\-array\<int\<0, max\>, \(TNewValue of TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\)\|TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\>\.$#'
1717
identifier: assign.propertyType
1818
count: 1
1919
path: ../../../src/tag/ListTag.php
20+
21+
-
22+
message: '#^Property pocketmine\\nbt\\tag\\ListTag\<TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\>\:\:\$value \(list\<TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\>\) does not accept non\-empty\-list\<\(TNewValue of TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\)\|TValue of pocketmine\\nbt\\tag\\Tag \= pocketmine\\nbt\\tag\\Tag\>\.$#'
23+
identifier: assign.propertyType
24+
count: 3
25+
path: ../../../src/tag/ListTag.php
26+
27+
-
28+
message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#'
29+
identifier: function.alreadyNarrowedType
30+
count: 1
31+
path: ../../phpunit/tag/CompoundTagTest.php
32+
33+
-
34+
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertNotSame\(\) with \*NEVER\* and pocketmine\\nbt\\tag\\Tag will always evaluate to true\.$#'
35+
identifier: staticMethod.alreadyNarrowedType
36+
count: 1
37+
path: ../../phpunit/tag/ListTagTest.php
38+
39+
-
40+
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with \*NEVER\* and pocketmine\\nbt\\tag\\Tag will always evaluate to false\.$#'
41+
identifier: staticMethod.impossibleType
42+
count: 1
43+
path: ../../phpunit/tag/ListTagTest.php
44+
45+
-
46+
message: '#^Instanceof between \*NEVER\* and pocketmine\\nbt\\tag\\ImmutableTag will always evaluate to false\.$#'
47+
identifier: instanceof.alwaysFalse
48+
count: 1
49+
path: ../../phpunit/tag/ListTagTest.php

0 commit comments

Comments
 (0)