Skip to content

IntArrayTag deserializes values as unsigned integers (inconsistency with IntTag) #136

@Yexeed

Description

@Yexeed

I noticed that while single integers (readInt()) are correctly read as signed values, readIntArray() uses raw unpacking (V*) without sign conversion. This causes negative numbers in arrays to be deserialized as large unsigned integers on 64-bit PHP.

This behavior seems inconsistent with the rest of the library, where IntTag correctly preserves the sign.
Test Code:

$serializer = new LittleEndianNbtSerializer();
// Test values including edges of 32-bit signed integers
$values = [
    1337,
    -127,
    Limits::INT32_MAX,  // From pmmp\utils Max Signed 32-bit
    Limits::INT32_MIN, // From pmmp\utils Min Signed 32-bit
];

echo "--- 1. Testing Single IntTags (Works correctly) ---\n";
foreach ($values as $expected) {
    $tag = CompoundTag::create()->setInt("val", $expected);
    // Write -> Read cycle
    $actual = ($serializer->read($serializer->write(new TreeRoot($tag))))
        ->mustGetCompoundTag()
        ->getInt("val");

    echo "Input: $expected -> Output: $actual\n";
}

echo "\n--- 2. Testing IntArrayTag (Returns Unsigned) ---\n";
$tag = CompoundTag::create()->setIntArray("vals", $values);
// Write -> Read cycle
$actualArray = ($serializer->read($serializer->write(new TreeRoot($tag))))
    ->mustGetCompoundTag()
    ->getIntArray("vals");

foreach ($actualArray as $i => $actual) {
    $expected = $values[$i];
    echo "Input: $expected -> Output: $actual\n";
}

outputs:

--- 1. Testing Single IntTags (Works correctly) ---
Input: 1337 -> Output: 1337
Input: -127 -> Output: -127
Input: 2147483647 -> Output: 2147483647
Input: -2147483648 -> Output: -2147483648

--- 2. Testing IntArrayTag (Returns Unsigned) ---
Input: 1337 -> Output: 1337
Input: -127 -> Output: 4294967169
Input: 2147483647 -> Output: 2147483647
Input: -2147483648 -> Output: 2147483648

My initial chain of thought:
My goal was to write LENativeNbtSerializer and LENativeNbtDeserializer (since ByteBuffer is split into reader/writer in ext-encoding). I was looking at the NBT specifications for Bedrock (where types are generally defined as Int16, Int32, etc.). Since NbtStreamReader explicitly provides methods like readSignedShort, I assumed the library intends to strictly adhere to signedness. However, seeing readIntArray returning unsigned values made me wonder if this is an intended performance trade-off (avoiding iteration for signInt) or an oversight, as it breaks arithmetic logic when expecting standard NBT behavior.

Environment

user@machine:~$ php -r "echo 'PHP: ' . PHP_VERSION . PHP_EOL; echo 'OS: ' . PHP_OS . PHP_EOL; echo 'Arch: ' . (PHP_INT_SIZE * 8) . '-bit' . PHP_EOL; echo 'Endianness: ' . (unpack('S', '\x01\x00')[1] === 1 ? 'Little' : 'Big') . PHP_EOL;"
PHP: 8.3.25
OS: Linux
Arch: 64-bit
Endianness: Big

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions