From 6dbb186286213d90445162e8f7e38d2bc5b2d3a6 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 17 Jul 2025 16:15:21 +0200 Subject: [PATCH 01/25] Add `woltlab/webp-exif` as a composer dependency --- .../files/lib/system/api/composer.json | 3 +- .../files/lib/system/api/composer.lock | 112 +- .../system/api/composer/autoload_classmap.php | 45 + .../system/api/composer/autoload_files.php | 2 +- .../lib/system/api/composer/autoload_psr4.php | 2 + .../system/api/composer/autoload_static.php | 57 +- .../lib/system/api/composer/installed.json | 116 ++ .../lib/system/api/composer/installed.php | 30 +- .../lib/system/api/nelexa/buffer/.gitignore | 5 + .../lib/system/api/nelexa/buffer/.travis.yml | 21 + .../lib/system/api/nelexa/buffer/README.md | 456 +++++++ .../system/api/nelexa/buffer/composer.json | 45 + .../lib/system/api/nelexa/buffer/phpunit.xml | 22 + .../buffer/src/Nelexa/Buffer/Buffer.php | 1170 +++++++++++++++++ .../src/Nelexa/Buffer/BufferException.php | 13 + .../nelexa/buffer/src/Nelexa/Buffer/Cast.php | 154 +++ .../buffer/src/Nelexa/Buffer/FileBuffer.php | 58 + .../Nelexa/Buffer/MemoryResourceBuffer.php | 34 + .../src/Nelexa/Buffer/ResourceBuffer.php | 306 +++++ .../buffer/src/Nelexa/Buffer/StringBuffer.php | 230 ++++ .../buffer/src/Nelexa/Buffer/TempBuffer.php | 36 + .../BinaryFormat/BinaryFileInterface.php | 12 + .../Buffer/BinaryFormat/BinaryFileItem.php | 93 ++ .../BinaryFormat/BinaryFileTestFormat.php | 100 ++ .../tests/Nelexa/Buffer/BufferTestCase.php | 583 ++++++++ .../buffer/tests/Nelexa/Buffer/CastTest.php | 440 +++++++ .../tests/Nelexa/Buffer/FileBufferTest.php | 42 + .../Nelexa/Buffer/GetInfoIcoFileTest.php | 51 + .../Buffer/MemoryResourceBufferTest.php | 16 + .../Nelexa/Buffer/ResourceBufferTest.php | 17 + .../tests/Nelexa/Buffer/StringBufferTest.php | 16 + .../tests/Nelexa/Buffer/TempBufferTest.php | 16 + .../buffer/tests/Nelexa/Buffer/test.ico | Bin 0 -> 1150 bytes .../api/woltlab/webp-exif/.gitattributes | 5 + .../system/api/woltlab/webp-exif/.gitignore | 4 + .../lib/system/api/woltlab/webp-exif/LICENSE | 21 + .../system/api/woltlab/webp-exif/README.md | 39 + .../api/woltlab/webp-exif/composer.json | 44 + .../api/woltlab/webp-exif/src/Chunk/Alph.php | 23 + .../api/woltlab/webp-exif/src/Chunk/Anim.php | 23 + .../api/woltlab/webp-exif/src/Chunk/Anmf.php | 115 ++ .../api/woltlab/webp-exif/src/Chunk/Chunk.php | 39 + .../AnimationFrameWithoutBitstream.php | 24 + .../Chunk/Exception/DimensionsExceedInt32.php | 23 + .../Chunk/Exception/EmptyAnimationFrame.php | 24 + .../src/Chunk/Exception/ExpectedKeyFrame.php | 23 + .../Chunk/Exception/MissingExifExtension.php | 26 + .../src/Chunk/Exception/MissingMagicByte.php | 23 + .../Exception/UnknownChunkWithKnownFourCC.php | 24 + .../Chunk/Exception/UnsupportedVersion.php | 23 + .../api/woltlab/webp-exif/src/Chunk/Exif.php | 85 ++ .../api/woltlab/webp-exif/src/Chunk/Iccp.php | 23 + .../webp-exif/src/Chunk/UnknownChunk.php | 25 + .../api/woltlab/webp-exif/src/Chunk/Vp8.php | 66 + .../api/woltlab/webp-exif/src/Chunk/Vp8l.php | 63 + .../api/woltlab/webp-exif/src/Chunk/Vp8x.php | 227 ++++ .../api/woltlab/webp-exif/src/Chunk/Xmp.php | 23 + .../api/woltlab/webp-exif/src/ChunkType.php | 42 + .../api/woltlab/webp-exif/src/Decoder.php | 121 ++ .../api/woltlab/webp-exif/src/Encoder.php | 179 +++ .../Exception/ExtraChunksInSimpleFormat.php | 24 + .../src/Exception/ExtraVp8xChunk.php | 22 + .../src/Exception/FileSizeMismatch.php | 22 + .../src/Exception/LengthOutOfBounds.php | 24 + .../webp-exif/src/Exception/MissingChunks.php | 22 + .../webp-exif/src/Exception/NotEnoughData.php | 22 + .../src/Exception/UnexpectedChunk.php | 23 + .../src/Exception/UnexpectedEndOfFile.php | 24 + .../src/Exception/UnrecognizedFileFormat.php | 22 + .../src/Exception/Vp8xAbsentChunk.php | 22 + .../Exception/Vp8xHeaderLengthMismatch.php | 22 + .../src/Exception/Vp8xMissingImageData.php | 28 + .../src/Exception/Vp8xWithoutChunks.php | 22 + .../src/Exception/WebpExifException.php | 16 + .../system/api/woltlab/webp-exif/src/WebP.php | 304 +++++ 75 files changed, 6269 insertions(+), 10 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/.gitignore create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/.travis.yml create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/README.md create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/composer.json create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/phpunit.xml create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/Buffer.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/BufferException.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/Cast.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/FileBuffer.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/MemoryResourceBuffer.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/ResourceBuffer.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/StringBuffer.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/TempBuffer.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BinaryFormat/BinaryFileInterface.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BinaryFormat/BinaryFileItem.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BinaryFormat/BinaryFileTestFormat.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BufferTestCase.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/CastTest.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/FileBufferTest.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/GetInfoIcoFileTest.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/MemoryResourceBufferTest.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/ResourceBufferTest.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/StringBufferTest.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/TempBufferTest.php create mode 100644 wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/test.ico create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitattributes create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitignore create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/LICENSE create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/README.md create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/composer.json create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Alph.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anim.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anmf.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Chunk.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/AnimationFrameWithoutBitstream.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/DimensionsExceedInt32.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/EmptyAnimationFrame.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/ExpectedKeyFrame.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingExifExtension.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingMagicByte.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnknownChunkWithKnownFourCC.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnsupportedVersion.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exif.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Iccp.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/UnknownChunk.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8l.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8x.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Xmp.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/ChunkType.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Decoder.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Encoder.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraChunksInSimpleFormat.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraVp8xChunk.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/FileSizeMismatch.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/LengthOutOfBounds.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/MissingChunks.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/NotEnoughData.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedChunk.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedEndOfFile.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnrecognizedFileFormat.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xAbsentChunk.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xHeaderLengthMismatch.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xMissingImageData.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xWithoutChunks.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/WebpExifException.php create mode 100644 wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/WebP.php diff --git a/wcfsetup/install/files/lib/system/api/composer.json b/wcfsetup/install/files/lib/system/api/composer.json index 882f3f453a..e21744b403 100644 --- a/wcfsetup/install/files/lib/system/api/composer.json +++ b/wcfsetup/install/files/lib/system/api/composer.json @@ -35,7 +35,8 @@ "symfony/polyfill-php83": "^1.32", "symfony/polyfill-php84": "^1.32", "symfony/polyfill-php85": "^1.32", - "willdurand/negotiation": "^3.1" + "willdurand/negotiation": "^3.1", + "woltlab/webp-exif": "^0.1.0" }, "replace": { "paragonie/random_compat": "*", diff --git a/wcfsetup/install/files/lib/system/api/composer.lock b/wcfsetup/install/files/lib/system/api/composer.lock index 8b8b60ec67..0f60a8a2f9 100644 --- a/wcfsetup/install/files/lib/system/api/composer.lock +++ b/wcfsetup/install/files/lib/system/api/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d95d8794edeb0688df52be753a441985", + "content-hash": "eed2db0cb13e347de015269424ed1f52", "packages": [ { "name": "brick/math", @@ -1160,6 +1160,66 @@ }, "time": "2025-01-29T17:44:07+00:00" }, + { + "name": "nelexa/buffer", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/Ne-Lexa/php-byte-buffer.git", + "reference": "c97bc126d5fbe0c94152fce406a054f681149fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ne-Lexa/php-byte-buffer/zipball/c97bc126d5fbe0c94152fce406a054f681149fac", + "reference": "c97bc126d5fbe0c94152fce406a054f681149fac", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nelexa\\Buffer\\": "src/Nelexa/Buffer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ne-Lexa", + "email": "alexey@nelexa.ru" + } + ], + "description": "Reading And Writing Binary Data (incl. primitive types, ex. byte, ubyte, short, ushort, int, uint, long, float, double). The classes also help with porting the I/O operations of the JAVA code.", + "keywords": [ + "Double", + "binary", + "byte", + "float", + "int", + "io", + "java", + "long", + "pack", + "primitive", + "short", + "ubyte", + "uint", + "unpack", + "ushort" + ], + "support": { + "issues": "https://github.com/Ne-Lexa/php-byte-buffer/issues", + "source": "https://github.com/Ne-Lexa/php-byte-buffer/tree/master" + }, + "time": "2019-05-25T17:47:34+00:00" + }, { "name": "nikic/fast-route", "version": "2.0.0-beta1", @@ -3267,6 +3327,56 @@ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" }, "time": "2022-01-30T20:08:53+00:00" + }, + { + "name": "woltlab/webp-exif", + "version": "v0.1.0", + "source": { + "type": "git", + "url": "https://github.com/WoltLab/webp-exif.git", + "reference": "8ec500ac949935c22a624595f99703b40feb4eaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WoltLab/webp-exif/zipball/8ec500ac949935c22a624595f99703b40feb4eaa", + "reference": "8ec500ac949935c22a624595f99703b40feb4eaa", + "shasum": "" + }, + "require": { + "ext-exif": "*", + "nelexa/buffer": "^1.3", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "symfony/polyfill-php84": "^1.31" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/php-code-coverage": "^12.0", + "phpunit/phpunit": "^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "WoltLab\\WebpExif\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extract and embed EXIF metadata from and to WebP images", + "keywords": [ + "Webp", + "exif" + ], + "support": { + "issues": "https://github.com/WoltLab/webp-exif/issues", + "source": "https://github.com/WoltLab/webp-exif/tree/v0.1.0" + }, + "time": "2025-07-08T10:41:59+00:00" } ], "packages-dev": [], diff --git a/wcfsetup/install/files/lib/system/api/composer/autoload_classmap.php b/wcfsetup/install/files/lib/system/api/composer/autoload_classmap.php index 88a58ddde9..616e7a7fec 100644 --- a/wcfsetup/install/files/lib/system/api/composer/autoload_classmap.php +++ b/wcfsetup/install/files/lib/system/api/composer/autoload_classmap.php @@ -1224,6 +1224,14 @@ 'Negotiation\\Exception\\InvalidMediaType' => $vendorDir . '/willdurand/negotiation/src/Negotiation/Exception/InvalidMediaType.php', 'Negotiation\\LanguageNegotiator' => $vendorDir . '/willdurand/negotiation/src/Negotiation/LanguageNegotiator.php', 'Negotiation\\Negotiator' => $vendorDir . '/willdurand/negotiation/src/Negotiation/Negotiator.php', + 'Nelexa\\Buffer\\Buffer' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/Buffer.php', + 'Nelexa\\Buffer\\BufferException' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/BufferException.php', + 'Nelexa\\Buffer\\Cast' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/Cast.php', + 'Nelexa\\Buffer\\FileBuffer' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/FileBuffer.php', + 'Nelexa\\Buffer\\MemoryResourceBuffer' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/MemoryResourceBuffer.php', + 'Nelexa\\Buffer\\ResourceBuffer' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/ResourceBuffer.php', + 'Nelexa\\Buffer\\StringBuffer' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/StringBuffer.php', + 'Nelexa\\Buffer\\TempBuffer' => $vendorDir . '/nelexa/buffer/src/Nelexa/Buffer/TempBuffer.php', 'NoDiscard' => $vendorDir . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php', 'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php', 'ParagonIE\\ConstantTime\\Base32' => $vendorDir . '/paragonie/constant_time_encoding/src/Base32.php', @@ -2008,4 +2016,41 @@ 'Webmozart\\Assert\\Assert' => $vendorDir . '/webmozart/assert/src/Assert.php', 'Webmozart\\Assert\\InvalidArgumentException' => $vendorDir . '/webmozart/assert/src/InvalidArgumentException.php', 'Webmozart\\Assert\\Mixin' => $vendorDir . '/webmozart/assert/src/Mixin.php', + 'WoltLab\\WebpExif\\ChunkType' => $vendorDir . '/woltlab/webp-exif/src/ChunkType.php', + 'WoltLab\\WebpExif\\Chunk\\Alph' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Alph.php', + 'WoltLab\\WebpExif\\Chunk\\Anim' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Anim.php', + 'WoltLab\\WebpExif\\Chunk\\Anmf' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Anmf.php', + 'WoltLab\\WebpExif\\Chunk\\Chunk' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Chunk.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\AnimationFrameWithoutBitstream' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/AnimationFrameWithoutBitstream.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\DimensionsExceedInt32' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/DimensionsExceedInt32.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\EmptyAnimationFrame' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/EmptyAnimationFrame.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\ExpectedKeyFrame' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/ExpectedKeyFrame.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\MissingExifExtension' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/MissingExifExtension.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\MissingMagicByte' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/MissingMagicByte.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\UnknownChunkWithKnownFourCC' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/UnknownChunkWithKnownFourCC.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\UnsupportedVersion' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exception/UnsupportedVersion.php', + 'WoltLab\\WebpExif\\Chunk\\Exif' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Exif.php', + 'WoltLab\\WebpExif\\Chunk\\Iccp' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Iccp.php', + 'WoltLab\\WebpExif\\Chunk\\UnknownChunk' => $vendorDir . '/woltlab/webp-exif/src/Chunk/UnknownChunk.php', + 'WoltLab\\WebpExif\\Chunk\\Vp8' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Vp8.php', + 'WoltLab\\WebpExif\\Chunk\\Vp8l' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Vp8l.php', + 'WoltLab\\WebpExif\\Chunk\\Vp8x' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Vp8x.php', + 'WoltLab\\WebpExif\\Chunk\\Xmp' => $vendorDir . '/woltlab/webp-exif/src/Chunk/Xmp.php', + 'WoltLab\\WebpExif\\Decoder' => $vendorDir . '/woltlab/webp-exif/src/Decoder.php', + 'WoltLab\\WebpExif\\Encoder' => $vendorDir . '/woltlab/webp-exif/src/Encoder.php', + 'WoltLab\\WebpExif\\Exception\\ExtraChunksInSimpleFormat' => $vendorDir . '/woltlab/webp-exif/src/Exception/ExtraChunksInSimpleFormat.php', + 'WoltLab\\WebpExif\\Exception\\ExtraVp8xChunk' => $vendorDir . '/woltlab/webp-exif/src/Exception/ExtraVp8xChunk.php', + 'WoltLab\\WebpExif\\Exception\\FileSizeMismatch' => $vendorDir . '/woltlab/webp-exif/src/Exception/FileSizeMismatch.php', + 'WoltLab\\WebpExif\\Exception\\LengthOutOfBounds' => $vendorDir . '/woltlab/webp-exif/src/Exception/LengthOutOfBounds.php', + 'WoltLab\\WebpExif\\Exception\\MissingChunks' => $vendorDir . '/woltlab/webp-exif/src/Exception/MissingChunks.php', + 'WoltLab\\WebpExif\\Exception\\NotEnoughData' => $vendorDir . '/woltlab/webp-exif/src/Exception/NotEnoughData.php', + 'WoltLab\\WebpExif\\Exception\\UnexpectedChunk' => $vendorDir . '/woltlab/webp-exif/src/Exception/UnexpectedChunk.php', + 'WoltLab\\WebpExif\\Exception\\UnexpectedEndOfFile' => $vendorDir . '/woltlab/webp-exif/src/Exception/UnexpectedEndOfFile.php', + 'WoltLab\\WebpExif\\Exception\\UnrecognizedFileFormat' => $vendorDir . '/woltlab/webp-exif/src/Exception/UnrecognizedFileFormat.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xAbsentChunk' => $vendorDir . '/woltlab/webp-exif/src/Exception/Vp8xAbsentChunk.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xHeaderLengthMismatch' => $vendorDir . '/woltlab/webp-exif/src/Exception/Vp8xHeaderLengthMismatch.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xMissingImageData' => $vendorDir . '/woltlab/webp-exif/src/Exception/Vp8xMissingImageData.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xWithoutChunks' => $vendorDir . '/woltlab/webp-exif/src/Exception/Vp8xWithoutChunks.php', + 'WoltLab\\WebpExif\\Exception\\WebpExifException' => $vendorDir . '/woltlab/webp-exif/src/Exception/WebpExifException.php', + 'WoltLab\\WebpExif\\WebP' => $vendorDir . '/woltlab/webp-exif/src/WebP.php', ); diff --git a/wcfsetup/install/files/lib/system/api/composer/autoload_files.php b/wcfsetup/install/files/lib/system/api/composer/autoload_files.php index bc5bdec724..a4827056e4 100644 --- a/wcfsetup/install/files/lib/system/api/composer/autoload_files.php +++ b/wcfsetup/install/files/lib/system/api/composer/autoload_files.php @@ -11,6 +11,7 @@ '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', + '9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php', '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', '07d7f1a47144818725fd8d91a907ac57' => $vendorDir . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php', 'da94ac5d3ca7d2dbab84ce561ce72bfd' => $vendorDir . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php', @@ -22,6 +23,5 @@ '253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php', '5897ea0ac4cccf14d323035e65887801' => $vendorDir . '/symfony/polyfill-php82/bootstrap.php', '662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php', - '9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php', '606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php', ); diff --git a/wcfsetup/install/files/lib/system/api/composer/autoload_psr4.php b/wcfsetup/install/files/lib/system/api/composer/autoload_psr4.php index ce204dc2b9..4f1607d127 100644 --- a/wcfsetup/install/files/lib/system/api/composer/autoload_psr4.php +++ b/wcfsetup/install/files/lib/system/api/composer/autoload_psr4.php @@ -6,6 +6,7 @@ $baseDir = $vendorDir; return array( + 'WoltLab\\WebpExif\\' => array($vendorDir . '/woltlab/webp-exif/src'), 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'), 'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'), 'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'), @@ -28,6 +29,7 @@ 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'), 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'), + 'Nelexa\\Buffer\\' => array($vendorDir . '/nelexa/buffer/src/Nelexa/Buffer'), 'Negotiation\\' => array($vendorDir . '/willdurand/negotiation/src/Negotiation'), 'Minishlink\\WebPush\\' => array($vendorDir . '/minishlink/web-push/src'), 'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'), diff --git a/wcfsetup/install/files/lib/system/api/composer/autoload_static.php b/wcfsetup/install/files/lib/system/api/composer/autoload_static.php index a6f8b161dc..84cc212576 100644 --- a/wcfsetup/install/files/lib/system/api/composer/autoload_static.php +++ b/wcfsetup/install/files/lib/system/api/composer/autoload_static.php @@ -12,6 +12,7 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', + '9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php', '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', '07d7f1a47144818725fd8d91a907ac57' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php', 'da94ac5d3ca7d2dbab84ce561ce72bfd' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php', @@ -23,13 +24,13 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d '253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php', '5897ea0ac4cccf14d323035e65887801' => __DIR__ . '/..' . '/symfony/polyfill-php82/bootstrap.php', '662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php', - '9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php', '606a39d89246991a373564698c2d8383' => __DIR__ . '/..' . '/symfony/polyfill-php85/bootstrap.php', ); public static $prefixLengthsPsr4 = array ( 'W' => array ( + 'WoltLab\\WebpExif\\' => 17, 'Webmozart\\Assert\\' => 17, ), 'S' => @@ -61,6 +62,7 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d ), 'N' => array ( + 'Nelexa\\Buffer\\' => 14, 'Negotiation\\' => 12, ), 'M' => @@ -102,6 +104,10 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d ); public static $prefixDirsPsr4 = array ( + 'WoltLab\\WebpExif\\' => + array ( + 0 => __DIR__ . '/..' . '/woltlab/webp-exif/src', + ), 'Webmozart\\Assert\\' => array ( 0 => __DIR__ . '/..' . '/webmozart/assert/src', @@ -192,6 +198,10 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d array ( 0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src', ), + 'Nelexa\\Buffer\\' => + array ( + 0 => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer', + ), 'Negotiation\\' => array ( 0 => __DIR__ . '/..' . '/willdurand/negotiation/src/Negotiation', @@ -1495,6 +1505,14 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d 'Negotiation\\Exception\\InvalidMediaType' => __DIR__ . '/..' . '/willdurand/negotiation/src/Negotiation/Exception/InvalidMediaType.php', 'Negotiation\\LanguageNegotiator' => __DIR__ . '/..' . '/willdurand/negotiation/src/Negotiation/LanguageNegotiator.php', 'Negotiation\\Negotiator' => __DIR__ . '/..' . '/willdurand/negotiation/src/Negotiation/Negotiator.php', + 'Nelexa\\Buffer\\Buffer' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/Buffer.php', + 'Nelexa\\Buffer\\BufferException' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/BufferException.php', + 'Nelexa\\Buffer\\Cast' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/Cast.php', + 'Nelexa\\Buffer\\FileBuffer' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/FileBuffer.php', + 'Nelexa\\Buffer\\MemoryResourceBuffer' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/MemoryResourceBuffer.php', + 'Nelexa\\Buffer\\ResourceBuffer' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/ResourceBuffer.php', + 'Nelexa\\Buffer\\StringBuffer' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/StringBuffer.php', + 'Nelexa\\Buffer\\TempBuffer' => __DIR__ . '/..' . '/nelexa/buffer/src/Nelexa/Buffer/TempBuffer.php', 'NoDiscard' => __DIR__ . '/..' . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php', 'Override' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/Override.php', 'ParagonIE\\ConstantTime\\Base32' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Base32.php', @@ -2279,6 +2297,43 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d 'Webmozart\\Assert\\Assert' => __DIR__ . '/..' . '/webmozart/assert/src/Assert.php', 'Webmozart\\Assert\\InvalidArgumentException' => __DIR__ . '/..' . '/webmozart/assert/src/InvalidArgumentException.php', 'Webmozart\\Assert\\Mixin' => __DIR__ . '/..' . '/webmozart/assert/src/Mixin.php', + 'WoltLab\\WebpExif\\ChunkType' => __DIR__ . '/..' . '/woltlab/webp-exif/src/ChunkType.php', + 'WoltLab\\WebpExif\\Chunk\\Alph' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Alph.php', + 'WoltLab\\WebpExif\\Chunk\\Anim' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Anim.php', + 'WoltLab\\WebpExif\\Chunk\\Anmf' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Anmf.php', + 'WoltLab\\WebpExif\\Chunk\\Chunk' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Chunk.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\AnimationFrameWithoutBitstream' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/AnimationFrameWithoutBitstream.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\DimensionsExceedInt32' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/DimensionsExceedInt32.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\EmptyAnimationFrame' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/EmptyAnimationFrame.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\ExpectedKeyFrame' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/ExpectedKeyFrame.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\MissingExifExtension' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/MissingExifExtension.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\MissingMagicByte' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/MissingMagicByte.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\UnknownChunkWithKnownFourCC' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/UnknownChunkWithKnownFourCC.php', + 'WoltLab\\WebpExif\\Chunk\\Exception\\UnsupportedVersion' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exception/UnsupportedVersion.php', + 'WoltLab\\WebpExif\\Chunk\\Exif' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Exif.php', + 'WoltLab\\WebpExif\\Chunk\\Iccp' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Iccp.php', + 'WoltLab\\WebpExif\\Chunk\\UnknownChunk' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/UnknownChunk.php', + 'WoltLab\\WebpExif\\Chunk\\Vp8' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Vp8.php', + 'WoltLab\\WebpExif\\Chunk\\Vp8l' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Vp8l.php', + 'WoltLab\\WebpExif\\Chunk\\Vp8x' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Vp8x.php', + 'WoltLab\\WebpExif\\Chunk\\Xmp' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Chunk/Xmp.php', + 'WoltLab\\WebpExif\\Decoder' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Decoder.php', + 'WoltLab\\WebpExif\\Encoder' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Encoder.php', + 'WoltLab\\WebpExif\\Exception\\ExtraChunksInSimpleFormat' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/ExtraChunksInSimpleFormat.php', + 'WoltLab\\WebpExif\\Exception\\ExtraVp8xChunk' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/ExtraVp8xChunk.php', + 'WoltLab\\WebpExif\\Exception\\FileSizeMismatch' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/FileSizeMismatch.php', + 'WoltLab\\WebpExif\\Exception\\LengthOutOfBounds' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/LengthOutOfBounds.php', + 'WoltLab\\WebpExif\\Exception\\MissingChunks' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/MissingChunks.php', + 'WoltLab\\WebpExif\\Exception\\NotEnoughData' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/NotEnoughData.php', + 'WoltLab\\WebpExif\\Exception\\UnexpectedChunk' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/UnexpectedChunk.php', + 'WoltLab\\WebpExif\\Exception\\UnexpectedEndOfFile' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/UnexpectedEndOfFile.php', + 'WoltLab\\WebpExif\\Exception\\UnrecognizedFileFormat' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/UnrecognizedFileFormat.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xAbsentChunk' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/Vp8xAbsentChunk.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xHeaderLengthMismatch' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/Vp8xHeaderLengthMismatch.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xMissingImageData' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/Vp8xMissingImageData.php', + 'WoltLab\\WebpExif\\Exception\\Vp8xWithoutChunks' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/Vp8xWithoutChunks.php', + 'WoltLab\\WebpExif\\Exception\\WebpExifException' => __DIR__ . '/..' . '/woltlab/webp-exif/src/Exception/WebpExifException.php', + 'WoltLab\\WebpExif\\WebP' => __DIR__ . '/..' . '/woltlab/webp-exif/src/WebP.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/wcfsetup/install/files/lib/system/api/composer/installed.json b/wcfsetup/install/files/lib/system/api/composer/installed.json index 325226b6c6..e186cdd581 100644 --- a/wcfsetup/install/files/lib/system/api/composer/installed.json +++ b/wcfsetup/install/files/lib/system/api/composer/installed.json @@ -1199,6 +1199,69 @@ }, "install-path": "../minishlink/web-push" }, + { + "name": "nelexa/buffer", + "version": "1.3.0", + "version_normalized": "1.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/Ne-Lexa/php-byte-buffer.git", + "reference": "c97bc126d5fbe0c94152fce406a054f681149fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ne-Lexa/php-byte-buffer/zipball/c97bc126d5fbe0c94152fce406a054f681149fac", + "reference": "c97bc126d5fbe0c94152fce406a054f681149fac", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, + "time": "2019-05-25T17:47:34+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Nelexa\\Buffer\\": "src/Nelexa/Buffer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ne-Lexa", + "email": "alexey@nelexa.ru" + } + ], + "description": "Reading And Writing Binary Data (incl. primitive types, ex. byte, ubyte, short, ushort, int, uint, long, float, double). The classes also help with porting the I/O operations of the JAVA code.", + "keywords": [ + "Double", + "binary", + "byte", + "float", + "int", + "io", + "java", + "long", + "pack", + "primitive", + "short", + "ubyte", + "uint", + "unpack", + "ushort" + ], + "support": { + "issues": "https://github.com/Ne-Lexa/php-byte-buffer/issues", + "source": "https://github.com/Ne-Lexa/php-byte-buffer/tree/master" + }, + "install-path": "../nelexa/buffer" + }, { "name": "nikic/fast-route", "version": "2.0.0-beta1", @@ -3402,6 +3465,59 @@ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" }, "install-path": "../willdurand/negotiation" + }, + { + "name": "woltlab/webp-exif", + "version": "v0.1.0", + "version_normalized": "0.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/WoltLab/webp-exif.git", + "reference": "8ec500ac949935c22a624595f99703b40feb4eaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WoltLab/webp-exif/zipball/8ec500ac949935c22a624595f99703b40feb4eaa", + "reference": "8ec500ac949935c22a624595f99703b40feb4eaa", + "shasum": "" + }, + "require": { + "ext-exif": "*", + "nelexa/buffer": "^1.3", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "symfony/polyfill-php84": "^1.31" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/php-code-coverage": "^12.0", + "phpunit/phpunit": "^12.2" + }, + "time": "2025-07-08T10:41:59+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "WoltLab\\WebpExif\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extract and embed EXIF metadata from and to WebP images", + "keywords": [ + "Webp", + "exif" + ], + "support": { + "issues": "https://github.com/WoltLab/webp-exif/issues", + "source": "https://github.com/WoltLab/webp-exif/tree/v0.1.0" + }, + "install-path": "../woltlab/webp-exif" } ], "dev": true, diff --git a/wcfsetup/install/files/lib/system/api/composer/installed.php b/wcfsetup/install/files/lib/system/api/composer/installed.php index d5484f09e2..04a9c7800a 100644 --- a/wcfsetup/install/files/lib/system/api/composer/installed.php +++ b/wcfsetup/install/files/lib/system/api/composer/installed.php @@ -1,9 +1,9 @@ array( 'name' => '__root__', - 'pretty_version' => '6.2.x-dev', - 'version' => '6.2.9999999.9999999-dev', - 'reference' => 'b66a1721eeb9abf3b9b7e52dce7a6ec949c24e6c', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, 'type' => 'project', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -11,9 +11,9 @@ ), 'versions' => array( '__root__' => array( - 'pretty_version' => '6.2.x-dev', - 'version' => '6.2.9999999.9999999-dev', - 'reference' => 'b66a1721eeb9abf3b9b7e52dce7a6ec949c24e6c', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, 'type' => 'project', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -160,6 +160,15 @@ 0 => '^1.0', ), ), + 'nelexa/buffer' => array( + 'pretty_version' => '1.3.0', + 'version' => '1.3.0.0', + 'reference' => 'c97bc126d5fbe0c94152fce406a054f681149fac', + 'type' => 'library', + 'install_path' => __DIR__ . '/../nelexa/buffer', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'nikic/fast-route' => array( 'pretty_version' => '2.0.0-beta1', 'version' => '2.0.0.0-beta1', @@ -522,5 +531,14 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'woltlab/webp-exif' => array( + 'pretty_version' => 'v0.1.0', + 'version' => '0.1.0.0', + 'reference' => '8ec500ac949935c22a624595f99703b40feb4eaa', + 'type' => 'library', + 'install_path' => __DIR__ . '/../woltlab/webp-exif', + 'aliases' => array(), + 'dev_requirement' => false, + ), ), ); diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/.gitignore b/wcfsetup/install/files/lib/system/api/nelexa/buffer/.gitignore new file mode 100644 index 0000000000..2b08048096 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/.gitignore @@ -0,0 +1,5 @@ +/vendor +composer.lock +.DS_Store +/.idea +/.php_cs.cache \ No newline at end of file diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/.travis.yml b/wcfsetup/install/files/lib/system/api/nelexa/buffer/.travis.yml new file mode 100644 index 0000000000..d884b7628a --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/.travis.yml @@ -0,0 +1,21 @@ +language: php +php: + - '5.4' + - '5.5' + - '5.6' + - '7.1' + - '7.2' + - '7.3' + +cache: + directories: + - vendor + - $HOME/.composer/cache + +install: + - travis_retry composer self-update && composer --version + - travis_retry composer install --no-interaction + +script: + - composer validate --no-check-lock + - vendor/bin/phpunit -c phpunit.xml diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/README.md b/wcfsetup/install/files/lib/system/api/nelexa/buffer/README.md new file mode 100644 index 0000000000..256d861c5c --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/README.md @@ -0,0 +1,456 @@ +# `nelexa/buffer` -> Read And Write Binary Data + +[![Packagist Version](https://img.shields.io/packagist/v/nelexa/buffer.svg)](https://packagist.org/packages/nelexa/buffer) +[![Packagist](https://img.shields.io/packagist/dt/nelexa/buffer.svg?color=%23ff007f)](https://packagist.org/packages/nelexa/buffer) +[![Build Status](https://travis-ci.org/Ne-Lexa/php-byte-buffer.svg?branch=master)](https://travis-ci.org/Ne-Lexa/php-byte-buffer +) +[![License](https://img.shields.io/packagist/l/nelexa/buffer.svg)](https://packagist.org/packages/nelexa/buffer) + +This is classes defines methods for **reading and writing** values of all primitive types. Primitive values are translated to (or from) sequences of bytes according to the buffer's current byte order, which may be retrieved and modified via the order methods. The initial order of a byte buffer is always Buffer::BIG_ENDIAN. + +### Requirements +* PHP >= 5.4 (64 bit) + +### Installation +```bash +composer require nelexa/buffer +``` + +### Documentation + +Class `\Nelexa\Buffer` is abstract and base methods for all other buffers. + +Initialize buffer as string. +```php +$buffer = new \Nelexa\StringBuffer(); +// or +$buffer = new \Nelexa\StringBuffer($text); +``` + +Initialize buffer as file. +```php +$buffer = new \Nelexa\FileBuffer($filename); +``` + +Initialize buffer as memory resource (php://memory). +```php +$buffer = new \Nelexa\MemoryReourceBuffer(); +// or +$buffer = new \Nelexa\MemoryReourceBuffer($text); +``` + +Initialize buffer as stream resource. +```php +$fp = fopen('php://temp', 'w+b'); +// or +$buffer = new \Nelexa\ResourceBuffer($fp); +``` + +Set read only buffer +```php +$buffer->setReadOnly(true); +``` + +Checking the possibility of recording in the buffer +```php +$boolValue = $buffer->isReadOnly(); +``` + +Modifies this buffer's byte order, either `Buffer::BIG_ENDIAN` or `Buffer::LITTLE_ENDIAN` +```php +$buffer->setOrder(\Nelexa\Buffer::LITTLE_ENDIAN); +``` + +Get buffer's byte order +```php +$byteOrder = $buffer->order(); +``` + +Get buffer size. +```php +$size = $buffer->size(); +``` + +Set buffer position. +```php +$buffer->setPosition($position); +``` + +Get buffer position. +```php +$position = $buffer->position(); +``` + +Skip bytes. +```php +$buffer->skip($count); + +// example +$buffer->insertString('Test value'); +assert($buffer->position() === 10); +$buffer->skip(-7); +assert($buffer->position() === 3); +$buffer->skip(2); +assert($buffer->position() === 5); +``` + +Skip primitive type size. +```php +$buffer->skipByte(); // skip 1 byte +$buffer->skipShort(); // skip 2 bytes +$buffer->skipInt(); // skip 4 bytes +$buffer->skipLong(); // skip 8 bytes +$buffer->skipFloat(); // skip 4 bytes +$buffer->skipDouble(); // skip 8 bytes +``` + +Rewinds this buffer. The position is set to zero. +```php +$buffer->rewind(); + +// example +$buffer->insertString('Test value'); +assert($buffer->position() === 10); +$buffer->rewind(); +assert($buffer->position() === 0); +assert($buffer->size() === 10); +``` + +Flips this buffer. The limit is set to the current position and then the position is set to zero. +```php +$buffer->flip(); + +// example +$buffer->insertString('Test value'); +assert($buffer->position() === 10); +$buffer->setPosition(5); +$buffer->flip(); +assert($buffer->position() === 0); +assert($buffer->size() === 5); +``` + +Returns the number of elements between the current position and the limit. +```php +$remaining = $buffer->remaining(); + +// example +$buffer->insertString('Test value'); +assert($buffer->position() === 10); +$buffer->setPosition(7); +assert($buffer->remaining() === 10 - 7); +``` + +Tells whether there are any elements between the current position and the limit. True if, and only if, there is at least one element remaining in this buffer +```php +$boolValue = $buffer->hasRemaining(); + +// example +$buffer->insertString('Test value'); +assert($buffer->position() === 10); +assert($buffer->hasRemaining() === false); +$buffer->setPosition(9); +assert($buffer->hasRemaining() === true); +``` + +Close buffer and release resources +```php +$buffer->close(); +``` + +### Read buffer + +Read the entire contents of the buffer into a string without changing the position of the buffer. +```php +$allBufferContent = $buffer->toString(); + +// example +$buffer->insertString('Test value'); +assert($buffer->position() === 10); +$buffer->setPosition(4); +$allBufferContent = $buffer->toString(); +assert($buffer->position() === 4); +assert($allBufferContent === 'Test value'); +``` + +Reads the string at this buffer's current position, and then increments the position. +```php +$content = $buffer->get($length); + +// example +$buffer->insertString('Test value'); +assert($buffer->position() === 10); +$buffer->setPosition(3); +$content = $buffer->get(5); +assert($buffer->position() === 8); +assert($content === 't val'); +``` +##### Read literal types +Method | Type | Values +--------------------------------- | ----------------------- | ----------------- +`$buffer->getBoolean` | boolean | `true` or `false` +`$buffer->getByte()` | byte | -128 ... 127 +`$buffer->getUnsignedByte()` | unsigned byte (ubyte) | 0 ... 255 +`$buffer->getShort()` | short (2 bytes) | -32768 ... 32767 +`$buffer->getUnsignedShort()` | unsigned short (ushort) | 0 ... 65535 +`$buffer->getInt()` | int (4 bytes) | -2147483648 ... 2147483647 +`$buffer->getUnsignedInt()` | unsigned int (uint) | 0 ... 4294967296 +`$buffer->getLong()` | long (8 bytes) | -9223372036854775808 ... 9223372036854775807 +`$buffer->getFloat()` | float (4 bytes) | single-precision 32-bit IEEE 754 floating point number +`$buffer->getDouble()` | double (5 bytes) | double-precision 64-bit IEEE 754 floating point number +`$buffer->getArrayBytes($length)` | byte[] | `array` +`$buffer->getString($length)` | string (length bytes) | `string` +`$buffer->getUTF()` | string | `string` +`$buffer->getUTF16($length)` | string (length * 2) | `string` + + +### Write to buffer + +#### Insert bytes to buffer +Insert string (byte[]) or Buffer to buffer. +```php +$buffer->insert('content'); +// or +$buffer->insert(new StringBuffer('Other buffer')); + +// example +assert($buffer->position() === 0); +assert($buffer->size() === 0); +$buffer->insert('Test value'); +assert($buffer->position() === 10); +assert($buffer->size() === 10); +$buffer->setPosition(4); +$buffer->insert('ed'); +assert($buffer->position() === 6); +assert($buffer->size() === 12); +assert($buffer->toString() === 'Tested value'); +``` +##### Insert primitive types +Insert boolean value `false` or `true`. Change size and position by +1. +```php +$buffer->insertBoolean($boolValue); +``` +Insert byte (-128 >= byte <= 127). Change size and position by +1. +```php +$buffer->insertByte($byteValue); +``` +Insert short value (-32768 >= short <= 32767). Change size and position by +2. +```php +$buffer->insertShort($shortValue); +``` +Insert integer value (-2147483648 >= int <= 2147483647). Change size and position by +4. +```php +$buffer->insertInt($intValue); +``` +Insert long value (-9223372036854775808 >= long <= 9223372036854775807). Change position +8. +```php +$buffer->insertLong($longValue); +``` +Insert float value (single-precision 32-bit IEEE 754 floating point number). Change position +4. +```php +$buffer->insertFloat($floatValue); +``` +Insert double value (double-precision 64-bit IEEE 754 floating point number). Change position +8. +```php +$buffer->insertDouble($doubleValue); +``` +Insert array bytes. Change size and position by +(size array). +```php +$buffer->insertArrayBytes($bytes); +``` +Insert string value. Change size and position by +(length string). +```php +$buffer->insertString($string); +``` +Insert UTF-8 string with encoding first two bytes as length string. Change size and position by +(2 + length string). + +Analog java [java.io.DataOutputStream#writeUTF(String str)](https://docs.oracle.com/javase/8/docs/api/java/io/DataOutputStream.html#writeUTF-java.lang.String-) +```php +$buffer->insertUTF($string); +``` +Insert string with UTF-16 encoding. Change size and position by +(2 * length string). +```php +$buffer->insertUTF16($string); +``` +#### Put bytes to buffer +Put string (byte[]) or Buffer to buffer and overwrite old value. +```php +$buffer->put('content'); +// or +$buffer->put(new StringBuffer('Other buffer')); + +// example +assert($buffer->position() === 0); +assert($buffer->size() === 0); +$buffer->insert('Test value'); +assert($buffer->position() === 10); +assert($buffer->size() === 10); +$buffer->setPosition(4); +$buffer->put('ed'); +assert($buffer->position() === 6); +assert($buffer->size() === 10); +assert($buffer->toString() === 'Testedalue'); +``` +##### Put primitive types +Put boolean value `false` or `true`. Change position by +1. +```php +$buffer->putBoolean($boolValue); +``` +Put byte (-128 >= byte <= 127). Change position by +1. +```php +$buffer->putByte($byteValue); +``` +Put short value (-32768 >= short <= 32767). Change position by +2. +```php +$buffer->putShort($shortValue); +``` +Put integer value (-2147483648 >= int <= 2147483647). Change position by +4. +```php +$buffer->putInt($intValue); +``` +Put long value (-9223372036854775808 >= long <= 9223372036854775807). Change position by +8. +```php +$buffer->putLong($longValue); +``` +Put float value (single-precision 32-bit IEEE 754 floating point number). Change position +4. +```php +$buffer->putFloat($floatValue); +``` +Put double value (double-precision 64-bit IEEE 754 floating point number). Change position +8. +```php +$buffer->putDouble($doubleValue); +``` +Put array bytes. Change position by +(size array). +```php +$buffer->putArrayBytes($bytes); +``` +Insert string value. Change position by +(length string). +```php +$buffer->putString($string); +``` +Put UTF-8 string with encoding first two bytes as length string. Change position by +(2 + length string). + +Analog java [java.io.DataOutputStream#writeUTF(String str)](https://docs.oracle.com/javase/8/docs/api/java/io/DataOutputStream.html#writeUTF-java.lang.String-) +```php +$buffer->puttUTF($string); +``` +Put string with UTF-16 encoding. Change position by +(2 * length string). +```php +$buffer->putUTF16($string); +``` +#### Replace bytes by buffer +Replace following a certain number of bytes by string or another Buffer. +```php +$buffer->replace('content', $length); +// or +$buffer->insert(new StringBuffer('Other buffer'), $length); + +// example +assert($buffer->position() === 0); +assert($buffer->size() === 0); +$buffer->insert('Test value'); +assert($buffer->position() === 10); +assert($buffer->size() === 10); +$buffer->setPosition(4); +$buffer->replace('ed', 4); // remove 4 next bytes and insert 2 bytes +assert($buffer->position() === 6); +assert($buffer->size() === 8); +assert($buffer->toString() === 'Testedlue'); +``` +##### Replace by primitive types +Replace by boolean value `false` or `true`. Change size by (-$length + 1) and position +1. +```php +$buffer->replaceBoolean($boolValue, $length); +``` +Replace by byte (-128 >= byte <= 127). Change size by (-$length + 1) and position +1. +```php +$buffer->replaceByte($byteValue, $length); +``` +Replace by short value (-32768 >= short <= 32767). Change size by (-$length + 2) and position +2. +```php +$buffer->replaceShort($shortValue, $length); +``` +Replace by integer value (-2147483648 >= int <= 2147483647). Change size by (-$length + 4) and position +4. +```php +$buffer->replaceInt($intValue, $length); +``` +Replace by long value (-9223372036854775808 >= long <= 9223372036854775807). Change size by (-$length + 8) and position +8. +```php +$buffer->replaceLong($longValue, $length); +``` +Replace by float value (single-precision 32-bit IEEE 754 floating point number). Change size by (-$length + 4) and position +4. +```php +$buffer->replaceFloat($floatValue, $length); +``` +Replace by double value (double-precision 64-bit IEEE 754 floating point number). Change size by (-$length + 8) and position +8. +```php +$buffer->replaceDouble($doubleValue, $length); +``` +Replace by array bytes. Change size by (-$length + size array) and position +(size array). +```php +$buffer->replaceArrayBytes($bytes, $length); +``` +Replace by string value. Change size by (-$length + length string) and position +(length string). +```php +$buffer->replaceString($string, $length); +``` +Replace by UTF-8 string with encoding first two bytes as length string. Change size by (-$length + 2 + length string) and position +(2 + length string). + +Analog java [java.io.DataOutputStream#writeUTF(String str)](https://docs.oracle.com/javase/8/docs/api/java/io/DataOutputStream.html#writeUTF-java.lang.String-) +```php +$buffer->replaceUTF($string, $length); +``` +Replace by string with UTF-16 encoding. Change size by (-$length + 2 * length string) and position +(2 * length string). +```php +$buffer->replaceUTF16($string, $length); +``` + +### Remove bytes by buffer +Remove a certain number of bytes. Change size by -$length. +```php +$buffer->remove($length); + +// example +assert($buffer->position() === 0); +assert($buffer->size() === 0); +$buffer->insert('Test value'); +assert($buffer->position() === 10); +assert($buffer->size() === 10); +$buffer->setPosition(4); +$buffer->remove(3); // remove 3 next bytes +assert($buffer->position() === 4); +assert($buffer->size() === 7); +assert($buffer->toString() === 'Testlue'); +``` +Remove all bytes. Truncate buffer. +```php +$buffer->truncate($size = 0); + +// example +assert($buffer->position() === 0); +assert($buffer->size() === 0); +$buffer->insert('Test value'); +assert($buffer->position() === 10); +assert($buffer->size() === 10); +$buffer->truncate(0); +assert($buffer->position() === 0); +assert($buffer->size() === 0); +``` + +### Fluent interface +```php +// example +($buffer = new StringBuffer()) + ->insertByte(1) + ->insertBoolean(true) + ->insertShort(5551) + ->skip(-2) + ->insertUTF("Hello, World") + ->truncate() + ->insertString(str_rot13('Hello World')) + ->setPosition(7) + ->flip(); + +assert($this->buffer->size() === 7); +assert($this->buffer->position() === 0); +assert($this->buffer->toString() === str_rot13('Hello W')); +``` diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/composer.json b/wcfsetup/install/files/lib/system/api/nelexa/buffer/composer.json new file mode 100644 index 0000000000..037da70065 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/composer.json @@ -0,0 +1,45 @@ +{ + "name": "nelexa/buffer", + "description": "Reading And Writing Binary Data (incl. primitive types, ex. byte, ubyte, short, ushort, int, uint, long, float, double). The classes also help with porting the I/O operations of the JAVA code.", + "type": "library", + "keywords": [ + "binary", + "primitive", + "byte", + "ubyte", + "short", + "ushort", + "int", + "uint", + "long", + "float", + "double", + "io", + "java", + "pack", + "unpack" + ], + "license": "MIT", + "authors": [ + { + "name": "Ne-Lexa", + "email": "alexey@nelexa.ru" + } + ], + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, + "autoload": { + "psr-4": { + "Nelexa\\Buffer\\": "src/Nelexa/Buffer" + } + }, + "autoload-dev": { + "psr-4": { + "Nelexa\\Buffer\\": "tests/Nelexa/Buffer" + } + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/phpunit.xml b/wcfsetup/install/files/lib/system/api/nelexa/buffer/phpunit.xml new file mode 100644 index 0000000000..65634f9e23 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/phpunit.xml @@ -0,0 +1,22 @@ + + + + + + tests/ + + + + + src/ + + + diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/Buffer.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/Buffer.php new file mode 100644 index 0000000000..ec92b2a4b2 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/Buffer.php @@ -0,0 +1,1170 @@ +position; + } + + /** + * Rewinds this buffer. The position is set to zero. + * + * Invoke this method before a sequence of channel-write or get + * operations, assuming that the limit has already been set + * appropriately. + * + * For example: + * + * $buf->writeString("Hello"); // Write remaining data + * $buf->rewind(); // Rewind buffer + * $buf->get(5); // get 5 bytes (Hello) + * + * @return Buffer + * @throws BufferException + */ + final public function rewind() + { + return $this->setPosition(0); + } + + /** + * Set buffer position. + * + * @param int $position + * @return Buffer + * @throws BufferException + */ + public function setPosition($position) + { + $position = (int)$position; + if ($position > $this->limit) { + throw new BufferException('Set position ' . $position . ' invalid. Exceeded limit ' . $this->limit); + } + $this->position = $position; + return $this; + } + + /** + * Flips this buffer. The limit is set to the current position and then + * the position is set to zero. + * + * After a sequence of channel-read or put operations, invoke + * this method to prepare for a sequence of channel-write or relative + * get operations. + * + * @return Buffer + */ + abstract public function flip(); + + /** + * Returns the number of elements between the current position and the + * limit. + * + * @return int The number of elements remaining in this buffer + */ + public function remaining() + { + return $this->limit - $this->position; + } + + /** + * Tells whether there are any elements between the current position and + * the limit. + * + * @return boolean true if, and only if, there is at least one element remaining in this buffer + */ + public function hasRemaining() + { + return $this->position < $this->limit; + } + + /** + * Modifies this buffer's byte order. + * + * @param string $order The new byte order, either Buffer::BIG_ENDIAN or Buffer::LITTLE_ENDIAN + * @return Buffer + * @see Buffer::BIG_ENDIAN + * @see Buffer::LITTLE_ENDIAN + * + */ + final public function setOrder($order) + { + $this->orderLittleEndian = $order === self::LITTLE_ENDIAN; + return $this; + } + + /** + * Set read only buffer. + * + * @param boolean $isReadOnly + * @return Buffer + */ + public function setReadOnly($isReadOnly) + { + $this->isReadOnly = $isReadOnly; + return $this; + } + + /** + * Skip 1 byte + * + * @return Buffer + * @throws BufferException + */ + public function skipByte() + { + return $this->skip(1); + } + + /** + * Skip number bytes. + * + * @param int $n The number of bytes to be skipped. The value may be negative. + * @return Buffer + * @throws BufferException + */ + public function skip($n) + { + return $this->setPosition($this->position + $n); + } + + /** + * Skip short (2 bytes) + * + * @return Buffer + * @throws BufferException + */ + public function skipShort() + { + return $this->skip(2); + } + + /** + * Skip int (4 bytes) + * + * @return Buffer + * @throws BufferException + */ + public function skipInt() + { + return $this->skip(4); + } + + /** + * Skip long (8 bytes) + * + * @return Buffer + * @throws BufferException + */ + public function skipLong() + { + return $this->skip(8); + } + + /** + * Skip float (4 bytes) + * + * @return $this + * @throws BufferException + */ + public function skipFloat() + { + return $this->skip(4); + } + + /** + * Skip double (8 bytes) + * + * @return $this + * @throws BufferException + */ + public function skipDouble() + { + return $this->skip(8); + } + + /** + * Reads one input byte and returns true if that byte is nonzero, + * false if that byte is zero. + * + * @return bool the boolean value read. + * @throws BufferException + */ + public function getBoolean() + { + return (bool)$this->getUnsignedByte(); + } + + /** + * Reads one input byte, zero-extends + * it to type int, and returns + * the result, which is therefore in the range + * 0 through 255. + * + * @return int the unsigned 8-bit value read. + * @throws BufferException + */ + public function getUnsignedByte() + { + return unpack('C', $this->get(1))[1]; + } + + /** + * Relative get method. + * Reads the string at this buffer's current position, and then increments the position. + * + * @param int $length + * @return string The strings at the buffer's current position + * @throws BufferException + */ + abstract protected function get($length); + + /** + * Reads and returns one input byte. + * The byte is treated as a signed value in + * the range -128 through 127, inclusive. + * + * @return int the 8-bit value read. + * @throws BufferException + */ + public function getByte() + { + return Cast::toByte($this->getUnsignedByte()); + } + + /** + * Reads two input bytes and returns + * a short value in the range -32768 through 32767. + * + * @return int the 16-bit value read. + * @throws BufferException + */ + public function getShort() + { + return Cast::toShort($this->getUnsignedShort()); + } + + /** + * Reads two input bytes and returns + * an int value in the range 0 through 65535. + * + * @return int the unsigned 16-bit value read. + * @throws BufferException + */ + public function getUnsignedShort() + { + return unpack($this->orderLittleEndian ? 'v' : 'n', $this->get(2))[1]; + } + + /** + * Reads four input bytes and returns an unsigned short value + * in the range -2147483648 through 2147483647. + * + * @return int the int value read. + * @throws BufferException + */ + public function getInt() + { + return Cast::toInt($this->getUnsignedInt()); + } + + /** + * Reads four input bytes and returns an unsigned int value + * in the range 0 through 4294967296. + * + * @return int the unsigned int value read. + * @throws BufferException + */ + public function getUnsignedInt() + { + return unpack($this->orderLittleEndian ? 'V' : 'N', $this->get(4))[1]; + } + + /** + * Reads eight input bytes and returns a long value + * in the range -9223372036854775808 through 9223372036854775807. + * + * @return string|int the long value read. + * @throws BufferException + */ + public function getLong() + { + $data = $this->get(8); + if (PHP_VERSION_ID >= 50603) { + return unpack($this->orderLittleEndian ? 'P' : 'J', $data)[1]; + } + + if ($this->orderLittleEndian) { + $unpack = unpack('Va/Vb', $data); + return $unpack['a'] + ($unpack['b'] << 32); + } + + $unpack = unpack('Na/Nb', $data); + return ($unpack['a'] << 32) | $unpack['b']; + } + + /** + * Reads four input bytes and returns a float value + * + * @return float the float value read. + * @throws BufferException + */ + public function getFloat() + { + self::checkPhpSupport(); + return unpack($this->orderLittleEndian ? 'g' : 'G', $this->get(4))[1]; + } + + /** + * Reads four input bytes and returns a double value + * + * @return double the double value read. + * @throws BufferException + */ + public function getDouble() + { + self::checkPhpSupport(); + return unpack($this->orderLittleEndian ? 'e' : 'E', $this->get(8))[1]; + } + + /** + * Reads $length bytes from an input stream. + * + * @param $length int + * @return int[] + * @throws BufferException + */ + public function getArrayBytes($length) + { + if ($length > 0) { + return array_values( + unpack('c*', $this->get($length)) + ); + } + return []; + } + + /** + * Reads in a string that has been encoded using + * a modified UTF-8 format. + * + * First, two bytes are read and used to + * construct an unsigned 16-bit integer in + * exactly the manner of the Buffer::readUnsignedShort() + * method. This integer value is called the UTF length + * and specifies the number of additional bytes to be read. + * + * Analog java @see java.io.DataOutputStream#readUTF() + * + * @return string + * @throws BufferException + */ + public function getUTF() + { + $size = $this->getUnsignedShort(); + if ($size > 0) { + return $this->getString($size); + } + return ''; + } + + /** + * Reads $length input bytes and returns a string value. + * + * @param $length int + * @return string + * @throws BufferException + */ + public function getString($length) + { + if ($length > 0) { + return $this->get($length); + } + return ''; + } + + /** + * Reads $length * 2 input bytes and returns a string value. + * + * @param $length int + * @return string + * @throws BufferException + * @deprecated + */ + public function getUTF16($length) + { + if ($length > 0) { + return implode('', array_map('chr', array_values(unpack('S*', $this->get($length << 1))))); + } + return ''; + } + + /** + * Insert boolean value + * + * @param $bool + * @return Buffer + * @throws BufferException + */ + public function insertBoolean($bool) + { + return $this->insert($this->writeBoolean($bool)); + } + + /** + * Insert Buffer or string. + * + * @param Buffer|string $buffer + * @return Buffer + * @throws BufferException + */ + abstract public function insert($buffer); + + /** + * @param bool $bool + * @return string + * @throws BufferException + */ + protected function writeBoolean($bool) + { + if ($bool === null) { + throw new BufferException('null boolean'); + } + return pack('c', $bool ? 1 : 0); + } + + /** + * Insert byte (-128 >= byte <= 127) + * + * @param int|string $byte + * @return Buffer + * @throws BufferException + */ + public function insertByte($byte) + { + return $this->insert($this->writeByte($byte)); + } + + /** + * @param int|string $byte + * @return string + * @throws BufferException + */ + protected function writeByte($byte) + { + if ($byte === null) { + throw new BufferException('null byte'); + } + return pack('c', $byte); + } + + /** + * Insert short value (-32768 >= short <= 32767) + * + * @param int|string $v + * @return Buffer + * @throws BufferException + */ + public function insertShort($v) + { + return $this->insert($this->writeShort($v)); + } + + /** + * @param int|string $v + * @return string + * @throws BufferException + */ + protected function writeShort($v) + { + if ($v === null) { + throw new BufferException('null short'); + } + return pack($this->orderLittleEndian ? 'v' : 'n', $v); + } + + /** + * Insert integer value (-2147483648 >= int <= 2147483647) + * + * @param int|string $v + * @return Buffer + * @throws BufferException + */ + public function insertInt($v) + { + return $this->insert($this->writeInt($v)); + } + + /** + * @param int|string $v + * @return string + * @throws BufferException + */ + protected function writeInt($v) + { + if ($v === null) { + throw new BufferException('null int'); + } + return pack($this->orderLittleEndian ? 'V' : 'N', $v); + } + + /** + * Insert long value (-9223372036854775808 >= long <= 9223372036854775807) + * + * @param int|string $v + * @return Buffer + * @throws BufferException + */ + public function insertLong($v) + { + return $this->insert($this->writeLong($v)); + } + + /** + * @param int|string $v + * @return string + * @throws BufferException + */ + protected function writeLong($v) + { + if ($v === null) { + throw new BufferException('null long'); + } + if (PHP_VERSION_ID >= 50603) { + return pack($this->orderLittleEndian ? 'P' : 'J', $v); + } + + $left = 0xffffffff00000000; + $right = 0x00000000ffffffff; + if ($this->orderLittleEndian) { + $r = ($v & $left) >> 32; + $l = $v & $right; + return pack('VV', $l, $r); + } + + $l = ($v & $left) >> 32; + $r = $v & $right; + return pack('NN', $l, $r); + } + + /** + * Insert float value + * + * @param float $v + * @return Buffer + * @throws BufferException + */ + public function insertFloat($v) + { + return $this->insert($this->writeFloat($v)); + } + + /** + * @param float $v + * @return string + * @throws BufferException + */ + protected function writeFloat($v) + { + self::checkPhpSupport(); + if ($v === null) { + throw new BufferException('null float'); + } + return pack($this->orderLittleEndian ? 'g' : 'G', $v); + } + + /** + * Insert double value + * + * @param double $v + * @return Buffer + * @throws BufferException + */ + public function insertDouble($v) + { + return $this->insert($this->writeDouble($v)); + } + + /** + * @param double $v + * @return string + * @throws BufferException + */ + protected function writeDouble($v) + { + self::checkPhpSupport(); + if ($v === null) { + throw new BufferException('null double'); + } + return pack($this->orderLittleEndian ? 'e' : 'E', $v); + } + + /** + * Insert string + * + * @param string $string + * @return Buffer + * @throws BufferException + */ + public function insertString($string) + { + return $this->insert($this->writeString($string)); + } + + /** + * @param string $string + * @return string + */ + protected function writeString($string) + { + return $string; + } + + /** + * Insert array bytes + * + * @param array $bytes + * @return Buffer + * @throws BufferException + */ + public function insertArrayBytes(array $bytes) + { + return $this->insert($this->writeArrayBytes($bytes)); + } + + /** + * @param array $bytes + * @return string + */ + protected function writeArrayBytes(array $bytes) + { + return call_user_func_array('pack', array_merge(['c*'], $bytes)); + } + + /** + * Writes a string to the underlying output stream using + * modified UTF-8 encoding in a machine-independent manner. + * + * @param string $string + * @return Buffer + * @throws BufferException + * @see Buffer::writeUTF() + * + */ + public function insertUTF($string) + { + return $this->insert($this->writeUTF($string)); + } + + /** + * Writes a string to the underlying output stream using + * modified UTF-8 encoding in a machine-independent manner. + * + * First, two bytes are written to the output stream as if by the + * Buffer::writeShort() method giving the number of bytes to + * follow. This value is the number of bytes actually written out, + * not the length of the string. + * + * Analog java @see java.io.DataOutputStream#writeUTF() + * + * @param string $str + * @return string + * @throws BufferException + */ + protected function writeUTF($str) + { + if ($str === null) { + throw new BufferException('$str is null'); + } + $bytes = unpack('c*', $str); + $length = count($bytes); + if ($length > 65535) { + throw new BufferException('Encoded string too long: ' . $length . ' bytes'); + } + array_unshift($bytes, 'c*'); + return $this->writeShort($length) . call_user_func_array('pack', $bytes); + } + + /** + * Insert UTF16 string + * + * @param string $string + * @return Buffer + * @throws BufferException + * @deprecated + */ + public function insertUTF16($string) + { + return $this->insert($this->writeUTF16($string)); + } + + /** + * @param string $string + * @return string + * @throws BufferException + * @deprecated + */ + protected function writeUTF16($string) + { + if ($string === null) { + throw new BufferException('$string is null'); + } + $args = array_map('ord', str_split($string)); + array_unshift($args, 'S*'); + return call_user_func_array('pack', $args); + } + + /** + * Put boolean value + * + * @param $bool + * @return Buffer + * @throws BufferException + */ + public function putBoolean($bool) + { + return $this->put($this->writeBoolean($bool)); + } + + /** + * Relative put method (optional operation). + * + * Writes the given string into this buffer at the current + * position, and then increments the position. + * + * @param Buffer|string $buffer + * @return Buffer + * @throws BufferException + */ + abstract public function put($buffer); + + /** + * Put byte (-128 >= byte <= 127) + * + * @param int|string $byte + * @return Buffer + * @throws BufferException + */ + public function putByte($byte) + { + return $this->put($this->writeByte($byte)); + } + + /** + * Put short value (-32768 >= short <= 32767) + * + * @param int|string $v + * @return Buffer + * @throws BufferException + */ + public function putShort($v) + { + return $this->put($this->writeShort($v)); + } + + /** + * Put integer value (-2147483648 >= int <= 2147483647) + * + * @param int|string $v + * @return Buffer + * @throws BufferException + */ + public function putInt($v) + { + return $this->put($this->writeInt($v)); + } + + /** + * Put long value (-9223372036854775808 >= long <= 9223372036854775807) + * + * @param int|string $v + * @return Buffer + * @throws BufferException + */ + public function putLong($v) + { + return $this->put($this->writeLong($v)); + } + + /** + * Put float value + * + * @param float $v + * @return Buffer + * @throws BufferException + */ + public function putFloat($v) + { + return $this->put($this->writeFloat($v)); + } + + /** + * Put double value + * + * @param double $v + * @return Buffer + * @throws BufferException + */ + public function putDouble($v) + { + return $this->put($this->writeDouble($v)); + } + + /** + * Put string + * + * @param string $string + * @return Buffer + * @throws BufferException + */ + public function putString($string) + { + return $this->put($this->writeString($string)); + } + + /** + * Put array bytes + * + * @param array $bytes + * @return Buffer + * @throws BufferException + */ + public function putArrayBytes(array $bytes) + { + return $this->put($this->writeArrayBytes($bytes)); + } + + /** + * Put UTF string (Format - java DataOutputStream.writeUTF) + * + * @param string $str + * @return Buffer + * @throws BufferException + */ + public function putUTF($str) + { + return $this->put($this->writeUTF($str)); + } + + /** + * Put UTF16 string + * + * @param string $str + * @return Buffer + * @throws BufferException + * @deprecated + */ + public function putUTF16($str) + { + return $this->put($this->writeUTF16($str)); + } + + /** + * Replace by boolean value + * + * @param bool $bool + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceBoolean($bool, $length) + { + return $this->replace($this->writeBoolean($bool), $length); + } + + /** + * Replace $length bytes in a string or Buffer. + * + * @param Buffer|string $buffer + * @param int $length remove length bytes + * @return Buffer + * @throws BufferException + */ + abstract public function replace($buffer, $length); + + /** + * Replace by byte (-128 >= byte <= 127) + * + * @param int|string $byte + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceByte($byte, $length) + { + return $this->replace($this->writeByte($byte), $length); + } + + /** + * Replace short value (-32768 >= short <= 32767) + * + * @param int|string $v + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceShort($v, $length) + { + return $this->replace($this->writeShort($v), $length); + } + + /** + * Replace integer value (-2147483648 >= int <= 2147483647) + * + * @param int|string $v + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceInt($v, $length) + { + return $this->replace($this->writeInt($v), $length); + } + + /** + * Replace long value (-9223372036854775808 >= long <= 9223372036854775807) + * + * @param int|string $v + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceLong($v, $length) + { + return $this->replace($this->writeLong($v), $length); + } + + /** + * Replace float value + * + * @param float $v + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceFloat($v, $length) + { + return $this->replace($this->writeFloat($v), $length); + } + + /** + * Replace double value + * + * @param double $v + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceDouble($v, $length) + { + return $this->replace($this->writeDouble($v), $length); + } + + /** + * Replace string + * + * @param string $string + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceString($string, $length) + { + return $this->replace($this->writeString($string), $length); + } + + /** + * Insert array bytes + * + * @param array $bytes + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceArrayBytes(array $bytes, $length) + { + return $this->replace($this->writeArrayBytes($bytes), $length); + } + + /** + * Replace UTF string (Format - java DataOutStream.writeUTF) + * + * @param string $str + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function replaceUTF($str, $length) + { + return $this->replace($this->writeUTF($str), $length); + } + + /** + * Replace UTF16 string + * + * @param string $str + * @param int $length + * @return Buffer + * @throws BufferException + * @deprecated + */ + public function replaceUTF16($str, $length) + { + return $this->replace($this->writeUTF16($str), $length); + } + + /** + * Remove a certain number of bytes. + * + * @param int $length + * @return Buffer + * @throws BufferException + */ + abstract public function remove($length); + + /** + * Truncate data + * + * @param int $size + * @return Buffer + */ + abstract public function truncate($size = 0); + + /** + * Close buffer. If this buffer resource that closes the stream. + */ + abstract public function close(); + + /** + * @return string + */ + abstract public function toString(); + + /** + * @return string + */ + public function __toString() + { + return get_called_class() . '{' . + 'position=' . $this->position . + ', limit=' . $this->size() . + ', order=' . $this->order() . + ', readOnly=' . ($this->isReadOnly() ? 'true' : 'false') . + '}'; + } + + /** + * Returns this buffer's limit. + * + * @return int The limit of this buffer + */ + final public function size() + { + return $this->limit; + } + + /** + * Retrieves this buffer's byte order. + * + * The byte order is used when reading or writing multibyte values, and + * when creating buffers that are views of this byte buffer. The order of + * a newly-created byte buffer is always Buffer::BIG_ENDIAN + * + * @return string This buffer's byte order + * @see Buffer::LITTLE_ENDIAN + * + * @see Buffer::BIG_ENDIAN + */ + final public function order() + { + return $this->orderLittleEndian ? self::LITTLE_ENDIAN : self::BIG_ENDIAN; + } + + /** + * Is read only buffer. + * + * @return boolean + */ + final public function isReadOnly() + { + return $this->isReadOnly; + } + + /** + * Sets this buffer's limit. If the position is larger than the new limit + * then it is set to the new limit. + * + * @param $newLimit int + * @return Buffer + * @throws BufferException + */ + protected function newLimit($newLimit) + { + if ($newLimit < 0) { + throw new BufferException('New Limit < 0'); + } + $this->limit = $newLimit; + if ($this->position > $this->limit) { + $this->position = $this->limit; + } + return $this; + } + + /** + * Buffer's byte order is Buffer::LITTLE_ENDIAN + * + * @return bool + * @see Buffer::LITTLE_ENDIAN + * + * @see Buffer::BIG_ENDIAN + */ + final protected function isOrderLE() + { + return $this->orderLittleEndian; + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/BufferException.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/BufferException.php new file mode 100644 index 0000000000..1c4cfd683e --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/BufferException.php @@ -0,0 +1,13 @@ += byte <= 127) + * + * @param int $i + * @return int + */ + public static function toByte($i) + { + $i &= 0xff; + if ($i < 128) { + return $i; + } + return $i - 256; + } + + /** + * Cast to unsigned byte (0 >= short <= 255) + * + * @param int $i + * @return int + */ + public static function toUnsignedByte($i) + { + return $i & 0xff; + } + + /** + * Cast to short (-32768 >= short <= 32767) + * + * @param int $i + * @return int + */ + public static function toShort($i) + { + $i &= 0xffff; + if ($i < 32768) { + return $i; + } + return $i - 65536; + } + + /** + * Cast to unsigned short (0 >= int <= 65535) + * + * @param int $i + * @return int + */ + public static function toUnsignedShort($i) + { + return $i & 0xffff; + } + + /** + * Cast to int (-2147483648 >= int <= 2147483647) + * + * @param int $i + * @return int + */ + public static function toInt($i) + { + if (PHP_INT_SIZE === 8) { + $i &= 0xffffffff; + if ($i < 2147483648) { + return $i; + } + return $i - 4294967296; + } + return $i; + } + + /** + * Cast to unsigned int (0 >= long <= 4294967296) + * + * @param int $i + * @return int + */ + public static function toUnsignedInt($i) + { + return $i & 0xffffffff; + } + + /** + * Cast to long (-9223372036854775808 >= long <= 9223372036854775807) + * + * @param int $i + * @return int + */ + public static function toLong($i) + { + $i = (int)$i; + if ($i > static::LONG_MAX_VALUE) { + throw new \RuntimeException('Invalid long value'); + } + + if ($i < static::LONG_MIN_VALUE) { + throw new \RuntimeException('Invalid long value'); + } + return $i; + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/FileBuffer.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/FileBuffer.php new file mode 100644 index 0000000000..c62e46d854 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/FileBuffer.php @@ -0,0 +1,58 @@ +writable = is_writable(dirname($file)); + parent::setReadOnly(!$this->writable); + + $mode = !$this->writable ? 'rb' : (file_exists($file) ? 'r+' : 'w+') . 'b'; + + if (($fp = fopen($file, $mode)) === false) { + throw new BufferException("file '$file' can not open."); + } + parent::__construct($fp); + } + + /** + * @param bool $isReadOnly + * @return Buffer + * @throws BufferException + */ + public function setReadOnly($isReadOnly) + { + if (!$this->writable && !$isReadOnly) { + throw new BufferException('You can not set the recording flag.' . + 'The directory containing the file is not available for recording.'); + } + return parent::setReadOnly($isReadOnly); + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/MemoryResourceBuffer.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/MemoryResourceBuffer.php new file mode 100644 index 0000000000..ebea717f43 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/MemoryResourceBuffer.php @@ -0,0 +1,34 @@ +setResource($resource); + } + + /** + * @param resource $resource + * @throws BufferException + */ + protected function setResource($resource) + { + if ($resource === null) { + throw new BufferException('Resource null'); + } + if (!is_resource($resource)) { + throw new BufferException('invalid type $resource - is not resource'); + } + if (!stream_is_local($resource)) { + throw new BufferException('invalid argument $resource - read only resource is not local'); + } + $meta = stream_get_meta_data($resource); + if (!$meta['seekable']) { + throw new BufferException('$resource cannot seekable stream.'); + } + $stats = fstat($resource); + if (isset($stats['size'])) { + $this->newLimit($stats['size']); + } + $this->resource = $resource; + $this->setPosition(0); + } + + /** + * @param int $position + * @return Buffer + * @throws BufferException + */ + public function setPosition($position) + { + if (!is_numeric($position)) { + throw new BufferException('position ' . $position . ' is not numeric'); + } + if (fseek($this->resource, $position) === 0) { + return parent::setPosition($position); + } + + throw new BufferException('set position ' . $position . ' failure'); + } + + /** + * @return string + * @throws BufferException + */ + final public function toString() + { + $position = $this->position; + $this->rewind(); + $content = stream_get_contents($this->resource); + $this->setPosition($position); + return $content; + } + + /** + * Flips this buffer. The limit is set to the current position and then + * the position is set to zero. + * + * After a sequence of channel-read or put operations, invoke + * this method to prepare for a sequence of channel-write or relative + * get operations. + * + * @return Buffer + * @throws BufferException + */ + public function flip() + { + $this->newLimit($this->position); + ftruncate($this->resource, $this->size()); + $this->setPosition(0); + return $this; + } + + /** + * @param Buffer|string $buffer + * @return Buffer + * @throws BufferException + */ + public function insert($buffer) + { + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($buffer === null) { + throw new BufferException('null buffer'); + } + if ($buffer instanceof Buffer) { + $buffer = $buffer->toString(); + } + $lengthBuffer = strlen($buffer); + if ($this->hasRemaining()) { + $buffer .= stream_get_contents($this->resource); + $this->setPosition($this->position); + } + $length = strlen($buffer); + + $lengthWrite = fwrite($this->resource, $buffer, $length); + if ($lengthWrite === false || $lengthWrite !== $length) { + throw new BufferException('Not write all bytes. Length: ' . $length . ', write length: ' . $lengthWrite); + } + $this->newLimit($this->size() + $lengthBuffer); + $this->position += $lengthBuffer; + return $this; + } + + /** + * Relative put method (optional operation). + * + * Writes the given string into this buffer at the current + * position, and then increments the position. + * + * @param Buffer|string $buffer + * @return Buffer + * @throws BufferException + */ + public function put($buffer) + { + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($buffer === null) { + throw new BufferException('null buffer'); + } + $length = null; + if ($buffer instanceof Buffer) { + $length = $buffer->size(); + $buffer = $buffer->toString(); + } else { + $length = strlen($buffer); + } + if ($length > $this->remaining()) { + throw new BufferException('put length > remaining'); + } + $lengthWrite = fwrite($this->resource, $buffer, $length); + if ($lengthWrite === false || $lengthWrite !== $length) { + throw new BufferException('Not write all bytes. Length: ' . $length . ', write length: ' . $lengthWrite); + } + $this->position += $length; + return $this; + } + + /** + * @param Buffer|string $buffer + * @param int $length remove length bytes + * @return Buffer + * @throws BufferException + */ + public function replace($buffer, $length) + { + $length = (int)$length; + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($length < 0) { + throw new BufferException('length < 0'); + } + if ($length > $this->remaining()) { + throw new BufferException('replace length > remaining'); + } + if ($buffer === null) { + throw new BufferException('null buffer'); + } + if ($buffer instanceof Buffer) { + $buffer = $buffer->toString(); + } + $lengthBuffer = strlen($buffer); + + $position = $this->position; + $this->setPosition($position + $length); + $buffer .= stream_get_contents($this->resource); + $this->setPosition($position); + ftruncate($this->resource, $position); + $lengthNewBuffer = strlen($buffer); + + $lengthWrite = fwrite($this->resource, $buffer, $lengthNewBuffer); + if ($lengthWrite === false || $lengthWrite !== $lengthNewBuffer) { + throw new BufferException('Not write all bytes. Length: ' . $lengthNewBuffer . ', write length: ' . $lengthWrite); + } + $this->newLimit($this->size() + $lengthBuffer - $length); + $this->position += $lengthBuffer; + return $this; + } + + /** + * @param int $length + * @return Buffer + * @throws BufferException + */ + public function remove($length) + { + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($length < 0) { + throw new BufferException('length < 0'); + } + if ($length > $this->remaining()) { + throw new BufferException('remove length > remaining'); + } + $position = $this->position; + $this->setPosition($position + $length); + $buffer = stream_get_contents($this->resource); + $this->setPosition($position); + ftruncate($this->resource, $position); + $lengthNewBuffer = strlen($buffer); + + $lengthWrite = fwrite($this->resource, $buffer, $lengthNewBuffer); + if ($lengthWrite === false || $lengthWrite !== $lengthNewBuffer) { + throw new BufferException('Not write all bytes. Length: ' . $lengthNewBuffer . ', write length: ' . $lengthWrite); + } + $this->newLimit($this->size() - $length); + $this->position += $position; + return $this; + } + + /** + * Truncate file + * + * @param int $size + * @return Buffer + * @throws BufferException + */ + final public function truncate($size = 0) + { + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + ftruncate($this->resource, $size); + $this->rewind(); + $this->newLimit($size); + return $this; + } + + /** + * Destruct object, close file description. + */ + public function __destruct() + { + $this->close(); + } + + /** + * Close buffer. If this buffer resource that closes the stream. + */ + public function close() + { + if ($this->resource !== null && is_resource($this->resource)) { + fclose($this->resource); + $this->resource = null; + } + } + + /** + * Relative get method. + * Reads the string at this buffer's current position, and then increments the position. + * + * @param int $length + * @return string The strings at the buffer's current position + * @throws BufferException + */ + protected function get($length) + { + if (!$this->hasRemaining()) { + throw new BufferException('get length > remaining'); + } + $str = fread($this->resource, $length); + if ($str === false) { + throw new BufferException('error read resource. position - ' . $this->position . ', limit: ' . $this->size()); + } + $this->position += $length; + return $str; + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/StringBuffer.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/StringBuffer.php new file mode 100644 index 0000000000..c9a7bd7796 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/StringBuffer.php @@ -0,0 +1,230 @@ +setString($string); + } + + /** + * @param string $string + * @throws BufferException + */ + final public function setString($string) + { + $this->string = $string; + $this->rewind(); + $this->newLimit(strlen($this->string)); + } + + /** + * @return string + */ + final public function toString() + { + return $this->string; + } + + /** + * Flips this buffer. The limit is set to the current position and then + * the position is set to zero. + * + * After a sequence of channel-read or put operations, invoke + * this method to prepare for a sequence of channel-write or relative + * get operations. + * + * @return Buffer + * @throws BufferException + */ + final public function flip() + { + $this->setString(substr($this->string, 0, $this->position)); + $this->setPosition(0); + return $this; + } + + /** + * @param Buffer|string $buffer + * @return Buffer + * @throws BufferException + */ + public function insert($buffer) + { + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($buffer === null) { + throw new BufferException('null buffer'); + } + if ($buffer instanceof Buffer) { + $buffer = $buffer->toString(); + } + $length = strlen($buffer); + $this->string = substr_replace($this->string, $buffer, $this->position, 0); + $this->newLimit($this->size() + $length); + $this->skip($length); + return $this; + } + + /** + * Relative put method (optional operation). + * + * Writes the given string into this buffer at the current + * position, and then increments the position. + * + * @param Buffer|string $buffer + * @return Buffer + * @throws BufferException + */ + public function put($buffer) + { + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($buffer === null) { + throw new BufferException('null buffer'); + } + if ($buffer instanceof Buffer) { + $length = $buffer->size(); + $buffer = $buffer->toString(); + } else { + $length = strlen($buffer); + } + if ($length > $this->remaining()) { + throw new BufferException('put length > remaining'); + } + $this->string = substr_replace($this->string, $buffer, $this->position, $length); + $this->skip($length); + return $this; + } + + /** + * @param Buffer|string $buffer + * @param int $length remove length bytes + * @return Buffer + * @throws BufferException + */ + public function replace($buffer, $length) + { + $length = (int)$length; + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($length < 0) { + throw new BufferException('length < 0'); + } + if ($length > $this->remaining()) { + throw new BufferException('replace length > remaining'); + } + if ($buffer === null) { + throw new BufferException('null buffer'); + } + if ($buffer instanceof Buffer) { + $buffer = $buffer->toString(); + } + $bufferLength = strlen($buffer); + $this->string = substr_replace($this->string, $buffer, $this->position, $length); + $this->newLimit($this->size() + $bufferLength - $length); + $this->skip($bufferLength); + return $this; + } + + /** + * @param int $length + * @return Buffer + * @throws BufferException + */ + final public function remove($length) + { + if ($this->isReadOnly()) { + throw new BufferException('Read Only'); + } + if ($length < 0) { + throw new BufferException('length < 0'); + } + if ($length > $this->remaining()) { + throw new BufferException('remove length > remaining'); + } + $this->string = substr_replace($this->string, '', $this->position, $length); + $this->newLimit($this->size() - $length); + return $this; + } + + /** + * Truncate buffer + * + * @param int $size + * @return Buffer + * @throws BufferException + */ + final public function truncate($size = 0) + { + if ($size < $this->size()) { + $this->setString(substr($this->string, 0, $size)); + } + return $this; + } + + /** + * Destruct object, close file description. + */ + public function __destruct() + { + $this->close(); + } + + /** + * Close buffer. If this buffer resource that closes the stream. + */ + public function close() + { + if ($this->string !== null) { + $this->string = null; + } + } + + /** + * Relative get method. + * Reads the string at this buffer's current position, and then increments the position. + * + * @param $length + * @return string The strings at the buffer's current position + * @throws BufferException + */ + protected function get($length) + { + if ($length > $this->remaining()) { + throw new BufferException('get length > remaining'); + } + $str = substr($this->string, $this->position, $length); + $this->skip($length); + return $str; + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/TempBuffer.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/TempBuffer.php new file mode 100644 index 0000000000..bfc5ced2a7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/src/Nelexa/Buffer/TempBuffer.php @@ -0,0 +1,36 @@ +setTimeMillis($timeMillis); + $instance->setCategories($categories); + return $instance; + } + + /** + * @return int + */ + public function getTimeMillis() + { + return $this->timeMillis; + } + + /** + * @param int $timeMillis + */ + public function setTimeMillis($timeMillis) + { + $this->timeMillis = $timeMillis; + } + + /** + * @return array + */ + public function getCategories() + { + return $this->categories; + } + + /** + * @param array $categories + */ + public function setCategories($categories) + { + $this->categories = $categories; + } + + /** + * @param Buffer $buffer + * @throws \Nelexa\Buffer\BufferException + */ + public function readObject(Buffer $buffer) + { + $this->timeMillis = $buffer->getLong(); + $length = $buffer->getInt(); + $this->categories = []; + for ($i = 0; $i < $length; $i++) { + $this->categories[] = $buffer->getUTF(); + } + } + + /** + * @param Buffer $buffer + * @throws \Nelexa\Buffer\BufferException + */ + public function writeObject(Buffer $buffer) + { + $buffer->insertLong($this->timeMillis); + $length = count($this->categories); + $buffer->insertInt($length); + foreach ($this->categories as $i => $iValue) { + $buffer->insertUTF($this->categories[$i]); + } + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BinaryFormat/BinaryFileTestFormat.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BinaryFormat/BinaryFileTestFormat.php new file mode 100644 index 0000000000..e063ca445c --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BinaryFormat/BinaryFileTestFormat.php @@ -0,0 +1,100 @@ +setName($name); + $instance->setItems($items); + return $instance; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return BinaryFileItem[] + */ + public function getItems() + { + return $this->items; + } + + /** + * @param BinaryFileItem[] $items + */ + public function setItems($items) + { + $this->items = $items; + } + + /** + * @param Buffer $buffer + * @throws \Nelexa\Buffer\BufferException + */ + public function readObject(Buffer $buffer) + { + $this->name = $buffer->getUTF(); + $length = $buffer->getInt(); + $this->items = []; + for ($i = 0; $i < $length; $i++) { + $item = new BinaryFileItem(); + $item->readObject($buffer); + $this->items[] = $item; + } + } + + /** + * @param Buffer $buffer + * @throws \Nelexa\Buffer\BufferException + */ + public function writeObject(Buffer $buffer) + { + $buffer->insertUTF($this->name); + $length = count($this->items); + $buffer->insertInt($length); + foreach ($this->items as $item) { + $item->writeObject($buffer); + } + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BufferTestCase.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BufferTestCase.php new file mode 100644 index 0000000000..c0434020ee --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/BufferTestCase.php @@ -0,0 +1,583 @@ +buffer = $this->createBuffer(); + if (!($this->buffer instanceof Buffer)) { + throw new \AssertionError('$buffer can\'t implements Buffer'); + } + } + + protected function tearDown() + { + parent::tearDown(); + $this->buffer->close(); + } + + /** + * @return Buffer + */ + abstract protected function createBuffer(); + + /** + * @throws BufferException + */ + public function testBaseFunctional() + { + $this->buffer->insertString('Telephone'); + $this->buffer->rewind(); + $this->buffer->putString('My I'); + $this->assertEquals($this->buffer->toString(), 'My Iphone'); + + $this->buffer->rewind(); + $this->buffer->replaceString('P', 5); + $this->assertEquals($this->buffer->toString(), 'Phone'); + + $this->buffer->rewind(); + $this->buffer->insertString('Tele'); + $this->assertEquals($this->buffer->toString(), 'TelePhone'); + + $this->buffer->skip(2); + $this->buffer->flip(); + $this->assertEquals($this->buffer->position(), 0); + $this->assertEquals($this->buffer->toString(), 'TelePh'); + + $this->buffer->truncate(); + $this->assertEquals($this->buffer->position(), 0); + $this->assertEquals($this->buffer->size(), 0); + } + + /** + * @throws BufferException + */ + public function testFluent() + { + $this->buffer->insertByte(1) + ->insertBoolean(true) + ->insertShort(5551) + ->skip(-2) + ->insertUTF('Hello, World') + ->truncate() + ->insertString(str_rot13('Hello World')) + ->setPosition(7) + ->flip(); + $this->assertEquals($this->buffer->size(), 7); + $this->assertEquals($this->buffer->position(), 0); + $this->assertEquals($this->buffer->toString(), str_rot13('Hello W')); + } + + /** + * @throws BufferException + */ + public function testInsertFunctional() + { + $orders = [Buffer::BIG_ENDIAN, Buffer::LITTLE_ENDIAN]; + + foreach ($orders as $order) { + $this->buffer->truncate(); + $this->buffer->setOrder($order); + + $byte1 = 34; + $byte2 = 3432424; + $byte3 = -100; + + $this->buffer->insertByte($byte1); + $this->buffer->insertByte($byte2); + $this->buffer->insertByte($byte3); + + $short1 = 31111; + $short2 = -12444; + $short3 = 243253233; + + $this->buffer->insertShort($short1); + $this->buffer->insertShort($short2); + $this->buffer->insertShort($short3); + + $int1 = Cast::INTEGER_MIN_VALUE; + $int2 = Cast::INTEGER_MIN_VALUE - 1; + $int3 = Cast::INTEGER_MAX_VALUE; + $int4 = Cast::INTEGER_MAX_VALUE + 1; + $int5 = 24234333; + + $this->buffer->insertInt($int1); + $this->buffer->insertInt($int2); + $this->buffer->insertInt($int3); + $this->buffer->insertInt($int4); + $this->buffer->insertInt($int5); + + $long1 = Cast::LONG_MIN_VALUE; + $long2 = Cast::LONG_MAX_VALUE; + $long3 = Cast::BYTE_MIN_VALUE; + $long4 = 0; + $long5 = 243535423222; + + $this->buffer->insertLong($long1); + $this->buffer->insertLong($long2); + $this->buffer->insertLong($long3); + $this->buffer->insertLong($long4); + $this->buffer->insertLong($long5); + + $bool1 = true; + $bool2 = false; + + $this->buffer->insertBoolean($bool1); + $this->buffer->insertBoolean($bool2); + + $arrayBytes = [0x01, 0x02, 0x03, 0x4, Cast::toByte(Cast::INTEGER_MAX_VALUE)]; + $this->buffer->insertArrayBytes($arrayBytes); + + $string = 'String... Строка... 串... + 😀 😬 😁 😂 😃 😄 😅 😆 😇 😉 😊 😊 🙂 🙃 ☺️ 😋 😌 😍 😘 + 🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 + 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇶 🇧🇦 🇧🇼 + 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇨🇻 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇰🇾 🇨🇫 🇹🇩 + 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇭🇷 🇨🇺 🇨🇼 🇨🇾 + 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 + 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 + 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 + 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇨🇮 🇯🇲 🇯🇵 🇯🇪 🇯🇴 🇰🇿 + 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 + 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 + 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 + 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇲🇵 🇰🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 + 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇧🇱 + 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇼🇸 🇸🇲 🇸🇹 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 + 🇸🇽 🇸🇰 🇸🇮 🇸🇧 🇸🇴 🇿🇦 🇬🇸 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇸🇩 🇸🇷 🇸🇿 + 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 + 🇹🇲 🇹🇨 🇹🇻 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🇺🇸 🇻🇮 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 + 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼 '; + $lengthString = strlen($string); + + $this->buffer->insertString($string); + $this->buffer->insertUTF($string); + $this->buffer->insertUTF16($string); + + $otherBuffer = new MemoryResourceBuffer(str_rot13($string)); + $this->buffer->insert($otherBuffer); + + $this->buffer->rewind(); + + $this->assertEquals($this->buffer->position(), 0); + $this->assertEquals($this->buffer->getByte(), Cast::toByte($byte1)); + $this->assertEquals($this->buffer->position(), 1); + $this->assertEquals($this->buffer->getByte(), Cast::toByte($byte2)); + $this->assertEquals($this->buffer->position(), 2); + $this->assertEquals($this->buffer->getByte(), Cast::toByte($byte3)); + $this->assertEquals($this->buffer->position(), 3); + + $this->buffer->setPosition(0); + + $this->assertEquals($this->buffer->position(), 0); + $this->assertEquals($this->buffer->getUnsignedByte(), Cast::toUnsignedByte($byte1)); + $this->assertEquals($this->buffer->position(), 1); + $this->assertEquals($this->buffer->getUnsignedByte(), Cast::toUnsignedByte($byte2)); + $this->assertEquals($this->buffer->position(), 2); + $this->assertEquals($this->buffer->getUnsignedByte(), Cast::toUnsignedByte($byte3)); + $this->assertEquals($this->buffer->position(), 3); + + $this->assertEquals($this->buffer->getShort(), Cast::toShort($short1)); + $this->assertEquals($this->buffer->position(), 5); + $this->assertEquals($this->buffer->getShort(), Cast::toShort($short2)); + $this->assertEquals($this->buffer->position(), 7); + $this->assertEquals($this->buffer->getShort(), Cast::toShort($short3)); + $this->assertEquals($this->buffer->position(), 9); + + $this->buffer->skip(-6); + + $this->assertEquals($this->buffer->position(), 3); + $this->assertEquals($this->buffer->getUnsignedShort(), Cast::toUnsignedShort($short1)); + $this->assertEquals($this->buffer->position(), 5); + $this->assertEquals($this->buffer->getUnsignedShort(), Cast::toUnsignedShort($short2)); + $this->assertEquals($this->buffer->position(), 7); + $this->assertEquals($this->buffer->getUnsignedShort(), Cast::toUnsignedShort($short3)); + $this->assertEquals($this->buffer->position(), 9); + + $this->assertEquals($this->buffer->getInt(), Cast::toInt($int1)); + $this->assertEquals($this->buffer->position(), 13); + $this->assertEquals($this->buffer->getInt(), Cast::toInt($int2)); + $this->assertEquals($this->buffer->position(), 17); + $this->assertEquals($this->buffer->getInt(), Cast::toInt($int3)); + $this->assertEquals($this->buffer->position(), 21); + $this->assertEquals($this->buffer->getInt(), Cast::toInt($int4)); + $this->assertEquals($this->buffer->position(), 25); + $this->assertEquals($this->buffer->getInt(), Cast::toInt($int5)); + $this->assertEquals($this->buffer->position(), 29); + + $this->buffer->skip(-20); + + $this->assertEquals($this->buffer->getUnsignedInt(), Cast::toUnsignedInt($int1)); + $this->assertEquals($this->buffer->position(), 13); + $this->assertEquals($this->buffer->getUnsignedInt(), Cast::toUnsignedInt($int2)); + $this->assertEquals($this->buffer->position(), 17); + $this->assertEquals($this->buffer->getUnsignedInt(), Cast::toUnsignedInt($int3)); + $this->assertEquals($this->buffer->position(), 21); + $this->assertEquals($this->buffer->getUnsignedInt(), Cast::toUnsignedInt($int4)); + $this->assertEquals($this->buffer->position(), 25); + $this->assertEquals($this->buffer->getUnsignedInt(), Cast::toUnsignedInt($int5)); + $this->assertEquals($this->buffer->position(), 29); + + $this->assertEquals($this->buffer->getLong(), Cast::toLong($long1)); + $this->assertEquals($this->buffer->position(), 37); + $this->assertEquals($this->buffer->getLong(), Cast::toLong($long2)); + $this->assertEquals($this->buffer->position(), 45); + $this->assertEquals($this->buffer->getLong(), Cast::toLong($long3)); + $this->assertEquals($this->buffer->position(), 53); + $this->assertEquals($this->buffer->getLong(), Cast::toLong($long4)); + $this->assertEquals($this->buffer->position(), 61); + $this->assertEquals($this->buffer->getLong(), Cast::toLong($long5)); + $this->assertEquals($this->buffer->position(), 69); + + $this->assertEquals($this->buffer->getBoolean(), $bool1); + $this->assertEquals($this->buffer->position(), 70); + $this->assertEquals($this->buffer->getBoolean(), $bool2); + $this->assertEquals($this->buffer->position(), 71); + + $this->assertEquals($this->buffer->getArrayBytes(5), $arrayBytes); + $this->assertEquals($this->buffer->position(), 76); + + $this->assertEquals($this->buffer->getString($lengthString), $string); + $this->assertEquals($this->buffer->position(), 76 + $lengthString); + + $this->assertEquals($this->buffer->getUTF(), $string); + $this->assertEquals($this->buffer->position(), 78 + $lengthString * 2); + + $this->assertEquals($this->buffer->getUTF16($lengthString), $string); + $this->assertEquals($this->buffer->position(), 78 + $lengthString * 4); + + $this->assertEquals($this->buffer->getString($lengthString), $otherBuffer->toString()); + $this->assertEquals($this->buffer->position(), 78 + $lengthString * 5); + } + } + + /** + * @throws BufferException + */ + public function testPutFunctional() + { + $this->buffer->setOrder(Buffer::BIG_ENDIAN); + $this->buffer->insertLong(12345); + $this->buffer->setPosition(4); + $this->buffer->putInt(98765); + $this->buffer->rewind(); + $this->assertEquals($this->buffer->getLong(), 98765); + + $this->buffer->rewind(); + $this->buffer->setOrder(Buffer::LITTLE_ENDIAN); + $this->buffer->putLong(12345); + $this->buffer->rewind(); + $this->assertEquals($this->buffer->getLong(), 12345); + $this->buffer->setPosition(0); + $this->buffer->putInt(98765); + $this->buffer->rewind(); + $this->assertEquals($this->buffer->getLong(), 98765); + } + + /** + * @throws BufferException + */ + public function testReplaceFunctional() + { + $this->buffer->insertString('123456789'); + $this->buffer->setPosition(3); + $this->buffer->replaceBoolean(true, 3); + $this->assertEquals('123789', $this->buffer->toString()); + $this->buffer->skip(-1); + $this->buffer->replaceString('', 1); + $this->assertEquals('123789', $this->buffer->toString()); + $this->buffer->replaceString('456', 0); + $this->assertEquals('123456789', $this->buffer->toString()); + } + + /** + * @throws BufferException + */ + public function testRemoveFunctional() + { + $this->buffer->insertString('123456789'); + $this->buffer->setPosition(3); + $this->buffer->remove(3); + $this->assertEquals('123789', $this->buffer->toString()); + } + + /** + * @expectedException \Nelexa\Buffer\BufferException + * @expectedExceptionMessage put length > remaining + */ + public function testPutException() + { + $this->assertEquals($this->buffer->size(), 0); + $this->buffer->putString('Test'); + } + + /** + * @expectedException \Nelexa\Buffer\BufferException + * @expectedExceptionMessage put length > remaining + */ + public function testPutException2() + { + $this->buffer + ->insertString('Test') + ->rewind() + ->putString('My Test'); + } + + /** + * @expectedException \Nelexa\Buffer\BufferException + * @expectedExceptionMessage replace length > remaining + */ + public function testReplaceException() + { + $this->assertEquals($this->buffer->size(), 0); + $this->buffer->replaceString('Test', 5); + } + + /** + * @expectedException \Nelexa\Buffer\BufferException + * @expectedExceptionMessage remove length > remaining + */ + public function testRemoveException() + { + $this->assertEquals($this->buffer->size(), 0); + $this->buffer->remove(1); + } + + /** + * @expectedException \Nelexa\Buffer\BufferException + * @expectedExceptionMessage Read Only + */ + public function testReadOnly() + { + $this->assertEquals($this->buffer->isReadOnly(), false); + $this->buffer->setReadOnly(true); + $this->assertEquals($this->buffer->isReadOnly(), true); + $this->buffer->insertBoolean(true); + } + + /** + * @throws BufferException + */ + public function testOrder() + { + $this->assertEquals($this->buffer->order(), Buffer::BIG_ENDIAN); + + $this->buffer->insertByte(50) + ->insertShort(5000) + ->insertInt(50000000) + ->insertLong(5000000000); + + $this->buffer->setOrder(Buffer::LITTLE_ENDIAN)->rewind(); + $this->assertEquals($this->buffer->order(), Buffer::LITTLE_ENDIAN); + + $this->assertEquals($this->buffer->getByte(), 50); + $this->assertEquals($this->buffer->getShort(), -30701); + $this->assertEquals($this->buffer->getInt(), -2131691006); + $this->assertEquals($this->buffer->getLong(), 68122622327521280); + + $this->buffer->setOrder(Buffer::BIG_ENDIAN)->rewind(); + $this->assertEquals($this->buffer->order(), Buffer::BIG_ENDIAN); + + $this->assertEquals($this->buffer->getByte(), 50); + $this->assertEquals($this->buffer->getShort(), 5000); + $this->assertEquals($this->buffer->getInt(), 50000000); + $this->assertEquals($this->buffer->getLong(), 5000000000); + } + + /** + * @throws BufferException + */ + public function testPositions() + { + $this->buffer->insertString('Test value'); + $this->assertEquals($this->buffer->size(), 10); + $this->assertEquals($this->buffer->position(), 10); + + $this->buffer->setPosition(3); + $this->assertEquals($this->buffer->position(), 3); + + $this->buffer->skip(2); + $this->assertEquals($this->buffer->position(), 5); + + $this->buffer->skip(-4); + $this->assertEquals($this->buffer->position(), 1); + + $this->assertEquals($this->buffer->remaining(), 9); + $this->assertEquals($this->buffer->hasRemaining(), true); + + $this->buffer->setPosition($this->buffer->size()); + $this->assertEquals($this->buffer->position(), 10); + $this->assertEquals($this->buffer->remaining(), 0); + $this->assertEquals($this->buffer->hasRemaining(), false); + + $this->buffer->rewind(); + $this->assertEquals($this->buffer->position(), 0); + + $this->buffer->insertString(str_repeat('*', 100)); + $this->assertEquals($this->buffer->position(), 100); + $this->assertEquals($this->buffer->size(), 110); + + $this->buffer->setPosition(0); + $this->assertEquals($this->buffer->position(), 0); + + $this->buffer->skipByte(); + $this->assertEquals($this->buffer->position(), 1); + + $this->buffer->skipShort(); + $this->assertEquals($this->buffer->position(), 3); + + $this->buffer->skipInt(); + $this->assertEquals($this->buffer->position(), 7); + + $this->buffer->skipLong(); + $this->assertEquals($this->buffer->position(), 15); + + $this->buffer->toString(); + $this->assertEquals($this->buffer->position(), 15); + + $this->buffer->flip(); + $this->assertEquals($this->buffer->position(), 0); + $this->assertEquals($this->buffer->size(), 15); + + $this->buffer->setPosition(5)->truncate(); + $this->assertEquals($this->buffer->position(), 0); + $this->assertEquals($this->buffer->size(), 0); + + $this->buffer->insertBoolean(true); + $this->assertEquals($this->buffer->position(), 1); + $this->assertEquals($this->buffer->size(), 1); + $this->buffer->truncate(); + + $this->buffer->insertByte(0); + $this->assertEquals($this->buffer->position(), 1); + $this->assertEquals($this->buffer->size(), 1); + $this->buffer->truncate(); + + $this->buffer->insertShort(0); + $this->assertEquals($this->buffer->position(), 2); + $this->assertEquals($this->buffer->size(), 2); + $this->buffer->truncate(); + + $this->buffer->insertInt(0); + $this->assertEquals($this->buffer->position(), 4); + $this->assertEquals($this->buffer->size(), 4); + $this->buffer->truncate(); + + $this->buffer->insertLong(0); + $this->assertEquals($this->buffer->position(), 8); + $this->assertEquals($this->buffer->size(), 8); + $this->buffer->truncate(); + + $this->buffer->insertArrayBytes([5, 5, 6, 5, 7, 8, 9]); + $this->assertEquals($this->buffer->position(), 7); + $this->assertEquals($this->buffer->size(), 7); + $this->buffer->truncate(); + + $this->buffer->insertUTF('Test'); + $this->assertEquals($this->buffer->position(), 6); + $this->assertEquals($this->buffer->size(), 6); + $this->buffer->truncate(); + + $this->buffer->insertUTF16('Test'); + $this->assertEquals($this->buffer->position(), 8); + $this->assertEquals($this->buffer->size(), 8); + $this->buffer->truncate(); + } + + /** + * @throws BufferException + */ + public function testBinaryFile() + { + $name = 'General Name'; + $items = [ + BinaryFileItem::create(time() * 1000, ['Category 1', 'Category 2']), + BinaryFileItem::create((time() - 3600) * 1000, ['Category 2', 'Category 3']), + BinaryFileItem::create((time() - 52222) * 1000, ['Category 4', 'Category 2', 'Category 7']), + ]; + + $binaryFileActual = BinaryFileTestFormat::create($name, $items); + $binaryFileActual->writeObject($this->buffer); + $output = $this->buffer->toString(); + + $buffer = new StringBuffer($output); + $binaryFileExpected = new BinaryFileTestFormat(); + $binaryFileExpected->readObject($buffer); + + $this->assertEquals($binaryFileExpected, $binaryFileActual); + } + + /** + * @throws BufferException + * @requires PHP 7.0.15 + */ + public function testDouble() + { + $double = 12.6664287277627762; // 64 bit + + $buffer = $this->createBuffer(); + + $buffer->insertDouble($double); + $this->assertEquals($buffer->size(), 8); + + $buffer->rewind(); + $this->assertEquals($buffer->getDouble(), $double); + $this->assertEquals($buffer->position(), 8); + + $buffer->rewind(); + $buffer->skipDouble(); + $this->assertEquals($buffer->position(), 8); + + $buffer->rewind(); + $this->assertEquals($buffer->getArrayBytes(8), [64, 41, 85, 54, 37, 109, -74, 71]); + } + + /** + * @throws BufferException + * @requires PHP 7.0.15 + */ + public function testFloat() + { + $float = 12.666428565979; // 32 bit + + $buffer = $this->createBuffer(); + + $buffer->insertFloat($float); + $this->assertEquals($buffer->size(), 4); + + $buffer->rewind(); + $this->assertEquals($buffer->getFloat(), $float); + $this->assertEquals($buffer->position(), 4); + + $buffer->rewind(); + $buffer->skipFloat(); + $this->assertEquals($buffer->position(), 4); + + $buffer->rewind(); + $this->assertEquals($buffer->getArrayBytes(4), [65, 74, -87, -79]); + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/CastTest.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/CastTest.php new file mode 100644 index 0000000000..9a614eb27c --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/CastTest.php @@ -0,0 +1,440 @@ + 86, -1960415292 => -60, -1799385197 => -109, -1721937620 => 44, + -1530997534 => -30, -1526855311 => 113, -1298630511 => -111, -1259382890 => -106, + -1074950937 => -25, -892932280 => 72, -737085923 => 29, -698175103 => -127, + -616855785 => 23, -486032759 => -119, -447665782 => -118, -421148997 => -69, + -412081654 => 10, -268644677 => -69, -244595046 => -102, -105212087 => 73, + -132 => 124, -131 => 125, -130 => 126, -129 => 127, -128 => -128, -127 => -127, + -126 => -126, -125 => -125, -124 => -124, -123 => -123, -120 => -120, -119 => -119, + -116 => -116, -115 => -115, -113 => -113, -107 => -107, -106 => -106, -105 => -105, + -104 => -104, -103 => -103, -100 => -100, -99 => -99, -91 => -91, -82 => -82, + -80 => -80, -75 => -75, -74 => -74, -73 => -73, -72 => -72, -70 => -70, -67 => -67, + -58 => -58, -57 => -57, -56 => -56, -55 => -55, -54 => -54, -53 => -53, -49 => -49, + -47 => -47, -46 => -46, -44 => -44, -38 => -38, -27 => -27, -23 => -23, -14 => -14, + -9 => -9, -8 => -8, -7 => -7, -6 => -6, -2 => -2, -1 => -1, 2 => 2, 3 => 3, 4 => 4, + 9 => 9, 11 => 11, 12 => 12, 14 => 14, 20 => 20, 23 => 23, 24 => 24, 27 => 27, 30 => 30, + 37 => 37, 41 => 41, 47 => 47, 48 => 48, 50 => 50, 51 => 51, 54 => 54, 55 => 55, + 57 => 57, 59 => 59, 68 => 68, 69 => 69, 70 => 70, 71 => 71, 74 => 74, 83 => 83, + 85 => 85, 88 => 88, 89 => 89, 93 => 93, 96 => 96, 103 => 103, 104 => 104, 107 => 107, + 111 => 111, 116 => 116, 120 => 120, 123 => 123, 124 => 124, 125 => 125, 126 => 126, + 127 => 127, 128 => -128, 129 => -127, 130 => -126, 131 => -125, 132 => -124, + 7207204 => 36, 233845854 => 94, 334702437 => 101, 340410750 => 126, 430326926 => -114, + 471183925 => 53, 499194653 => 29, 682760454 => 6, 691691143 => -121, 972475720 => 72, + 1130159086 => -18, 1238801648 => -16, 1273111800 => -8, 1326838210 => -62, + 1462828643 => 99, 1495706013 => -99, 1664325026 => -94, 1752068802 => -62, + 1798269036 => 108, 1883221567 => 63, + ]; + + $expectedCast = []; + foreach ($actualCast as $i => $value) { + $expectedCast[$i] = Cast::toByte($i); + } + $this->assertEquals($expectedCast, $actualCast); + } + + public function testCastToUnsignedByte() + { + $actualCast = [ + -2136595330 => 126, -2128000797 => 227, -2112934983 => 185, -2024602240 => 128, + -1983244330 => 214, -1588151360 => 192, -1536373296 => 208, -1527028339 => 141, + -1497595627 => 21, -1388221194 => 246, -1346350122 => 214, -1323025019 => 133, + -951240289 => 159, -868324821 => 43, -817456424 => 216, -807634866 => 78, + -557532140 => 20, -258867856 => 112, -247321292 => 52, -46127326 => 34, + -132 => 124, -131 => 125, -130 => 126, -129 => 127, -128 => 128, -127 => 129, + -126 => 130, -125 => 131, -124 => 132, -123 => 133, -122 => 134, -120 => 136, + -118 => 138, -114 => 142, -109 => 147, -106 => 150, -105 => 151, -102 => 154, + -99 => 157, -97 => 159, -87 => 169, -86 => 170, -82 => 174, -78 => 178, + -75 => 181, -74 => 182, -69 => 187, -66 => 190, -58 => 198, -57 => 199, -54 => 202, + -50 => 206, -34 => 222, -32 => 224, -25 => 231, -24 => 232, -23 => 233, -18 => 238, + -17 => 239, -13 => 243, -11 => 245, -9 => 247, -7 => 249, -6 => 250, -5 => 251, + -4 => 252, -3 => 253, 5 => 5, 11 => 11, 13 => 13, 14 => 14, 15 => 15, 17 => 17, + 18 => 18, 19 => 19, 23 => 23, 27 => 27, 29 => 29, 31 => 31, 38 => 38, 40 => 40, + 41 => 41, 46 => 46, 48 => 48, 54 => 54, 57 => 57, 59 => 59, 61 => 61, 62 => 62, + 65 => 65, 70 => 70, 71 => 71, 75 => 75, 76 => 76, 77 => 77, 79 => 79, 81 => 81, + 82 => 82, 95 => 95, 101 => 101, 104 => 104, 112 => 112, 114 => 114, 116 => 116, + 117 => 117, 119 => 119, 120 => 120, 122 => 122, 123 => 123, 124 => 124, 125 => 125, + 126 => 126, 127 => 127, 128 => 128, 129 => 129, 130 => 130, 131 => 131, 132 => 132, + 70749593 => 153, 373009742 => 78, 393363356 => 156, 403215862 => 246, + 526361226 => 138, 740206296 => 216, 744006616 => 216, 823793575 => 167, + 887569610 => 202, 889805411 => 99, 920302796 => 204, 973062939 => 27, + 1150941609 => 169, 1261437697 => 1, 1322397075 => 147, 1363958510 => 238, + 1656026962 => 82, 1721052657 => 241, 1945030068 => 180, 1986358021 => 5, + ]; + + $expectedCast = []; + foreach ($actualCast as $i => $value) { + $expectedCast[$i] = Cast::toUnsignedByte($i); + } + $this->assertEquals($expectedCast, $actualCast); + } + + public function testCastToShort() + { + $actualCast = [ + -1971175757 => 16051, -1968146222 => 30930, -1925425704 => 21976, + -1868190929 => -21713, -1821565081 => 8039, -1685254381 => 3859, + -1436069992 => 20376, -1185520696 => 25544, -1062425742 => -21646, + -999882771 => -19, -816367898 => 14054, -725302878 => -15966, + -490449662 => 21762, -378319475 => 19853, -304165789 => -13213, + -275551518 => 27362, -262172158 => -28158, -254535372 => 6452, - + 72772953 => -27993, -41911111 => 31929, -32772 => 32764, -32771 => 32765, + -32770 => 32766, -32769 => 32767, -32768 => -32768, -32767 => -32767, + -32766 => -32766, -32765 => -32765, -32764 => -32764, -32763 => -32763, + -31605 => -31605, -31279 => -31279, -31152 => -31152, -28716 => -28716, + -27155 => -27155, -26048 => -26048, -24067 => -24067, -23840 => -23840, + -23723 => -23723, -22833 => -22833, -22344 => -22344, -22189 => -22189, + -22046 => -22046, -21882 => -21882, -21787 => -21787, -21011 => -21011, + -20809 => -20809, -20377 => -20377, -20197 => -20197, -20025 => -20025, + -18803 => -18803, -18692 => -18692, -18235 => -18235, -17069 => -17069, + -16937 => -16937, -16602 => -16602, -15059 => -15059, -14614 => -14614, + -14380 => -14380, -14106 => -14106, -12603 => -12603, -11774 => -11774, + -9780 => -9780, -9540 => -9540, -8646 => -8646, -8387 => -8387, -8063 => -8063, + -6846 => -6846, -4381 => -4381, -3648 => -3648, -2900 => -2900, -2408 => -2408, + -4 => -4, 1168 => 1168, 1329 => 1329, 1834 => 1834, 2081 => 2081, 2710 => 2710, + 3355 => 3355, 4376 => 4376, 4868 => 4868, 5156 => 5156, 5287 => 5287, + 6027 => 6027, 6167 => 6167, 9684 => 9684, 10350 => 10350, 11797 => 11797, + 11982 => 11982, 12126 => 12126, 12444 => 12444, 12512 => 12512, 12674 => 12674, + 13176 => 13176, 13714 => 13714, 14760 => 14760, 16165 => 16165, 16665 => 16665, + 16862 => 16862, 17506 => 17506, 18983 => 18983, 19099 => 19099, 19513 => 19513, + 19574 => 19574, 19811 => 19811, 20268 => 20268, 20349 => 20349, 20794 => 20794, + 21199 => 21199, 21334 => 21334, 21891 => 21891, 21965 => 21965, 24248 => 24248, + 24844 => 24844, 25011 => 25011, 25254 => 25254, 26311 => 26311, 26521 => 26521, + 26586 => 26586, 26815 => 26815, 27370 => 27370, 27426 => 27426, 27756 => 27756, + 28437 => 28437, 29696 => 29696, 29953 => 29953, 30160 => 30160, 30393 => 30393, + 30968 => 30968, 31071 => 31071, 32763 => 32763, 32764 => 32764, 32765 => 32765, + 32766 => 32766, 32767 => 32767, 32768 => -32768, 32769 => -32767, + 32770 => -32766, 32771 => -32765, 32772 => -32764, 67183888 => 9488, + 351130529 => -11359, 416036100 => 13572, 419148444 => -19812, + 514228657 => -32335, 654021230 => -28050, 743113735 => 1031, + 805407091 => -30349, 875355480 => -8872, 878683109 => -23579, + 941712008 => 25224, 1505475186 => -17806, 1509506213 => 15525, + 1524854795 => 28683, 1581123294 => 1758, 1665603962 => 6522, + 1826409361 => -13423, 1979551636 => -28780, 2054791296 => -24448, 2143956813 => 12109, + ]; + $expectedCast = []; + foreach ($actualCast as $i => $value) { + $expectedCast[$i] = Cast::toShort($i); + } + $this->assertEquals($expectedCast, $actualCast); + } + + public function testCastToUnsignedShort() + { + $actualCast = [ + -1545551207 => 49817, -1416288743 => 9753, -1374427376 => 59152, + -1358269249 => 29887, -1356152893 => 49091, -1127384673 => 31135, + -1122383938 => 51134, -1093018380 => 56564, -988578364 => 32196, + -932564899 => 12381, -723356577 => 29791, -719126083 => 445, + -513574700 => 30932, -480460457 => 49495, -379567831 => 16681, + -277828067 => 44573, -238559590 => 56986, -236028290 => 32382, + -220004359 => 65529, -46021472 => 50336, -32772 => 32764, + -32771 => 32765, -32770 => 32766, -32769 => 32767, -32768 => 32768, + -32767 => 32769, -32766 => 32770, -32765 => 32771, -32764 => 32772, + -32763 => 32773, -32318 => 33218, -31670 => 33866, -31598 => 33938, + -31443 => 34093, -31142 => 34394, -30229 => 35307, -29387 => 36149, + -29306 => 36230, -29234 => 36302, -28815 => 36721, -28334 => 37202, + -25406 => 40130, -25135 => 40401, -24032 => 41504, -22198 => 43338, + -20584 => 44952, -19906 => 45630, -19621 => 45915, -19173 => 46363, + -18876 => 46660, -18427 => 47109, -17124 => 48412, -16953 => 48583, + -15452 => 50084, -15074 => 50462, -14621 => 50915, -12600 => 52936, + -12373 => 53163, -12241 => 53295, -12204 => 53332, -11883 => 53653, + -11614 => 53922, -11393 => 54143, -11251 => 54285, -11063 => 54473, + -10834 => 54702, -8947 => 56589, -8940 => 56596, -8696 => 56840, + -8592 => 56944, -8095 => 57441, -7949 => 57587, -7761 => 57775, + -7415 => 58121, -7255 => 58281, -4564 => 60972, -1841 => 63695, + -1537 => 63999, -1463 => 64073, -851 => 64685, -698 => 64838, + 1951 => 1951, 2211 => 2211, 2452 => 2452, 2540 => 2540, 3429 => 3429, + 3592 => 3592, 5413 => 5413, 5919 => 5919, 6061 => 6061, 6268 => 6268, + 6917 => 6917, 7153 => 7153, 7168 => 7168, 7833 => 7833, 10152 => 10152, + 10234 => 10234, 10604 => 10604, 10739 => 10739, 12036 => 12036, + 12427 => 12427, 14441 => 14441, 15158 => 15158, 15258 => 15258, + 15768 => 15768, 17450 => 17450, 17805 => 17805, 17865 => 17865, + 18795 => 18795, 18907 => 18907, 19026 => 19026, 20432 => 20432, + 20939 => 20939, 21141 => 21141, 21362 => 21362, 22254 => 22254, + 22941 => 22941, 24201 => 24201, 24874 => 24874, 25556 => 25556, + 25982 => 25982, 27238 => 27238, 28042 => 28042, 29510 => 29510, + 30145 => 30145, 30703 => 30703, 31015 => 31015, 31311 => 31311, + 32269 => 32269, 32397 => 32397, 32763 => 32763, 32764 => 32764, + 32765 => 32765, 32766 => 32766, 32767 => 32767, 32768 => 32768, + 32769 => 32769, 32770 => 32770, 32771 => 32771, 32772 => 32772, + 65185292 => 42508, 235338675 => 64435, 578753291 => 4875, + 725428702 => 10718, 831988380 => 8860, 880887738 => 18362, + 994444768 => 1504, 1024813905 => 27473, 1155681093 => 19269, + 1206707727 => 58895, 1237051794 => 59794, 1370943847 => 61799, + 1383859255 => 1079, 1392915340 => 13196, 1476657022 => 65406, + 1496526464 => 11904, 1545061262 => 50062, 1552719781 => 40869, + 1773429172 => 25012, 1899457496 => 27608, + ]; + $expectedCast = []; + foreach ($actualCast as $i => $value) { + $expectedCast[$i] = Cast::toUnsignedShort($i); + } + $this->assertEquals($expectedCast, $actualCast); + } + + public function testCastToInteger() + { + $actualCast = [ + -8452929933163922722 => -427853090, -8227809746396717005 => -494477261, + -8069447908768999435 => 2018383861, -7322911264700188104 => -774456776, + -7157486745453003186 => 481623630, -6294471842345940868 => 2084931708, + -5404213518254922788 => 437068764, -5121227846828842541 => 535667155, + -5031464466134410432 => -737223872, -4427976296617921775 => -969067759, + -4142492729853974452 => 1617586252, -3857943065800693273 => -817583641, + -3850022288003099130 => -1707272698, -3545206896667219024 => 1558358960, + -3304792279149730522 => -2133613274, -2947055917115005649 => -282745553, + -2434852234131164058 => 537637990, -585511715851594644 => 1423316076, + -536617668063548519 => -220222567, -461261921154970582 => 19188778, + -2147483652 => 2147483644, -2147483651 => 2147483645, -2147483650 => 2147483646, + -2147483649 => 2147483647, -2147483648 => -2147483648, -2147483647 => -2147483647, + -2147483646 => -2147483646, -2147483645 => -2147483645, -2147483644 => -2147483644, + -2147483643 => -2147483643, -2142666998 => -2142666998, -2111112991 => -2111112991, + -1997217588 => -1997217588, -1966955733 => -1966955733, -1945305989 => -1945305989, + -1935727095 => -1935727095, -1913777766 => -1913777766, -1907399486 => -1907399486, + -1872185297 => -1872185297, -1817768170 => -1817768170, -1809184885 => -1809184885, + -1715578898 => -1715578898, -1577437854 => -1577437854, -1539215067 => -1539215067, + -1508269993 => -1508269993, -1474712926 => -1474712926, -1444810077 => -1444810077, + -1416724699 => -1416724699, -1230624732 => -1230624732, -1066332637 => -1066332637, + -1025824621 => -1025824621, -992283120 => -992283120, -987038548 => -987038548, + -984948950 => -984948950, -954412209 => -954412209, -939075790 => -939075790, + -925408230 => -925408230, -793882323 => -793882323, -699831952 => -699831952, + -698682415 => -698682415, -613971751 => -613971751, -602932883 => -602932883, + -594971095 => -594971095, -580811529 => -580811529, -496216881 => -496216881, + -469326095 => -469326095, -425917711 => -425917711, -359291923 => -359291923, + -323749607 => -323749607, -228392199 => -228392199, -177008858 => -177008858, + -166848142 => -166848142, -124158916 => -124158916, -102650436 => -102650436, + -37289172 => -37289172, -19690778 => -19690778, 4395181 => 4395181, + 54754652 => 54754652, 75855103 => 75855103, 161761412 => 161761412, + 189289804 => 189289804, 233340012 => 233340012, 244137255 => 244137255, + 259625493 => 259625493, 275176129 => 275176129, 313881666 => 313881666, + 318619740 => 318619740, 361580175 => 361580175, 409226251 => 409226251, + 595656064 => 595656064, 699826088 => 699826088, 743084268 => 743084268, + 797188569 => 797188569, 911067568 => 911067568, 919736047 => 919736047, + 933800054 => 933800054, 981043666 => 981043666, 1011235379 => 1011235379, + 1054793626 => 1054793626, 1066243960 => 1066243960, 1111879441 => 1111879441, + 1137927280 => 1137927280, 1161141390 => 1161141390, 1207494809 => 1207494809, + 1224323748 => 1224323748, 1449283101 => 1449283101, 1476184529 => 1476184529, + 1484497025 => 1484497025, 1502384742 => 1502384742, 1560706915 => 1560706915, + 1643117236 => 1643117236, 1698234817 => 1698234817, 1700075744 => 1700075744, + 1704418040 => 1704418040, 1797315426 => 1797315426, 1827706831 => 1827706831, + 1873827816 => 1873827816, 1880985817 => 1880985817, 1885321445 => 1885321445, + 1887945117 => 1887945117, 1910918422 => 1910918422, 1913396612 => 1913396612, + 1917787717 => 1917787717, 2017893546 => 2017893546, 2023721819 => 2023721819, + 2039230599 => 2039230599, 2047689033 => 2047689033, 2051371828 => 2051371828, + 2113394667 => 2113394667, 2115087454 => 2115087454, 2147483643 => 2147483643, + 2147483644 => 2147483644, 2147483645 => 2147483645, 2147483646 => 2147483646, + 2147483647 => 2147483647, 2147483648 => -2147483648, 2147483649 => -2147483647, + 2147483650 => -2147483646, 2147483651 => -2147483645, 2147483652 => -2147483644, + 327455818446817879 => -98022825, 672496567785689947 => -1148644517, + 792152072093005609 => -785287383, 1158754392966664154 => 649301978, + 1278651530880918028 => -240475636, 3110847094773196354 => 1652762178, + 3413345115981031970 => 1574775330, 3796731187096572787 => 311022451, + 4820534321106058373 => -1122304891, 4896272896071787352 => -440704168, + 5416563045183045367 => 866434807, 5691445208075489224 => 502288328, + 6819073379251083503 => 60119279, 7303323242445241002 => 1506638506, + 7332172857428419953 => -959961743, 7646687339917640626 => 742687666, + 8920184389710306811 => -139796997, 9024321225994333040 => -1771508880, + 9145625486009181568 => -2041174656, 9158246028883747635 => -1522572493, + ]; + $expectedCast = []; + foreach ($actualCast as $i => $value) { + $expectedCast[$i] = Cast::toInt($i); + } + $this->assertEquals($expectedCast, $actualCast); + } + + public function testCastToUnsignedInteger() + { + $actualCast = [ + -8828932113531775224 => 988174088, -7828090724457198174 => 187878818, + -7545005068607356200 => 2166555352, -6180013005281428198 => 820509978, + -6072007252811478348 => 244550324, -5660547352329380296 => 2250317368, + -5565331019887832226 => 2115998558, -4759428743990999011 => 480380957, + -3954477486133169125 => 2529211419, -3655807572739950250 => 3282499926, + -3372368140996826985 => 403318935, -2535801509511858890 => 1554347318, + -2465348119161971711 => 2511100929, -2236391832617430983 => 641781817, + -2216259712569952100 => 2655139996, -1985261835992256688 => 3884625744, + -1403646043161133542 => 1325366810, -1117145770488479569 => 867451055, + -418467636862935386 => 3267993254, -56945642043723349 => 2189046187, + -2147483652 => 2147483644, -2147483651 => 2147483645, -2147483650 => 2147483646, + -2147483649 => 2147483647, -2147483648 => 2147483648, -2147483647 => 2147483649, + -2147483646 => 2147483650, -2147483645 => 2147483651, -2147483644 => 2147483652, + -2147483643 => 2147483653, -2117373025 => 2177594271, -2096041860 => 2198925436, + -2085299236 => 2209668060, -2075027272 => 2219940024, -1840198102 => 2454769194, + -1838509590 => 2456457706, -1761991609 => 2532975687, -1673101190 => 2621866106, + -1639045374 => 2655921922, -1635955752 => 2659011544, -1603451580 => 2691515716, + -1412048907 => 2882918389, -1371267853 => 2923699443, -1356726173 => 2938241123, + -1339647661 => 2955319635, -1238648054 => 3056319242, -1235985228 => 3058982068, + -1053158605 => 3241808691, -1001847353 => 3293119943, -947719476 => 3347247820, + -933098500 => 3361868796, -931941220 => 3363026076, -909764689 => 3385202607, + -903828185 => 3391139111, -902419021 => 3392548275, -889390863 => 3405576433, + -800969358 => 3493997938, -754863798 => 3540103498, -733394875 => 3561572421, + -732264085 => 3562703211, -723061508 => 3571905788, -687630440 => 3607336856, + -656521337 => 3638445959, -641159514 => 3653807782, -574042748 => 3720924548, + -484453139 => 3810514157, -451429548 => 3843537748, -384042562 => 3910924734, + -369278089 => 3925689207, -355941740 => 3939025556, -347870529 => 3947096767, + -337105223 => 3957862073, -324979224 => 3969988072, -186559730 => 4108407566, + -177881990 => 4117085306, -174455223 => 4120512073, -113198032 => 4181769264, + -101455194 => 4193512102, 3247566 => 3247566, 7056894 => 7056894, 37982905 => 37982905, + 102721942 => 102721942, 120367902 => 120367902, 131098802 => 131098802, + 149697898 => 149697898, 175671830 => 175671830, 205852524 => 205852524, + 208289875 => 208289875, 217767742 => 217767742, 459120502 => 459120502, + 574068094 => 574068094, 641095145 => 641095145, 658830419 => 658830419, + 690676300 => 690676300, 701773339 => 701773339, 709110931 => 709110931, + 731722628 => 731722628, 798837210 => 798837210, 819990933 => 819990933, + 912629027 => 912629027, 990890039 => 990890039, 1018131378 => 1018131378, + 1073281498 => 1073281498, 1193197075 => 1193197075, 1209553263 => 1209553263, + 1244583127 => 1244583127, 1262106470 => 1262106470, 1298309267 => 1298309267, + 1302783738 => 1302783738, 1312432823 => 1312432823, 1460631232 => 1460631232, + 1464332686 => 1464332686, 1485792110 => 1485792110, 1497462232 => 1497462232, + 1512577020 => 1512577020, 1602608160 => 1602608160, 1716466972 => 1716466972, + 1739230715 => 1739230715, 1777289703 => 1777289703, 1866315228 => 1866315228, + 1877556734 => 1877556734, 1929669130 => 1929669130, 1959469805 => 1959469805, + 1961146571 => 1961146571, 1966221636 => 1966221636, 1968675697 => 1968675697, + 1970375630 => 1970375630, 1979086003 => 1979086003, 2069784384 => 2069784384, + 2123695038 => 2123695038, 2147483643 => 2147483643, 2147483644 => 2147483644, + 2147483645 => 2147483645, 2147483646 => 2147483646, 2147483647 => 2147483647, + 2147483648 => 2147483648, 2147483649 => 2147483649, 2147483650 => 2147483650, + 2147483651 => 2147483651, 2147483652 => 2147483652, 373886987528618573 => 4123574861, + 771009823256948802 => 1771122754, 1004387722978796655 => 82453615, + 1243209402936707322 => 2071447802, 1655422001120174598 => 170025478, + 1772149229859146775 => 3602680855, 2507262907742073252 => 2658840996, + 2813363649054078496 => 1037923872, 2876534798038013405 => 2233144797, + 3297680158231392904 => 3460067976, 3328118166197267052 => 3400952428, + 3572321990398647028 => 1560746740, 4149946872460889004 => 1908820908, + 5788410328610570922 => 2332623530, 5825079135471215377 => 1917657873, + 6555518071408694953 => 217585321, 6739408486548733086 => 3304946846, + 7079040899421182461 => 276175357, 8145391911457264336 => 2394952400, + 9147936191948800226 => 2903261410, + ]; + $expectedCast = []; + foreach ($actualCast as $i => $value) { + $expectedCast[$i] = Cast::toUnsignedInt($i); + } + $this->assertEquals($expectedCast, $actualCast); + } + + public function testCastToLong() + { + $actualCast = [ + -9170085652559072218 => -9170085652559072218, + -8762875851716154430 => -8762875851716154430, + -8412777414798920406 => -8412777414798920406, + -8411232067770162241 => -8411232067770162241, + -7956962034042337916 => -7956962034042337916, + -7741733997731069659 => -7741733997731069659, + -7562413780647168309 => -7562413780647168309, + -7279532752286714404 => -7279532752286714404, + -6875053898264821215 => -6875053898264821215, + -6546188122974899677 => -6546188122974899677, + -6539319718043370422 => -6539319718043370422, + -6269360680735574015 => -6269360680735574015, + -6245212573735285524 => -6245212573735285524, + -6008164560998165202 => -6008164560998165202, + -5673130211365790481 => -5673130211365790481, + -5550558879395416461 => -5550558879395416461, + -5315545680575876829 => -5315545680575876829, + -5270629292889039812 => -5270629292889039812, + -5257244755715615860 => -5257244755715615860, + -4697072760722166619 => -4697072760722166619, + -4378266633934162190 => -4378266633934162190, + -4351003675881447778 => -4351003675881447778, + -4344868801464432933 => -4344868801464432933, + -4043702893244004445 => -4043702893244004445, + -3481799324128556516 => -3481799324128556516, + -3401006296811244597 => -3401006296811244597, + -3310806867794682093 => -3310806867794682093, + -3057425529762577563 => -3057425529762577563, + -3040853836146852724 => -3040853836146852724, + -2817295230844194271 => -2817295230844194271, + -2568471746216522125 => -2568471746216522125, + -2519307567020828553 => -2519307567020828553, + -1975798901698214275 => -1975798901698214275, + -1859083390560038307 => -1859083390560038307, + -1774553370580724902 => -1774553370580724902, + -1634855409015608429 => -1634855409015608429, + -1381951605941422786 => -1381951605941422786, + -1196959685645247832 => -1196959685645247832, + -1036253099502184090 => -1036253099502184090, + -914626182071417150 => -914626182071417150, + -912583761313393119 => -912583761313393119, + -756029432352135202 => -756029432352135202, + -669177640404047297 => -669177640404047297, + -279752108241877035 => -279752108241877035, + -14037444279643280 => -14037444279643280, + 89235183761627361 => 89235183761627361, + 98272798287346379 => 98272798287346379, + 341611274837543729 => 341611274837543729, + 617025623621229439 => 617025623621229439, + 688607913671564955 => 688607913671564955, + 700601410549691241 => 700601410549691241, + 700727038457061037 => 700727038457061037, + 816476720065344870 => 816476720065344870, + 955192676500220111 => 955192676500220111, + 1214033201966406170 => 1214033201966406170, + 1414590019617251171 => 1414590019617251171, + 1528545757917024358 => 1528545757917024358, + 1614326684909925062 => 1614326684909925062, + 1944672876122505798 => 1944672876122505798, + 2207144148508699333 => 2207144148508699333, + 2278302445836788036 => 2278302445836788036, + 2384226760042317776 => 2384226760042317776, + 2573344486686176471 => 2573344486686176471, + 2616173938058851058 => 2616173938058851058, + 2805044067171596076 => 2805044067171596076, + 2894299281119165483 => 2894299281119165483, + 3063062202059229841 => 3063062202059229841, + 3466271178310710564 => 3466271178310710564, + 3632373078879533041 => 3632373078879533041, + 3642290790028469551 => 3642290790028469551, + 3662343508920772932 => 3662343508920772932, + 3769065765930590922 => 3769065765930590922, + 4256964567272000930 => 4256964567272000930, + 4263913004739735112 => 4263913004739735112, + 4526517888199137475 => 4526517888199137475, + 4649412930539981605 => 4649412930539981605, + 4733367622092357447 => 4733367622092357447, + 4907150005690517596 => 4907150005690517596, + 5037687856571754869 => 5037687856571754869, + 5204427615109238084 => 5204427615109238084, + 5480343987422738941 => 5480343987422738941, + 5501650918319918819 => 5501650918319918819, + 5664389307290364572 => 5664389307290364572, + 5680136290368167730 => 5680136290368167730, + 5741424946854193175 => 5741424946854193175, + 6043965460032981040 => 6043965460032981040, + 6203696150695101213 => 6203696150695101213, + 6350747862140861316 => 6350747862140861316, + 6974846481983904529 => 6974846481983904529, + 7028264173177402809 => 7028264173177402809, + 7311629905623817713 => 7311629905623817713, + 7595125476648712557 => 7595125476648712557, + 7638473695518713044 => 7638473695518713044, + 7720855249472985748 => 7720855249472985748, + 7966241693142082724 => 7966241693142082724, + 8110180085178212388 => 8110180085178212388, + 8684182726111037256 => 8684182726111037256, + 8842898155976061943 => 8842898155976061943, + 8882296136384501261 => 8882296136384501261, + 8989594683105135694 => 8989594683105135694, + 9223372036854775803 => 9223372036854775803, + 9223372036854775804 => 9223372036854775804, + 9223372036854775805 => 9223372036854775805, + 9223372036854775806 => 9223372036854775806, + 9223372036854775807 => 9223372036854775807, + ]; + $expectedCast = []; + foreach ($actualCast as $i => $value) { + $expectedCast[$i] = Cast::toLong($i); + } + $this->assertEquals($expectedCast, $actualCast); + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/FileBufferTest.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/FileBufferTest.php new file mode 100644 index 0000000000..c10d300701 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/FileBufferTest.php @@ -0,0 +1,42 @@ +outputFilename = tempnam(sys_get_temp_dir(), 'temp'); + parent::setUp(); + } + + /** + * After test + */ + protected function tearDown() + { + parent::tearDown(); + + if ($this->outputFilename !== null && file_exists($this->outputFilename)) { + unlink($this->outputFilename); + } + } + + /** + * @return Buffer + * @throws BufferException + */ + protected function createBuffer() + { + return new FileBuffer($this->outputFilename); + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/GetInfoIcoFileTest.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/GetInfoIcoFileTest.php new file mode 100644 index 0000000000..f944ae94c0 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/GetInfoIcoFileTest.php @@ -0,0 +1,51 @@ +setReadOnly(true); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + + // ico header + $this->assertEquals($buffer->getShort(), 0); // reserved + $type = $buffer->getShort(); + $this->assertTrue($type === 1 || $type === 2); // type icon + $count = $buffer->getShort(); + $this->assertTrue($count > 0); // count images + + // image directory + for ($i = 0; $i < $count; $i++) { + $width = $buffer->getByte(); + $height = $buffer->getByte(); + $colors = $buffer->getByte(); + $this->assertEquals($buffer->getByte(), 0); // reserved + $planes = $buffer->getShort(); + $bpp = $buffer->getShort(); + $size = $buffer->getInt(); + $offset = $buffer->getInt(); + + $buffer->setPosition($offset + $size); + $this->assertFalse($buffer->hasRemaining()); + + $this->assertEquals($width, 16); + $this->assertEquals($height, 16); + $this->assertEquals($colors, 0); + $this->assertEquals($planes, 1); + $this->assertEquals($bpp, 32); + } + $buffer->close(); + } +} diff --git a/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/MemoryResourceBufferTest.php b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/MemoryResourceBufferTest.php new file mode 100644 index 0000000000..8f77c11784 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/nelexa/buffer/tests/Nelexa/Buffer/MemoryResourceBufferTest.php @@ -0,0 +1,16 @@ +RWL#bjCM9$1atVX^m(5QI1s>NXCP(ch(Bs<)oop;UjZjK19S187hEr~W>3 zrO!V~EmB5b&i_N^Qt(VaBe(UtqhFcanRjWj^SXXHgY@kF1l;(?|M$*znczklRR)$m z-QMII_REA8WVe2Brui$V*Ph_K__m1Bt!+vUg_M{KD`~gt(SsOfa zuvo{v^^k7XyUWJg`-r)ZFkP}|<_!EA_8n7dszq-foYY7oo|Zwi)T+dyn?hFu%kgr^ zczcG7uY1h+&hVzV7rh#(Euj`mu37HbD%~Nn8@1Y8PJy`EoVHDOE k`QO6qXBdE;z<-v^{k~fZr6=>}rm&-%x4x&xrBJi^AARhwQ2+n{ literal 0 HcmV?d00001 diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitattributes b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitattributes new file mode 100644 index 0000000000..3999c7ad72 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitattributes @@ -0,0 +1,5 @@ +/composer.lock export-ignore +/generate-coverage.sh export-ignore +/phpstan.neon export-ignore +/phpunit.xml.dist export-ignore +/test/ export-ignore diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitignore b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitignore new file mode 100644 index 0000000000..6692847596 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/.gitignore @@ -0,0 +1,4 @@ +/coverage/ +/vendor/ +.phpunit.cache/ +.DS_Store diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/LICENSE b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/LICENSE new file mode 100644 index 0000000000..041d492a6c --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 WoltLab GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/README.md b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/README.md new file mode 100644 index 0000000000..fefacf0ecc --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/README.md @@ -0,0 +1,39 @@ +WebP Encoder/Decoder and EXIF Extractor +======================================= + +This library can decode WebP images to extract data, for example, EXIF and XMP +chunks. The deconstructed image can be modified and then enconded as a WebP +image again, using the most efficient format. + +The most notable feature is that this library is able to parse EXIF data +embedded in WebP images. PHP’s `exif_read_data()` function is limited to JPEG +images and to work around this limitation the EXIF chunk offers the ability to +inject the extracted EXIF data in a small host JPEG to process it with PHP. + +Usage +----- + +```php +fromBinary($binary); +if ($webp->getExif() !== null) { + // Extract the EXIF data to store it separate from the file. + $exifData = $webp->getExif()->getRawBytes(); + // … or just retrieve the parsed EXIF data. + \var_dump($webp->getExif()->getParsedExif()); + + // Strip the EXIF data. + $webp->withExif(null); +} + +$encoder = new Encoder(); +$binary = $encoder->fromWebP($webp); +``` + +The decoder performs a strict validation of the input data and will throw an +exception when it encounters any violations. If you do not care about the +actual problem, you can catch the generic interface `WebpExifException` that +is implemented by all exceptions. diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/composer.json b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/composer.json new file mode 100644 index 0000000000..236331ad90 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/composer.json @@ -0,0 +1,44 @@ +{ + "name": "woltlab/webp-exif", + "description": "Extract and embed EXIF metadata from and to WebP images", + "type": "library", + "license": "MIT", + "keywords": [ + "exif", + "webp" + ], + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "ext-exif": "*", + "nelexa/buffer": "^1.3", + "symfony/polyfill-php84": "^1.31" + }, + "require-dev": { + "phpunit/phpunit": "^12.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/extension-installer": "^1.4", + "phpunit/php-code-coverage": "^12.0" + }, + "autoload": { + "psr-4": { + "WoltLab\\WebpExif\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "WoltLabTest\\WebpExif\\": "test/" + } + }, + "scripts": { + "test": "phpunit --colors=always test", + "phpstan": "phpstan" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Alph.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Alph.php new file mode 100644 index 0000000000..49f8727e8a --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Alph.php @@ -0,0 +1,23 @@ + + */ +final class Alph extends Chunk +{ + private function __construct(int $offset, string $data) + { + parent::__construct("ALPH", $offset, $data); + } + + public static function forBytes(int $offset, string $bytes): self + { + return new Alph($offset, $bytes); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anim.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anim.php new file mode 100644 index 0000000000..0470d13794 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anim.php @@ -0,0 +1,23 @@ + + */ +final class Anim extends Chunk +{ + private function __construct(int $offset, string $data) + { + parent::__construct("ANIM", $offset, $data); + } + + public static function forBytes(int $offset, string $bytes): self + { + return new Anim($offset, $bytes); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anmf.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anmf.php new file mode 100644 index 0000000000..41ef78371b --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Anmf.php @@ -0,0 +1,115 @@ + + */ +final class Anmf extends Chunk +{ + /** @param list $chunks */ + private function __construct( + int $offset, + string $data, + private readonly array $chunks + ) { + parent::__construct("ANMF", $offset, $data); + } + + #[Override] + public function getLength(): int + { + return \array_reduce( + $this->chunks, + static function ($acc, $chunk) { + $paddingByte = $chunk->getLength() % 2; + return $acc + $chunk->getLength() + $paddingByte + 8; + }, + parent::getLength(), + ); + } + + /** + * @return list + */ + public function getDataChunks(): array + { + return $this->chunks; + } + + public static function fromBuffer(Buffer $buffer): self + { + $offset = $buffer->position(); + $length = $buffer->getUnsignedInt(); + if ($length > $buffer->remaining()) { + throw new LengthOutOfBounds($length, $offset, $buffer->remaining()); + } + + // An animation frame contains at least 16 bytes for the header. + if ($buffer->remaining() < 16) { + throw new UnexpectedEndOfFile($buffer->position(), $buffer->remaining()); + } + + // The next 8 bytes contain the X and Y coordinates, as well as the + // frame witdth and height. Afterwards there are 3 bytes for the frame + // duration followed by 1 byte representing a bit field. (= 16 bytes) + $frameHeader = $buffer->getString(16); + + $chunks = []; + $decoder = new Decoder(); + while ($buffer->position() < $offset + 4 + $length) { + $chunks[] = $decoder->decodeChunk($buffer); + } + + self::validateChunks($offset, $chunks); + + return new Anmf($offset, $frameHeader, $chunks); + } + + /** + * @param list $chunks + */ + private static function validateChunks(int $offset, array $chunks): void + { + if ($chunks === []) { + throw new EmptyAnimationFrame($offset); + } + + // An ALPH chunk can only appear at the start of the frame data. + if ($chunks[0] instanceof Alph) { + \array_shift($chunks); + + if ($chunks === []) { + throw new AnimationFrameWithoutBitstream($offset); + } + } + + // A bitstream chunk can only appear at the first position or after an + // ALPH chunk. + if ($chunks[0] instanceof Vp8 || $chunks[0] instanceof Vp8l) { + \array_shift($chunks); + } else { + throw new AnimationFrameWithoutBitstream($offset); + } + + // After the bitstream chunk there may be an infinite number of unknown + // chunks, but no known ones. + $disallowedChunk = \array_find($chunks, static fn($chunk) => !($chunk instanceof UnknownChunk)); + if ($disallowedChunk instanceof Chunk) { + throw new UnexpectedChunk($disallowedChunk->getFourCC(), $disallowedChunk->getOffset()); + } + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Chunk.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Chunk.php new file mode 100644 index 0000000000..03e4915f3f --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Chunk.php @@ -0,0 +1,39 @@ + + */ +abstract class Chunk +{ + protected function __construct( + private readonly string $fourCC, + private readonly int $offset, + private readonly string $data, + ) {} + + public function getFourCC(): string + { + return $this->fourCC; + } + + public function getLength(): int + { + return \strlen($this->data); + } + + public function getOffset(): int + { + return $this->offset; + } + + public function getRawBytes(): string + { + return $this->data; + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/AnimationFrameWithoutBitstream.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/AnimationFrameWithoutBitstream.php new file mode 100644 index 0000000000..b0ca6705d4 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/AnimationFrameWithoutBitstream.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +final class AnimationFrameWithoutBitstream extends RuntimeException implements WebpExifException +{ + public function __construct(int $offset) + { + $offset = \dechex($offset); + parent::__construct("The ANMF frame at offset 0x{$offset} does not contain a bitstream chunk"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/DimensionsExceedInt32.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/DimensionsExceedInt32.php new file mode 100644 index 0000000000..71ce47ed50 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/DimensionsExceedInt32.php @@ -0,0 +1,23 @@ + + * + * @internal + */ +final class DimensionsExceedInt32 extends OutOfRangeException implements WebpExifException +{ + public function __construct(int $width, int $height) + { + parent::__construct("The product of {$width} and {$height} exceeds the boundary of 2^31 - 1"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/EmptyAnimationFrame.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/EmptyAnimationFrame.php new file mode 100644 index 0000000000..90a19d349a --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/EmptyAnimationFrame.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +final class EmptyAnimationFrame extends RuntimeException implements WebpExifException +{ + public function __construct(int $offset) + { + $offset = \dechex($offset); + parent::__construct("The ANMF frame at offset 0x{$offset} contains no chunks"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/ExpectedKeyFrame.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/ExpectedKeyFrame.php new file mode 100644 index 0000000000..d145ed4d73 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/ExpectedKeyFrame.php @@ -0,0 +1,23 @@ + + * + * @internal + */ +final class ExpectedKeyFrame extends RuntimeException implements WebpExifException +{ + public function __construct() + { + parent::__construct("Expected a keyframe to be the first frame"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingExifExtension.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingExifExtension.php new file mode 100644 index 0000000000..f675160691 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingExifExtension.php @@ -0,0 +1,26 @@ + + * + * @internal + */ +final class MissingExifExtension extends RuntimeException implements WebpExifException +{ + /** + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct("The `php_exif` extension is required to parse EXIF data"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingMagicByte.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingMagicByte.php new file mode 100644 index 0000000000..f03a7fca55 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/MissingMagicByte.php @@ -0,0 +1,23 @@ + + * + * @internal + */ +final class MissingMagicByte extends RuntimeException implements WebpExifException +{ + public function __construct(string $fourCC) + { + parent::__construct("The data for `{$fourCC}` is missing the magic byte"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnknownChunkWithKnownFourCC.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnknownChunkWithKnownFourCC.php new file mode 100644 index 0000000000..3a2189cd80 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnknownChunkWithKnownFourCC.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +final class UnknownChunkWithKnownFourCC extends RuntimeException implements WebpExifException +{ + public function __construct(string $fourCC) + { + parent::__construct("The FourCC code `{$fourCC}` is well-known and must not be used with " . UnknownChunk::class); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnsupportedVersion.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnsupportedVersion.php new file mode 100644 index 0000000000..cccf00e6a2 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exception/UnsupportedVersion.php @@ -0,0 +1,23 @@ + + * + * @internal + */ +final class UnsupportedVersion extends RuntimeException implements WebpExifException +{ + public function __construct(string $fourCC, int $found, int $expected) + { + parent::__construct("Expected version `{$expected}` for `{$fourCC}` but found `{$found}`"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exif.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exif.php new file mode 100644 index 0000000000..76dc16c50e --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Exif.php @@ -0,0 +1,85 @@ + + */ +final class Exif extends Chunk +{ + private function __construct(int $offset, string $data) + { + parent::__construct("EXIF", $offset, $data); + } + + /** + * @return null|array + * @throws MissingExifExtension + */ + public function getParsedExif(): ?array + { + $bytes = $this->getRawBytes(); + if (\strlen($bytes) === 0) { + return null; + } + + // We're offloading the EXIF decoding task for `exif_read_data()` which + // cannot process WebP. + if (!\function_exists('exif_read_data')) { + // @codeCoverageIgnoreStart + throw new MissingExifExtension(); + // @codeCoverageIgnoreEnd + } + + // A tiny JPEG is used as the host for the EXIF data. + // See https://github.com/mathiasbynens/small/blob/267b39f682598eebb0dafe7590b1504be79b5cad/jpeg.jpg + // This is a modified version without the leading 0xFF 0xD8 (SOI)! + $jpegBody = "\xFF\xDB\x00\x43\x00\x03\x02\x02\x02\x02\x02\x03\x02\x02\x02\x03\x03\x03\x03\x04\x06\x04\x04\x04\x04\x04\x08\x06\x06\x05\x06\x09\x08\x0A\x0A\x09\x08\x09\x09\x0A\x0C\x0F\x0C\x0A\x0B\x0E\x0B\x09\x09\x0D\x11\x0D\x0E\x0F\x10\x10\x11\x10\x0A\x0C\x12\x13\x12\x10\x13\x0F\x10\x10\x10\xFF\xC9\x00\x0B\x08\x00\x01\x00\x01\x01\x01\x11\x00\xFF\xCC\x00\x06\x00\x10\x10\x05\xFF\xDA\x00\x08\x01\x01\x00\x00\x3F\x00\xD2\xCF\x20\xFF\xD9"; + + // The image does not have a JFIF tag and instead directly starts with + // the quantization table (DQT, 0xFF xDB). The SOI (start of image, 0xFF + // 0xD8) is prepended below to simpify the construction of the image. + $soiTag = "\xFF\xD8"; + + $app1Tag = "\xFF\xE1"; + $exifHeader = "\x45\x78\x69\x66\x00\x00"; + + // The byte length is the length of the EXIF header (6), the payload and + // the two bytes for the length itself. + $byteLength = \pack("n", 2 + 6 + \strlen($bytes)); + + // We must suppress warnings here to gracefully recover from bad EXIF data. + $exif = @\exif_read_data( + \sprintf( + "data://image/jpeg;base64,%s", + \base64_encode( + $soiTag . + $app1Tag . + $byteLength . + $exifHeader . + $bytes . + $jpegBody + ), + ), + ); + + // There is no known case where the call to `\exif_read_data()` can fail + // hard, there may be warnings about garbage data but since the + // constructed host image is guaranteed to be valid, it is infallible. + \assert($exif !== false); + + /** @var array $exif */ + return $exif; + } + + public static function forBytes(int $offset, string $bytes): self + { + return new Exif($offset, $bytes); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Iccp.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Iccp.php new file mode 100644 index 0000000000..37d2e91b20 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Iccp.php @@ -0,0 +1,23 @@ + + */ +final class Iccp extends Chunk +{ + private function __construct(int $offset, string $data) + { + parent::__construct("ICCP", $offset, $data); + } + + public static function forBytes(int $offset, string $bytes): self + { + return new Iccp($offset, $bytes); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/UnknownChunk.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/UnknownChunk.php new file mode 100644 index 0000000000..419ffdc369 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/UnknownChunk.php @@ -0,0 +1,25 @@ + + */ +final class UnknownChunk extends Chunk +{ + public static function forBytes(string $fourCC, int $offset, string $bytes): self + { + if (ChunkType::fromFourCC($fourCC) !== ChunkType::UnknownChunk) { + throw new UnknownChunkWithKnownFourCC($fourCC); + } + + return new UnknownChunk($fourCC, $offset, $bytes); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8.php new file mode 100644 index 0000000000..c88c50c3b0 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8.php @@ -0,0 +1,66 @@ + + */ +final class Vp8 extends Chunk +{ + private function __construct( + public readonly int $width, + public readonly int $height, + int $offset, + string $data, + ) { + parent::__construct("VP8 ", $offset, $data); + } + + public static function fromBuffer(Buffer $buffer): self + { + $length = $buffer->getUnsignedInt(); + $startOfData = $buffer->position(); + if ($length > $buffer->remaining()) { + throw new LengthOutOfBounds($length, $buffer->position(), $buffer->remaining()); + } + + $tag = $buffer->getUnsignedByte(); + + // We expect the first frame to be a keyframe. + $frameType = $tag & 1; + if ($frameType !== 0) { + throw new ExpectedKeyFrame(); + } + + // Skip the next two bytes, they are part of the header but do not + // contain any information that is relevant to us. + $buffer->skip(2); + + // Keyframes must start with 3 magic bytes. + $marker = $buffer->getString(3); + if ($marker !== "\x9D\x01\x2A") { + throw new MissingMagicByte("VP8"); + } + + // The width and height are encoded using 2 bytes each. However, the + // first two bits are the scale followed by 14 bits for the dimension. + $width = $buffer->getUnsignedShort() & 0x3FFF; + $height = $buffer->getUnsignedShort() & 0x3FFF; + + return new Vp8( + $width, + $height, + $startOfData - 8, + $buffer->setPosition($startOfData)->getString($length) + ); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8l.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8l.php new file mode 100644 index 0000000000..7d2171f866 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8l.php @@ -0,0 +1,63 @@ + + */ +final class Vp8l extends Chunk +{ + private function __construct( + public readonly int $width, + public readonly int $height, + int $offset, + string $data, + ) { + parent::__construct("VP8L", $offset, $data); + } + + public static function fromBuffer(Buffer $buffer): self + { + $length = $buffer->getUnsignedInt(); + $startOfData = $buffer->position(); + if ($length > $buffer->remaining()) { + throw new LengthOutOfBounds($length, $buffer->position(), $buffer->remaining()); + } + + $signature = $buffer->getUnsignedByte(); + if ($signature !== 0x2F) { + throw new MissingMagicByte("VP8L"); + } + + $header = $buffer->getUnsignedInt(); + + // The header contains the following data: + // 0-13: width - 1 + // 14-27: height - 1 + // 28: alpha_is_used + // 29-31: version (must be 0) + $version = $header >> 29; + if ($version !== 0) { + throw new UnsupportedVersion("VP8L", $version, 0); + } + + $width = ($header & 0x3FFF) + 1; + $height = (($header >> 14) & 0x3FFF) + 1; + + return new Vp8l( + $width, + $height, + $startOfData - 8, + $buffer->setPosition($startOfData)->getString($length) + ); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8x.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8x.php new file mode 100644 index 0000000000..5e46318d40 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Vp8x.php @@ -0,0 +1,227 @@ + + */ +final class Vp8x extends Chunk +{ + private function __construct( + public readonly int $width, + public readonly int $height, + int $offset, + public readonly bool $iccProfile, + public readonly bool $alpha, + public readonly bool $exif, + public readonly bool $xmp, + public readonly bool $animation, + ) { + parent::__construct( + "VP8X", + $offset, + // VP8X only contains the header because the actual payload are the + // chunks that follow afterwards. + "" + ); + } + + /** + * Filters the list of chunks and validates them against the specificiations + * for VP8X images. + * + * @param list $chunks + * @return list + */ + public function filterChunks(array $chunks): array + { + if ($chunks === []) { + throw new Vp8xWithoutChunks(); + } + + $nestedVp8x = \array_find($chunks, static fn($chunk) => $chunk instanceof Vp8x); + if ($nestedVp8x !== null) { + throw new ExtraVp8xChunk(); + } + + if ($this->iccProfile) { + $hasIccProfile = self::removeExtraChunks(Iccp::class, $chunks); + if (!$hasIccProfile) { + throw new Vp8xAbsentChunk("ICCP"); + } + } + + if ($this->alpha) { + $hasAlpha = self::removeExtraChunks(Alph::class, $chunks); + if (!$hasAlpha) { + throw new Vp8xAbsentChunk("ALPH"); + } + } + + if ($this->exif) { + $hasExif = self::removeExtraChunks(Exif::class, $chunks); + if (!$hasExif) { + throw new Vp8xAbsentChunk("EXIF"); + } + } + + if ($this->xmp) { + $hasXmp = self::removeExtraChunks(Xmp::class, $chunks); + if (!$hasXmp) { + throw new Vp8xAbsentChunk("XMP "); + } + } + + $frames = \array_filter( + $chunks, + static fn($chunk) => $chunk instanceof Anmf + ); + $bitstreams = \array_filter( + $chunks, + static fn($chunk) => ($chunk instanceof Vp8) || ($chunk instanceof Vp8l) + ); + + // The VP8X chunk must contain image data that can come in two flavors: + // 1. Still images must contain either a VP8 or VP8L chunk. + // 2. Animated images must contain multiple frames. + if ($this->animation) { + if (\count($frames) < 2) { + throw new Vp8xMissingImageData(stillImage: false); + } + + $bitstream = \array_shift($bitstreams); + if ($bitstream !== null) { + throw new UnexpectedChunk($bitstream->getFourCC(), $bitstream->getOffset()); + } + } else { + if (\count($bitstreams) !== 1) { + throw new Vp8xMissingImageData(stillImage: true); + } + + $frame = \array_shift($frames); + if ($frame !== null) { + throw new UnexpectedChunk($frame->getFourCC(), $frame->getOffset()); + } + } + + return $chunks; + } + + /** + * Removes all chunks sharing the same class except for the first + * occurrence. Returns true if at least one such chunk was found. + * + * @param class-string $className + * @param list $chunks + */ + private function removeExtraChunks(string $className, array &$chunks): bool + { + $hasChunk = false; + $chunks = \array_values(\array_filter($chunks, static function ($chunk) use ($className, &$hasChunk) { + if (!($chunk instanceof $className)) { + return true; + } + + if (!$hasChunk) { + $hasChunk = true; + return true; + } + + return false; + })); + + return $hasChunk; + } + + public static function fromBuffer(Buffer $buffer): self + { + // The next 4 bytes represent the length of the VP8X header which must + // be 10 bytes long. + $expectedHeaderLength = 10; + $length = $buffer->getUnsignedInt(); + if ($length !== $expectedHeaderLength) { + throw new Vp8xHeaderLengthMismatch($expectedHeaderLength, $length); + } + + $startOfData = $buffer->position(); + + // The next byte contains a bit field for the features of this image, + // the first two bits and the last bit are reserved and MUST be ignored. + $bitField = $buffer->getByte(); + $iccProfile = ($bitField & 0b00100000) === 32; + $alpha = ($bitField & 0b00010000) === 16; + $exif = ($bitField & 0b00001000) === 8; + $xmp = ($bitField & 0b00000100) === 4; + $animation = ($bitField & 0b00000010) === 2; + + // The next 24 bits are reserved. + $buffer->skip(3); + + // The width of the canvas is represented as a uint24LE but minus one, + // therefore we have to add 1 when decoding the value. + $width = self::decodeDimension($buffer); + + // The height follows the same rules as the width. + $height = self::decodeDimension($buffer); + + // The product of `width` and `height` must not exceed 2^31 - 1, the + // maximum value of int32. We cannot assume that PHP is a 64 bit build + // therefore we calculate the largest possible value for `$height` that + // would not exceed the maximum value. This approach avoids hitting an + // integer overflow on 32 bit builds when calculating the product. + $maxInt32 = 2_147_483_647; + $maximumHeight = $maxInt32 / $width; + if ($maximumHeight < $height) { + throw new DimensionsExceedInt32($width, $height); + } + + return new Vp8x( + $width, + $height, + $startOfData - 8, + $iccProfile, + $alpha, + $exif, + $xmp, + $animation + ); + } + + /** + * @internal + */ + public static function fromParameters( + int $offset, + int $width, + int $height, + bool $iccProfile, + bool $alpha, + bool $exif, + bool $xmp, + bool $animation + ): self { + return new Vp8x($width, $height, $offset, $iccProfile, $alpha, $exif, $xmp, $animation); + } + + private static function decodeDimension(Buffer $buffer): int + { + $a = $buffer->getUnsignedByte(); + $b = $buffer->getUnsignedByte(); + $c = $buffer->getUnsignedByte(); + + return ($a | ($b << 8) | ($c << 16)) + 1; + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Xmp.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Xmp.php new file mode 100644 index 0000000000..7864c24290 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Chunk/Xmp.php @@ -0,0 +1,23 @@ + + */ +final class Xmp extends Chunk +{ + private function __construct(int $offset, string $data) + { + parent::__construct("XMP ", $offset, $data); + } + + public static function forBytes(int $offset, string $bytes): self + { + return new Xmp($offset, $bytes); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/ChunkType.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/ChunkType.php new file mode 100644 index 0000000000..9d9e83e6ec --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/ChunkType.php @@ -0,0 +1,42 @@ + + */ +enum ChunkType +{ + case ALPH; + case ANIM; + case ANMF; + case EXIF; + case ICCP; + case VP8; + case VP8L; + case VP8X; + case XMP; + case UnknownChunk; + + public static function fromFourCC(string $fourCC): self + { + return match ($fourCC) { + "ALPH" => self::ALPH, + "ANIM" => self::ANIM, + "ANMF" => self::ANMF, + "EXIF" => self::EXIF, + "ICCP" => self::ICCP, + "VP8 " => self::VP8, + "VP8L" => self::VP8L, + "VP8X" => self::VP8X, + "XMP " => self::XMP, + default => self::UnknownChunk, + }; + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Decoder.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Decoder.php new file mode 100644 index 0000000000..86209cb266 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Decoder.php @@ -0,0 +1,121 @@ + + */ +final class Decoder +{ + public function fromBinary(string $binary): WebP + { + $buffer = new StringBuffer($binary); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + $buffer->setReadOnly(true); + + // A RIFF container at its minimum contains the "RIFF" header, a + // uint32LE representing the chunk size, the "WEBP" type and the data + // section. The data section of a WebP at minimum contains one chunk + // (header + uint32LE + data). + // + // The shortest possible WebP image is a simple VP8L container that + // contains only the magic byte, a uint32 for the flags and dimensions, + // and at last a single byte of data. This takes up 26 bytes in total. + $expectedMinimumFileSize = 26; + if ($buffer->size() < $expectedMinimumFileSize) { + throw new NotEnoughData($expectedMinimumFileSize, $buffer->size()); + } + + $riff = $buffer->getString(4); + $length = $buffer->getUnsignedInt(); + $format = $buffer->getString(4); + if ($riff !== 'RIFF' || $format !== 'WEBP') { + throw new UnrecognizedFileFormat(); + } + + // The length in the header does not include "RIFF" and the length + // itself. It must therefore be exactly 8 bytes shorter than the total + // size. + $actualLength = $buffer->size() - 8; + if ($length !== $actualLength) { + throw new FileSizeMismatch($length, $actualLength); + } + + /** @var list */ + $chunks = []; + do { + $chunks[] = $this->decodeChunk($buffer); + } while ($buffer->hasRemaining()); + + return WebP::fromChunks($chunks); + } + + /** + * @internal + */ + public function decodeChunk(Buffer $buffer): Chunk + { + $remainingBytes = $buffer->remaining(); + if ($remainingBytes < 8) { + throw new UnexpectedEndOfFile($buffer->position(), $buffer->remaining()); + } + + $chunkPosition = $buffer->position(); + $fourCC = $buffer->getString(4); + $originalOffset = $buffer->position(); + $length = $buffer->getUnsignedInt(); + if ($buffer->remaining() < $length) { + throw new LengthOutOfBounds($length, $originalOffset, $buffer->remaining()); + } + + $chunk = match (ChunkType::fromFourCC($fourCC)) { + ChunkType::ALPH => Alph::forBytes($chunkPosition, $buffer->getString($length)), + ChunkType::ANIM => Anim::forBytes($chunkPosition, $buffer->getString($length)), + ChunkType::ANMF => Anmf::fromBuffer($buffer->setPosition($originalOffset)), + ChunkType::EXIF => Exif::forBytes($chunkPosition, $buffer->getString($length)), + ChunkType::ICCP => Iccp::forBytes($chunkPosition, $buffer->getString($length)), + ChunkType::XMP => Xmp::forBytes($chunkPosition, $buffer->getString($length)), + default => UnknownChunk::forBytes($fourCC, $chunkPosition, $buffer->getString($length)), + + // VP8, VP8L and VP8X are a bit different because these need to be + // able to evaluate the length of the chunk themselves for various + // reasons. + ChunkType::VP8 => Vp8::fromBuffer($buffer->setPosition($originalOffset)), + ChunkType::VP8L => Vp8l::fromBuffer($buffer->setPosition($originalOffset)), + ChunkType::VP8X => Vp8x::fromBuffer($buffer->setPosition($originalOffset)), + }; + + // The length of every chunk in a RIFF container must be of an even + // length. Uneven chunks must be padded by a single 0x00 at the end. + if ($length % 2 === 1) { + $buffer->setPosition($buffer->position() + 1); + } + + return $chunk; + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Encoder.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Encoder.php new file mode 100644 index 0000000000..f2d92d838b --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Encoder.php @@ -0,0 +1,179 @@ + + */ +final class Encoder +{ + /** + * Encodes a `WebP` object into the simple or extended format depending on + * the contained chunks. + * + * @return string raw bytes + */ + public function fromWebP(WebP $webp): string + { + if ($webp->containsOnlyBitstream()) { + return $this->toSimpleFormat($webp); + } + + return $this->toExtendedFileFormat($webp); + } + + private function toSimpleFormat(WebP $webp): string + { + $chunks = $webp->getChunks(); + \assert(\count($chunks) === 1); + $bitstream = $chunks[0]; + + $buffer = new StringBuffer(); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + + $buffer->insertString("RIFF"); + + // The header length does include neither "RIFF" nor the length itself. + $riffHeaderLength = 4; + $chunkHeader = 8; + $buffer->insertInt($riffHeaderLength + $chunkHeader + $bitstream->getLength()); + + $buffer->insertString("WEBP"); + + $buffer->insertString($bitstream->getFourCC()); + $buffer->insertInt($bitstream->getLength()); + $buffer->insertString($bitstream->getRawBytes()); + + if (\strlen($bitstream->getRawBytes()) % 2 === 1) { + // The padding byte is not part of the RIFF length. + $buffer->insertByte(0); + } + + return $buffer->toString(); + } + + private function toExtendedFileFormat(WebP $webp): string + { + $buffer = new StringBuffer(); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + + $buffer->insertString("RIFF"); + // Notice: The length will be inserted here at the end. + $buffer->insertString("WEBP"); + + $buffer->insertString("VP8X"); + // The VP8X header has a fixed length of 10 bytes. + $buffer->insertInt(10); + $this->encodeExtendedFormatFeatures($webp, $buffer); + $buffer->insertString("\x00\x00\x00"); + $this->encodeDimensions($webp, $buffer); + + $iccp = $webp->getIccProfile(); + if ($iccp !== null) { + $this->writeChunk($iccp, $buffer); + } + + $anim = $webp->getAnimation(); + if ($anim !== null) { + $this->writeChunk($anim, $buffer); + + $frames = $webp->getAnimationFrames(); + \assert(\count($frames) !== 0); + + foreach ($frames as $frame) { + $this->writeChunk($frame, $buffer); + + foreach ($frame->getDataChunks() as $dataChunk) { + $this->writeChunk($dataChunk, $buffer); + } + } + } else { + $alph = $webp->getAlpha(); + if ($alph !== null) { + $this->writeChunk($alph, $buffer); + } + + $bitstream = $webp->getBitstream(); + assert($bitstream !== null); + $this->writeChunk($bitstream, $buffer); + } + + $exif = $webp->getExif(); + if ($exif !== null) { + $this->writeChunk($exif, $buffer); + } + + $xmp = $webp->getXmp(); + if ($xmp !== null) { + $this->writeChunk($xmp, $buffer); + } + + foreach ($webp->getUnknownChunks() as $chunk) { + $this->writeChunk($chunk, $buffer); + } + + $buffer->setPosition(4); + // The header length does include neither "RIFF" nor the length itself. + // Only substract 4 because the length increases the size by 4 bytes. + $buffer->insertInt($buffer->size() - 4); + + return $buffer->toString(); + } + + private function writeChunk(Chunk $chunk, Buffer $buffer): void + { + $buffer->insertString($chunk->getFourCC()); + $buffer->insertInt($chunk->getLength()); + $buffer->insertString($chunk->getRawBytes()); + + if (\strlen($chunk->getRawBytes()) % 2 === 1) { + $buffer->insertByte(0); + } + } + + private function encodeExtendedFormatFeatures(WebP $webp, Buffer $buffer): void + { + $bitfield = 0; + if ($webp->getIccProfile() !== null) { + $bitfield |= 0b00100000; + } + if ($webp->getAlpha() !== null) { + $bitfield |= 0b00010000; + } + if ($webp->getExif() !== null) { + $bitfield |= 0b00001000; + } + if ($webp->getXmp() !== null) { + $bitfield |= 0b00000100; + } + if ($webp->getAnimation() !== null) { + $bitfield |= 0b00000010; + } + + $buffer->insertByte($bitfield); + } + + private function encodeDimensions(WebP $webp, Buffer $buffer): void + { + // Encode the width and height as a 3 byte value each. + $width = ($webp->width - 1) & 0x00FFFFFF; + $buffer->insertByte(($width >> 0) & 0xFF); + $buffer->insertByte(($width >> 8) & 0xFF); + $buffer->insertByte(($width >> 16) & 0xFF); + + $height = ($webp->height - 1) & 0x00FFFFFF; + $buffer->insertByte(($height >> 0) & 0xFF); + $buffer->insertByte(($height >> 8) & 0xFF); + $buffer->insertByte(($height >> 16) & 0xFF); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraChunksInSimpleFormat.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraChunksInSimpleFormat.php new file mode 100644 index 0000000000..f6a3bfadb8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraChunksInSimpleFormat.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +final class ExtraChunksInSimpleFormat extends RuntimeException implements WebpExifException +{ + /** @param string[] $chunkNames */ + public function __construct(string $fourCC, array $chunkNames) + { + $names = \implode(', ', $chunkNames); + parent::__construct("The file was recognized as simple {$fourCC} but contains extra chunks: {$names}"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraVp8xChunk.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraVp8xChunk.php new file mode 100644 index 0000000000..502208acad --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/ExtraVp8xChunk.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class ExtraVp8xChunk extends OutOfBoundsException implements WebpExifException +{ + public function __construct() + { + parent::__construct("An extended WebP image format may only contain a single VP8X chunk"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/FileSizeMismatch.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/FileSizeMismatch.php new file mode 100644 index 0000000000..32041c95f5 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/FileSizeMismatch.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class FileSizeMismatch extends RuntimeException implements WebpExifException +{ + public function __construct(int $expected, int $found) + { + parent::__construct("The file reports a payload of {$expected} bytes, but actually contains {$found} bytes"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/LengthOutOfBounds.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/LengthOutOfBounds.php new file mode 100644 index 0000000000..a1b2e5dfbe --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/LengthOutOfBounds.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +final class LengthOutOfBounds extends OutOfBoundsException implements WebpExifException +{ + public function __construct(int $length, int $offset, int $remainingBytes) + { + $offset = \dechex($offset); + + parent::__construct("Found the length {$length} at offset 0x{$offset} but there are only {$remainingBytes} bytes remaining"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/MissingChunks.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/MissingChunks.php new file mode 100644 index 0000000000..052746a541 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/MissingChunks.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class MissingChunks extends RuntimeException implements WebpExifException +{ + public function __construct() + { + parent::__construct("A WebP container must contain at least one data chunk"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/NotEnoughData.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/NotEnoughData.php new file mode 100644 index 0000000000..9a5b49515c --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/NotEnoughData.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class NotEnoughData extends RuntimeException implements WebpExifException +{ + public function __construct(int $expected, int $found) + { + parent::__construct("The file size is expected to be at least {$expected} bytes but is only {$found} bytes long"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedChunk.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedChunk.php new file mode 100644 index 0000000000..414338c36e --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedChunk.php @@ -0,0 +1,23 @@ + + * + * @internal + */ +final class UnexpectedChunk extends RuntimeException implements WebpExifException +{ + public function __construct(string $fourCC, int $offset) + { + $offset = \dechex($offset); + parent::__construct("Found the unexpected chunk `{$fourCC}` at offset 0x{$offset}"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedEndOfFile.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedEndOfFile.php new file mode 100644 index 0000000000..6d280a15d1 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnexpectedEndOfFile.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +final class UnexpectedEndOfFile extends RuntimeException implements WebpExifException +{ + public function __construct(int $offset, int $remainingBytes) + { + $offset = \dechex($offset); + + parent::__construct("Expected more data after offset 0x{$offset} ({$remainingBytes} bytes remaining)"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnrecognizedFileFormat.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnrecognizedFileFormat.php new file mode 100644 index 0000000000..77c3dd9c08 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/UnrecognizedFileFormat.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class UnrecognizedFileFormat extends RuntimeException implements WebpExifException +{ + public function __construct() + { + parent::__construct("The provided source appears not be a WebP image"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xAbsentChunk.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xAbsentChunk.php new file mode 100644 index 0000000000..a07c13ea51 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xAbsentChunk.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class Vp8xAbsentChunk extends RuntimeException implements WebpExifException +{ + public function __construct(string $fourCC) + { + parent::__construct("The VP8X header indicates the presence of one or more `{$fourCC}` chunks but none are present"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xHeaderLengthMismatch.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xHeaderLengthMismatch.php new file mode 100644 index 0000000000..e1f366772b --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xHeaderLengthMismatch.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class Vp8xHeaderLengthMismatch extends OutOfBoundsException implements WebpExifException +{ + public function __construct(int $expected, int $found) + { + parent::__construct("The length of the VP8X header was expected to be {$expected} but found {$found}"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xMissingImageData.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xMissingImageData.php new file mode 100644 index 0000000000..b763f3c7eb --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xMissingImageData.php @@ -0,0 +1,28 @@ + + * + * @internal + */ +final class Vp8xMissingImageData extends OutOfBoundsException implements WebpExifException +{ + public function __construct(bool $stillImage) + { + if ($stillImage) { + $message = "The file did not contain any VP8 or VP8L chunks"; + } else { + $message = "The file did not contain multiple ANMF chunks"; + } + + parent::__construct($message); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xWithoutChunks.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xWithoutChunks.php new file mode 100644 index 0000000000..addd1cb32b --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/Vp8xWithoutChunks.php @@ -0,0 +1,22 @@ + + * + * @internal + */ +final class Vp8xWithoutChunks extends RuntimeException implements WebpExifException +{ + public function __construct() + { + parent::__construct("The file uses the extended WebP format but does not provide any other chunks"); + } +} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/WebpExifException.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/WebpExifException.php new file mode 100644 index 0000000000..2460ddafbc --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/Exception/WebpExifException.php @@ -0,0 +1,16 @@ + + */ +interface WebpExifException extends Throwable {} diff --git a/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/WebP.php b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/WebP.php new file mode 100644 index 0000000000..cbe3892463 --- /dev/null +++ b/wcfsetup/install/files/lib/system/api/woltlab/webp-exif/src/WebP.php @@ -0,0 +1,304 @@ + + */ +final class WebP +{ + private function __construct( + public readonly int $width, + public readonly int $height, + /** @var list */ + private array $chunks, + ) {} + + /** + * @return list + */ + public function getChunks(): array + { + return $this->chunks; + } + + /** + * Returns the length of all chunks including an additional 8 bytes per + * chunk for the chunk header. + */ + public function getByteLength(): int + { + return \array_reduce( + $this->chunks, + static fn(int $length, Chunk $chunk) => $length + $chunk->getLength() + 8, + 0 + ); + } + + /** + * WebP can be encoded using the simple format that only contains a single + * VP8 or VP8L chunk and is a bit smaller. Animations or any sort of extra + * information is only supported in the extended file format. + */ + public function containsOnlyBitstream(): bool + { + if (\count($this->chunks) > 1) { + return false; + } + + // The simple format can only contain a single VP8/VP8L chunk. + return \array_all($this->chunks, static function ($chunk) { + return match ($chunk::class) { + Vp8::class, Vp8l::class => true, + default => false, + }; + }); + } + + public function getIccProfile(): ?Iccp + { + return \array_find($this->chunks, static fn($chunk) => $chunk instanceof Iccp); + } + + public function getAlpha(): ?Alph + { + return \array_find($this->chunks, static fn($chunk) => $chunk instanceof Alph); + } + + public function getExif(): ?Exif + { + return \array_find($this->chunks, static fn($chunk) => $chunk instanceof Exif); + } + + public function getXmp(): ?Xmp + { + return \array_find($this->chunks, static fn($chunk) => $chunk instanceof Xmp); + } + + public function getAnimation(): ?Anim + { + return \array_find($this->chunks, static fn($chunk) => $chunk instanceof Anim); + } + + /** + * @return list + */ + public function getAnimationFrames(): array + { + return \array_values( + \array_filter( + $this->chunks, + static fn($chunk) => $chunk instanceof Anmf + ) + ); + } + + public function getBitstream(): Vp8|Vp8l|null + { + if ($this->getAnimation() !== null) { + return null; + } + + return \array_find( + $this->chunks, + static fn($chunk) => $chunk instanceof Vp8 || $chunk instanceof Vp8l + ); + } + + /** + * @return list + */ + public function getUnknownChunks(): array + { + return \array_values( + \array_filter( + $this->chunks, + static fn($chunk) => $chunk instanceof UnknownChunk + ) + ); + } + + /** + * Adds or removes EXIF data. + */ + public function withExif(?Exif $exif): self + { + $chunks = \array_values( + \array_filter( + $this->chunks, + static fn($chunk) => !($chunk instanceof Exif) + ) + ); + + if ($exif !== null) { + $chunks[] = $exif; + } + + $webp = clone $this; + $webp->chunks = $chunks; + + return $webp; + } + + /** + * Adds or removes an ICC profile. + */ + public function withIccp(?Iccp $iccp): self + { + $chunks = \array_values( + \array_filter( + $this->chunks, + static fn($chunk) => !($chunk instanceof Iccp) + ) + ); + + if ($iccp !== null) { + $chunks[] = $iccp; + } + + $webp = clone $this; + $webp->chunks = $chunks; + + return $webp; + } + + /** + * Adds or removes XMP data. + */ + public function withXmp(?Xmp $xmp): self + { + $chunks = \array_values( + \array_filter( + $this->chunks, + static fn($chunk) => !($chunk instanceof Xmp) + ) + ); + + if ($xmp !== null) { + $chunks[] = $xmp; + } + + $webp = clone $this; + $webp->chunks = $chunks; + + return $webp; + } + + /** + * Adds a list of unhandled chunks. It is not possibly to remove unknown + * chunks at this time. + * + * @param list $chunks + */ + public function withUnknownChunks(array $chunks): self + { + $newChunks = $this->chunks; + foreach ($chunks as $chunk) { + if (!($chunk instanceof UnknownChunk)) { + throw new BadMethodCallException( + \sprintf( + "Expected a list of %s, received %s instead", + UnknownChunk::class, + \get_debug_type($chunk), + ), + ); + } + + $newChunks[] = $chunk; + } + + $webp = clone $this; + $webp->chunks = $newChunks; + + return $webp; + } + + /** + * Creates a new WebP object from the provided chunks. Please use the + * `Decoder` class to decode the binary data of a WebP image. + * + * @param list $chunks + */ + public static function fromChunks(array $chunks): self + { + foreach ($chunks as $chunk) { + if (!($chunk instanceof Chunk)) { + throw new BadMethodCallException( + \sprintf( + "Expected a list of %s, received %s instead", + Chunk::class, + \get_debug_type($chunk), + ), + ); + } + } + + if ($chunks === []) { + throw new MissingChunks(); + } + + // The first chunk must be one of VP8, VP8L or VP8X. + $firstChunk = \array_shift($chunks); + \assert($firstChunk !== null); + + return match ($firstChunk::class) { + Vp8::class, Vp8l::class => self::fromSimpleFormat($firstChunk, $chunks), + Vp8x::class => self::fromExtendedFormat($firstChunk, $chunks), + default => throw new UnexpectedChunk($firstChunk->getFourCC(), 12) + }; + } + + /** + * @param list $chunks + */ + private static function fromSimpleFormat(Vp8|Vp8l $imageData, array $chunks): self + { + if ($chunks !== []) { + throw new ExtraChunksInSimpleFormat( + $imageData->getFourCC(), + \array_map(static fn(Chunk $chunk) => $chunk->getFourCC(), $chunks) + ); + } + + return new WebP( + $imageData->width, + $imageData->height, + [ + $imageData, + ...$chunks, + ] + ); + } + + /** + * @param list $chunks + */ + private static function fromExtendedFormat(Vp8x $vp8x, array $chunks): self + { + $chunks = $vp8x->filterChunks($chunks); + + return new WebP($vp8x->width, $vp8x->height, $chunks); + } +} From 0ce531e96c193edbe6495133ee139b8414a3e937 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 17 Jul 2025 16:36:07 +0200 Subject: [PATCH 02/25] Add support for EXIF data from WebP images --- .../install/files/lib/util/ExifUtil.class.php | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/wcfsetup/install/files/lib/util/ExifUtil.class.php b/wcfsetup/install/files/lib/util/ExifUtil.class.php index a454323e32..a854f23753 100644 --- a/wcfsetup/install/files/lib/util/ExifUtil.class.php +++ b/wcfsetup/install/files/lib/util/ExifUtil.class.php @@ -2,6 +2,9 @@ namespace wcf\util; +use WoltLab\WebpExif\Decoder; +use WoltLab\WebpExif\Exception\WebpExifException; + /** * Provides exif-related functions. * @@ -83,11 +86,27 @@ private function __construct() */ public static function getExifData(string $filename): array { - if (\function_exists('exif_read_data')) { - $exifData = @\exif_read_data($filename, '', true); - if ($exifData !== false) { - return $exifData; + $mimeType = FileUtil::getMimeType($filename); + if ($mimeType === 'image/webp') { + $decoder = new Decoder(); + + try { + $webp = $decoder->fromBinary(\file_get_contents($filename)); + } catch (WebpExifException) { + return []; + } + + $exifData = $webp->getExif()?->getParsedExif(); + if ($exifData === null) { + return []; } + + return $exifData; + } + + $exifData = @\exif_read_data($filename, '', true); + if ($exifData !== false) { + return $exifData; } return []; From ea8e23570040535c6f4cd172574205cf49491689 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 17 Jul 2025 17:12:35 +0200 Subject: [PATCH 03/25] Store EXIF data of uploaded images --- .../install/files/lib/data/file/File.class.php | 1 + .../files/lib/data/file/FileEditor.class.php | 14 +++++++++++++- .../data/file/temporary/FileTemporary.class.php | 1 + wcfsetup/setup/db/install.sql | 6 ++++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/wcfsetup/install/files/lib/data/file/File.class.php b/wcfsetup/install/files/lib/data/file/File.class.php index 2a80dc1a9e..9c68846d7c 100644 --- a/wcfsetup/install/files/lib/data/file/File.class.php +++ b/wcfsetup/install/files/lib/data/file/File.class.php @@ -32,6 +32,7 @@ * @property-read int|null $height * @property-read string|null $fileHashWebp * @property-read int $uploadTime + * @property-read string|null $exifData */ class File extends DatabaseObject implements ITitledLinkObject, IImageDataProvider { diff --git a/wcfsetup/install/files/lib/data/file/FileEditor.class.php b/wcfsetup/install/files/lib/data/file/FileEditor.class.php index d93e705593..a6ce386e8c 100644 --- a/wcfsetup/install/files/lib/data/file/FileEditor.class.php +++ b/wcfsetup/install/files/lib/data/file/FileEditor.class.php @@ -11,6 +11,7 @@ use wcf\system\image\ImageHandler; use wcf\util\ExifUtil; use wcf\util\FileUtil; +use wcf\util\JSON; /** * @author Alexander Ebert @@ -99,6 +100,7 @@ public static function createFromTemporary(FileTemporary $fileTemporary): File 'width' => $width, 'height' => $height, 'uploadTime' => \TIME_NOW, + 'exifData' => $fileTemporary->exifData, ]]); $file = $fileAction->executeAction()['returnValues']; \assert($file instanceof File); @@ -124,7 +126,8 @@ public static function createFromExistingFile( string $originalFilename, string $objectTypeName, bool $copy = false, - ?int $uploadTime = null + ?int $uploadTime = null, + ?array $exifData = null, ): ?File { if (!\is_readable($pathname)) { return null; @@ -144,6 +147,14 @@ public static function createFromExistingFile( default => false, }; + if ($exifData === null) { + $exifData = ExifUtil::getExifData($pathname); + + if ($exifData === []) { + $exifData = null; + } + } + $width = $height = null; if ($isImage) { try { @@ -173,6 +184,7 @@ public static function createFromExistingFile( 'width' => $width, 'height' => $height, 'uploadTime' => $uploadTime, + 'exifData' => JSON::encode($exifData), ]]); $file = $fileAction->executeAction()['returnValues']; \assert($file instanceof File); diff --git a/wcfsetup/install/files/lib/data/file/temporary/FileTemporary.class.php b/wcfsetup/install/files/lib/data/file/temporary/FileTemporary.class.php index 35161f3382..98f4109909 100644 --- a/wcfsetup/install/files/lib/data/file/temporary/FileTemporary.class.php +++ b/wcfsetup/install/files/lib/data/file/temporary/FileTemporary.class.php @@ -20,6 +20,7 @@ * @property-read int|null $objectTypeID * @property-read string $context * @property-read string $chunks + * @property-read string|null $exifData */ class FileTemporary extends DatabaseObject { diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 3872d826f2..99388982a3 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -607,7 +607,8 @@ CREATE TABLE wcf1_file ( width INT, height INT, fileHashWebp CHAR(64), - uploadTime INT + uploadTime INT, + exifData MEDIUMTEXT ); DROP TABLE IF EXISTS wcf1_file_temporary; @@ -619,7 +620,8 @@ CREATE TABLE wcf1_file_temporary ( fileHash CHAR(64) NOT NULL, objectTypeID INT, context TEXT, - chunks VARBINARY(255) NOT NULL + chunks VARBINARY(255) NOT NULL, + exifData MEDIUMTEXT ); DROP TABLE IF EXISTS wcf1_file_thumbnail; From 495b1865e0bc1c2e89473195e8a6ba24dfbfbc8c Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 18 Jul 2025 15:14:27 +0200 Subject: [PATCH 04/25] Extract EXIF data and store it outside the file --- ts/WoltLabSuite/Core/Api/Files/Upload.ts | 8 + ts/WoltLabSuite/Core/Component/File/Upload.ts | 82 ++++++-- ts/WoltLabSuite/Core/Image/ExifUtil.ts | 15 ++ ts/WoltLabSuite/Core/Image/WebP.ts | 196 ++++++++++++++++++ .../update_com.woltlab.wcf_6.2_step1.php | 5 + .../js/WoltLabSuite/Core/Api/Files/Upload.js | 7 +- .../Core/Component/File/Upload.js | 45 +++- .../js/WoltLabSuite/Core/Image/ExifUtil.js | 13 +- .../files/js/WoltLabSuite/Core/Image/WebP.js | 140 +++++++++++++ .../core/files/PrepareUpload.class.php | 3 + 10 files changed, 485 insertions(+), 29 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Image/WebP.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Image/WebP.js diff --git a/ts/WoltLabSuite/Core/Api/Files/Upload.ts b/ts/WoltLabSuite/Core/Api/Files/Upload.ts index 1c0e1ac689..3b0895bdcc 100644 --- a/ts/WoltLabSuite/Core/Api/Files/Upload.ts +++ b/ts/WoltLabSuite/Core/Api/Files/Upload.ts @@ -1,5 +1,6 @@ import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; +import type { Exif } from "WoltLabSuite/Core/Image/ExifUtil"; type Response = { identifier: string; @@ -12,15 +13,22 @@ export async function upload( fileHash: string, objectType: string, context: string, + exifBytes: Exif | null = null, ): Promise> { const url = new URL(`${window.WSC_RPC_API_URL}core/files/upload`); + let exifData: string | null = null; + if (exifBytes !== null) { + exifData = new TextDecoder().decode(exifBytes); + } + const payload = { filename, fileSize, fileHash, objectType, context, + exifData, }; let response: Response; diff --git a/ts/WoltLabSuite/Core/Component/File/Upload.ts b/ts/WoltLabSuite/Core/Component/File/Upload.ts index 093ce10bd7..ff85e79dd1 100644 --- a/ts/WoltLabSuite/Core/Component/File/Upload.ts +++ b/ts/WoltLabSuite/Core/Component/File/Upload.ts @@ -13,6 +13,7 @@ import { innerError } from "WoltLabSuite/Core/Dom/Util"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { createSHA256 } from "hash-wasm"; import { cropImage, CropperConfiguration } from "WoltLabSuite/Core/Component/Image/Cropper"; +import { Exif, getExifBytesFromJpeg, getExifBytesFromWebP } from "WoltLabSuite/Core/Image/ExifUtil"; export type CkeditorDropEvent = { file: File; @@ -44,6 +45,7 @@ async function upload( element: WoltlabCoreFileUploadElement, file: File, fileHash: string, + exifData: Exif | null, ): Promise { const objectType = element.dataset.objectType!; @@ -54,7 +56,14 @@ async function upload( const event = new CustomEvent("uploadStart", { detail: fileElement }); element.dispatchEvent(event); - const response = await filesUpload(file.name, file.size, fileHash, objectType, element.dataset.context || ""); + const response = await filesUpload( + file.name, + file.size, + fileHash, + objectType, + element.dataset.context || "", + exifData, + ); if (!response.ok) { const validationError = response.error.getValidationError(); if (validationError === undefined) { @@ -290,6 +299,24 @@ function reportError(element: WoltlabCoreFileUploadElement, file: File | null, m innerError(element, message); } +async function getExifBytes(file: File): Promise { + if (file.type === "image/jpeg") { + try { + return await getExifBytesFromJpeg(file); + } catch { + return null; + } + } else if (file.type === "image/webp") { + try { + return await getExifBytesFromWebP(file); + } catch { + return null; + } + } + + return null; +} + export function setup(): void { wheneverFirstSeen("woltlab-core-file-upload", (element: WoltlabCoreFileUploadElement) => { element.addEventListener("upload:files", (event: CustomEvent<{ files: File[] }>) => { @@ -311,11 +338,15 @@ export function setup(): void { element.markAsBusy(); + const exifData = new Map(); + let processImage: (file: File) => Promise; if (element.dataset.cropperConfiguration) { const cropperConfiguration = JSON.parse(element.dataset.cropperConfiguration) as CropperConfiguration; processImage = async (file) => { + exifData.set(file, await getExifBytes(file)); + try { return await cropImage(element, file, cropperConfiguration); } catch (e) { @@ -325,7 +356,11 @@ export function setup(): void { } }; } else { - processImage = async (file) => resizeImage(element, file); + processImage = async (file) => { + exifData.set(file, await getExifBytes(file)); + + return resizeImage(element, file); + }; } // Resize all files in parallel but keep the original order. This ensures @@ -359,7 +394,8 @@ export function setup(): void { const result = checksums[i]; if (result.status === "fulfilled") { - void upload(element, validFiles[i], result.value); + const exif = exifData.get(validFiles[i]) || null; + void upload(element, validFiles[i], result.value, exif); } else { throw new Error(result.reason); } @@ -394,26 +430,32 @@ export function setup(): void { return; } - void resizeImage(element, file).then(async (resizeFile) => { - try { - const checksum = await getSha256Hash(resizeFile); - const data = await upload(element, resizeFile, checksum); - if (data === undefined || typeof data.data.attachmentID !== "number") { + let exifData: Exif | null; + void getExifBytes(file) + .then((exif) => { + exifData = exif; + }) + .then(() => resizeImage(element, file)) + .then(async (resizeFile) => { + try { + const checksum = await getSha256Hash(resizeFile); + const data = await upload(element, resizeFile, checksum, exifData); + if (data === undefined || typeof data.data.attachmentID !== "number") { + promiseReject(); + } else { + const attachmentData: AttachmentData = { + attachmentId: data.data.attachmentID, + url: data.link, + }; + + promiseResolve(attachmentData); + } + } catch (e) { promiseReject(); - } else { - const attachmentData: AttachmentData = { - attachmentId: data.data.attachmentID, - url: data.link, - }; - promiseResolve(attachmentData); + throw e; } - } catch (e) { - promiseReject(); - - throw e; - } - }); + }); }); }); } diff --git a/ts/WoltLabSuite/Core/Image/ExifUtil.ts b/ts/WoltLabSuite/Core/Image/ExifUtil.ts index 4a6df6e762..3eb6cd8f72 100644 --- a/ts/WoltLabSuite/Core/Image/ExifUtil.ts +++ b/ts/WoltLabSuite/Core/Image/ExifUtil.ts @@ -7,6 +7,8 @@ * @woltlabExcludeBundle tiny */ +import { parseWebPFromBuffer } from "./WebP"; + const Tag = { SOI: 0xd8, // Start of image APP0: 0xe0, // JFIF tag @@ -113,6 +115,19 @@ export async function getExifBytesFromJpeg(blob: Blob | File): Promise { return exif; } +export async function getExifBytesFromWebP(blob: Blob | File): Promise { + if (!((blob as any) instanceof Blob) && !(blob instanceof File)) { + throw new TypeError("The argument must be a Blob or a File"); + } + + const webp = parseWebPFromBuffer(await blob.arrayBuffer()); + if (webp === undefined) { + return null; + } + + return webp.getExifData(); +} + /** * Removes all EXIF and XMP sections of a JPEG blob. */ diff --git a/ts/WoltLabSuite/Core/Image/WebP.ts b/ts/WoltLabSuite/Core/Image/WebP.ts new file mode 100644 index 0000000000..7a487b27d2 --- /dev/null +++ b/ts/WoltLabSuite/Core/Image/WebP.ts @@ -0,0 +1,196 @@ +/** + * Provides helper functions to decode a WebP image. + * + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ + +import type { Exif } from "./ExifUtil"; + +const enum ChunkHeader { + ALPH = "ALPH", + ANIM = "ANIM", + ANMF = "ANMF", + EXIF = "EXIF", + ICCP = "ICCP", + RIFF = "RIFF", + VP8 = "VP8 ", + VP8L = "VP8L", + VP8X = "VP8X", + WEBP = "WEBP", + XMP = "XMP ", +} + +function decodeHeader(uint32BE: number): ChunkHeader | number { + switch (uint32BE) { + case 0x414c5048: + return ChunkHeader.ALPH; + + case 0x414e494d: + return ChunkHeader.ANIM; + + case 0x414e4d46: + return ChunkHeader.ANMF; + + case 0x45584946: + return ChunkHeader.EXIF; + + case 0x49434350: + return ChunkHeader.ICCP; + + case 0x52494646: + return ChunkHeader.RIFF; + + case 0x56503820: + return ChunkHeader.VP8; + + case 0x5650384c: + return ChunkHeader.VP8L; + + case 0x56503858: + return ChunkHeader.VP8X; + + case 0x57454250: + return ChunkHeader.WEBP; + + case 0x584d5020: + return ChunkHeader.XMP; + + default: + return uint32BE; + } +} + +type Offset = number; +type ChunkSize = number; +type Chunk = [ChunkHeader | number, Offset, ChunkSize]; + +class WebP { + readonly #buffer: ArrayBuffer; + readonly #chunks: Chunk[]; + readonly #height: number; + readonly #width: number; + + constructor(buffer: ArrayBuffer, width: number, height: number, chunks: Chunk[]) { + this.#buffer = buffer; + this.#chunks = chunks; + this.#height = height; + this.#width = width; + } + + getExifData(): Exif | null { + for (const [chunkHeader, offset, chunkSize] of this.#chunks) { + if (chunkHeader === ChunkHeader.EXIF) { + return new Uint8Array(this.#buffer.slice(offset, offset + chunkSize)); + } + } + + return null; + } + + get height(): number { + return this.#height; + } + + get width(): number { + return this.#width; + } +} + +function parseVp8x(buffer: ArrayBuffer, dataView: DataView): WebP { + if (dataView.byteLength <= 30) { + throw new Error("A VP8X encoded WebP must be larger than 30 bytes."); + } + + // If we reach this point, then we have already consumed the first 20 bytes of + // the buffer. (offset = 20) + + // The next 8 bits contain the flags. (offset + 1 = 21) + + // The next 24 bits are reserved. (offset + 3 = 24) + + // The next 48 bits contain the width and height, represented as uint24LE, but + // using the value - 1, thus we need to add 1 to each calculated value. + const width = ((dataView.getUint8(26) << 16) | (dataView.getUint8(25) << 8) | dataView.getUint8(24)) + 1; + const height = ((dataView.getUint8(29) << 16) | (dataView.getUint8(28) << 8) | dataView.getUint8(27)) + 1; + + const chunks: Chunk[] = []; + let offset = 30; + while (offset < dataView.byteLength) { + const chunkHeader = decodeHeader(dataView.getUint32(offset)); + const chunkSize = dataView.getUint32(offset + 4, true); + offset += 8; + + chunks.push([chunkHeader, offset, chunkSize]); + + offset += chunkSize; + + if (chunkSize % 2 === 1) { + // "If Chunk Size is odd, a single padding byte -- which MUST be 0 to + // conform with RIFF -- is added." + offset += 1; + } + + if (offset > dataView.byteLength) { + const header = typeof chunkHeader === "number" ? `0x${chunkHeader.toString(16)}` : chunkHeader; + throw new Error(`Corrupted image detected, offset ${offset} > ${dataView.byteLength} for chunk ${header}.`); + } + } + + return new WebP(buffer, width, height, chunks); +} + +function getDimensions(buffer: ArrayBuffer): [number, number] { + // This is the lazy version that avoids having to implement an RFC 6386 parser + // to extract the dimensions from the VP8/VP8L frames. + const blob = new Blob([new Uint8Array(buffer)], { type: "image/webp" }); + const img = new Image(); + img.src = window.URL.createObjectURL(blob); + + return [img.naturalWidth, img.naturalHeight]; +} + +export function parseWebPFromBuffer(buffer: ArrayBuffer): WebP | undefined { + const dataView = new DataView(buffer, 0, buffer.byteLength); + if (dataView.byteLength < 20) { + // Anything below 20 bytes cannot be an WebP image. The first 12 bytes are + // the RIFF header followed by at least 8 bytes for a chunk plus its size. + return undefined; + } + + if (decodeHeader(dataView.getUint32(0)) !== ChunkHeader.RIFF) { + return undefined; + } + + // The next 4 bytes represent the total size of the file. + + if (decodeHeader(dataView.getUint32(8)) !== ChunkHeader.WEBP) { + return undefined; + } + + const firstChunk = decodeHeader(dataView.getUint32(12)); + if (typeof firstChunk === "number") { + // The first chunk must be a known value. + throw new Error(`Unrecognized chunk 0x${firstChunk.toString(16)} at the first position`); + } + + const chunkSize = dataView.getUint32(16, true); + + switch (firstChunk) { + case ChunkHeader.VP8: + case ChunkHeader.VP8L: { + const [width, height] = getDimensions(buffer); + + return new WebP(buffer, width, height, [[firstChunk, 20, chunkSize]]); + } + + case ChunkHeader.VP8X: + return parseVp8x(buffer, dataView); + + default: + throw new Error(`Unexpected chunk "${firstChunk}" at the first position`); + } +} diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php index 8fce51d209..a7bfc88c4e 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php @@ -62,5 +62,10 @@ PartialDatabaseTable::create('wcf1_file') ->columns([ IntDatabaseTableColumn::create('uploadTime'), + MediumtextDatabaseTableColumn::create('exifData'), + ]), + PartialDatabaseTable::create('wcf1_file_temporary') + ->columns([ + MediumtextDatabaseTableColumn::create('exifData'), ]), ]; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/Upload.js index 762ec12c86..b97f50bf6b 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/Upload.js @@ -2,14 +2,19 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.upload = upload; - async function upload(filename, fileSize, fileHash, objectType, context) { + async function upload(filename, fileSize, fileHash, objectType, context, exifBytes = null) { const url = new URL(`${window.WSC_RPC_API_URL}core/files/upload`); + let exifData = null; + if (exifBytes !== null) { + exifData = new TextDecoder().decode(exifBytes); + } const payload = { filename, fileSize, fileHash, objectType, context, + exifData, }; let response; try { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js index e60a620f9c..63294d4969 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js @@ -1,18 +1,18 @@ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/Files/Upload", "WoltLabSuite/Core/Api/Files/Chunk/Chunk", "WoltLabSuite/Core/Api/Files/GenerateThumbnails", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "hash-wasm", "WoltLabSuite/Core/Component/Image/Cropper"], function (require, exports, tslib_1, Selector_1, Upload_1, Chunk_1, GenerateThumbnails_1, Resizer_1, Util_1, Language_1, hash_wasm_1, Cropper_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/Files/Upload", "WoltLabSuite/Core/Api/Files/Chunk/Chunk", "WoltLabSuite/Core/Api/Files/GenerateThumbnails", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "hash-wasm", "WoltLabSuite/Core/Component/Image/Cropper", "WoltLabSuite/Core/Image/ExifUtil"], function (require, exports, tslib_1, Selector_1, Upload_1, Chunk_1, GenerateThumbnails_1, Resizer_1, Util_1, Language_1, hash_wasm_1, Cropper_1, ExifUtil_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.clearPreviousErrors = clearPreviousErrors; exports.setup = setup; Resizer_1 = tslib_1.__importDefault(Resizer_1); const BUFFER_SIZE = 10 * 1_024 * 1_024; - async function upload(element, file, fileHash) { + async function upload(element, file, fileHash, exifData) { const objectType = element.dataset.objectType; const fileElement = document.createElement("woltlab-core-file"); fileElement.dataset.filename = file.name; fileElement.dataset.fileSize = file.size.toString(); const event = new CustomEvent("uploadStart", { detail: fileElement }); element.dispatchEvent(event); - const response = await (0, Upload_1.upload)(file.name, file.size, fileHash, objectType, element.dataset.context || ""); + const response = await (0, Upload_1.upload)(file.name, file.size, fileHash, objectType, element.dataset.context || "", exifData); if (!response.ok) { const validationError = response.error.getValidationError(); if (validationError === undefined) { @@ -193,6 +193,25 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol } (0, Util_1.innerError)(element, message); } + async function getExifBytes(file) { + if (file.type === "image/jpeg") { + try { + return await (0, ExifUtil_1.getExifBytesFromJpeg)(file); + } + catch { + return null; + } + } + else if (file.type === "image/webp") { + try { + return await (0, ExifUtil_1.getExifBytesFromWebP)(file); + } + catch { + return null; + } + } + return null; + } function setup() { (0, Selector_1.wheneverFirstSeen)("woltlab-core-file-upload", (element) => { element.addEventListener("upload:files", (event) => { @@ -210,10 +229,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol } } element.markAsBusy(); + const exifData = new Map(); let processImage; if (element.dataset.cropperConfiguration) { const cropperConfiguration = JSON.parse(element.dataset.cropperConfiguration); processImage = async (file) => { + exifData.set(file, await getExifBytes(file)); try { return await (0, Cropper_1.cropImage)(element, file, cropperConfiguration); } @@ -224,7 +245,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol }; } else { - processImage = async (file) => resizeImage(element, file); + processImage = async (file) => { + exifData.set(file, await getExifBytes(file)); + return resizeImage(element, file); + }; } // Resize all files in parallel but keep the original order. This ensures // that files are uploaded in the same order that they were provided by @@ -255,7 +279,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol for (let i = 0, length = checksums.length; i < length; i++) { const result = checksums[i]; if (result.status === "fulfilled") { - void upload(element, validFiles[i], result.value); + const exif = exifData.get(validFiles[i]) || null; + void upload(element, validFiles[i], result.value, exif); } else { throw new Error(result.reason); @@ -283,10 +308,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol promiseReject(); return; } - void resizeImage(element, file).then(async (resizeFile) => { + let exifData; + void getExifBytes(file) + .then((exif) => { + exifData = exif; + }) + .then(() => resizeImage(element, file)) + .then(async (resizeFile) => { try { const checksum = await getSha256Hash(resizeFile); - const data = await upload(element, resizeFile, checksum); + const data = await upload(element, resizeFile, checksum, exifData); if (data === undefined || typeof data.data.attachmentID !== "number") { promiseReject(); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ExifUtil.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ExifUtil.js index e09ff1091d..4643001197 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ExifUtil.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ExifUtil.js @@ -6,10 +6,11 @@ * @license GNU Lesser General Public License * @woltlabExcludeBundle tiny */ -define(["require", "exports"], function (require, exports) { +define(["require", "exports", "./WebP"], function (require, exports, WebP_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getExifBytesFromJpeg = getExifBytesFromJpeg; + exports.getExifBytesFromWebP = getExifBytesFromWebP; exports.removeExifData = removeExifData; exports.setExifData = setExifData; const Tag = { @@ -100,6 +101,16 @@ define(["require", "exports"], function (require, exports) { } return exif; } + async function getExifBytesFromWebP(blob) { + if (!(blob instanceof Blob) && !(blob instanceof File)) { + throw new TypeError("The argument must be a Blob or a File"); + } + const webp = (0, WebP_1.parseWebPFromBuffer)(await blob.arrayBuffer()); + if (webp === undefined) { + return null; + } + return webp.getExifData(); + } /** * Removes all EXIF and XMP sections of a JPEG blob. */ diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Image/WebP.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/WebP.js new file mode 100644 index 0000000000..6ce81ccd56 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/WebP.js @@ -0,0 +1,140 @@ +/** + * Provides helper functions to decode a WebP image. + * + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ +define(["require", "exports"], function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.parseWebPFromBuffer = parseWebPFromBuffer; + function decodeHeader(uint32BE) { + switch (uint32BE) { + case 0x414c5048: + return "ALPH" /* ChunkHeader.ALPH */; + case 0x414e494d: + return "ANIM" /* ChunkHeader.ANIM */; + case 0x414e4d46: + return "ANMF" /* ChunkHeader.ANMF */; + case 0x45584946: + return "EXIF" /* ChunkHeader.EXIF */; + case 0x49434350: + return "ICCP" /* ChunkHeader.ICCP */; + case 0x52494646: + return "RIFF" /* ChunkHeader.RIFF */; + case 0x56503820: + return "VP8 " /* ChunkHeader.VP8 */; + case 0x5650384c: + return "VP8L" /* ChunkHeader.VP8L */; + case 0x56503858: + return "VP8X" /* ChunkHeader.VP8X */; + case 0x57454250: + return "WEBP" /* ChunkHeader.WEBP */; + case 0x584d5020: + return "XMP " /* ChunkHeader.XMP */; + default: + return uint32BE; + } + } + class WebP { + #buffer; + #chunks; + #height; + #width; + constructor(buffer, width, height, chunks) { + this.#buffer = buffer; + this.#chunks = chunks; + this.#height = height; + this.#width = width; + } + getExifData() { + for (const [chunkHeader, offset, chunkSize] of this.#chunks) { + if (chunkHeader === "EXIF" /* ChunkHeader.EXIF */) { + return new Uint8Array(this.#buffer.slice(offset, offset + chunkSize)); + } + } + return null; + } + get height() { + return this.#height; + } + get width() { + return this.#width; + } + } + function parseVp8x(buffer, dataView) { + if (dataView.byteLength <= 30) { + throw new Error("A VP8X encoded WebP must be larger than 30 bytes."); + } + // If we reach this point, then we have already consumed the first 20 bytes of + // the buffer. (offset = 20) + // The next 8 bits contain the flags. (offset + 1 = 21) + // The next 24 bits are reserved. (offset + 3 = 24) + // The next 48 bits contain the width and height, represented as uint24LE, but + // using the value - 1, thus we need to add 1 to each calculated value. + const width = ((dataView.getUint8(26) << 16) | (dataView.getUint8(25) << 8) | dataView.getUint8(24)) + 1; + const height = ((dataView.getUint8(29) << 16) | (dataView.getUint8(28) << 8) | dataView.getUint8(27)) + 1; + const chunks = []; + let offset = 30; + while (offset < dataView.byteLength) { + const chunkHeader = decodeHeader(dataView.getUint32(offset)); + const chunkSize = dataView.getUint32(offset + 4, true); + offset += 8; + chunks.push([chunkHeader, offset, chunkSize]); + offset += chunkSize; + if (chunkSize % 2 === 1) { + // "If Chunk Size is odd, a single padding byte -- which MUST be 0 to + // conform with RIFF -- is added." + offset += 1; + } + if (offset > dataView.byteLength) { + const header = typeof chunkHeader === "number" ? `0x${chunkHeader.toString(16)}` : chunkHeader; + throw new Error(`Corrupted image detected, offset ${offset} > ${dataView.byteLength} for chunk ${header}.`); + } + } + return new WebP(buffer, width, height, chunks); + } + function getDimensions(buffer) { + // This is the lazy version that avoids having to implement an RFC 6386 parser + // to extract the dimensions from the VP8/VP8L frames. + const blob = new Blob([new Uint8Array(buffer)], { type: "image/webp" }); + const img = new Image(); + img.src = window.URL.createObjectURL(blob); + return [img.naturalWidth, img.naturalHeight]; + } + function parseWebPFromBuffer(buffer) { + const dataView = new DataView(buffer, 0, buffer.byteLength); + if (dataView.byteLength < 20) { + // Anything below 20 bytes cannot be an WebP image. The first 12 bytes are + // the RIFF header followed by at least 8 bytes for a chunk plus its size. + return undefined; + } + if (decodeHeader(dataView.getUint32(0)) !== "RIFF" /* ChunkHeader.RIFF */) { + return undefined; + } + // The next 4 bytes represent the total size of the file. + if (decodeHeader(dataView.getUint32(8)) !== "WEBP" /* ChunkHeader.WEBP */) { + return undefined; + } + const firstChunk = decodeHeader(dataView.getUint32(12)); + if (typeof firstChunk === "number") { + // The first chunk must be a known value. + throw new Error(`Unrecognized chunk 0x${firstChunk.toString(16)} at the first position`); + } + const chunkSize = dataView.getUint32(16, true); + switch (firstChunk) { + case "VP8 " /* ChunkHeader.VP8 */: + case "VP8L" /* ChunkHeader.VP8L */: { + const [width, height] = getDimensions(buffer); + return new WebP(buffer, width, height, [[firstChunk, 20, chunkSize]]); + } + case "VP8X" /* ChunkHeader.VP8X */: + return parseVp8x(buffer, dataView); + default: + throw new Error(`Unexpected chunk "${firstChunk}" at the first position`); + } + } +}); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php index 03abd26fcd..d99234ca6b 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php @@ -85,6 +85,7 @@ private function createTemporaryFile(PostUploadParameters $parameters, int $numb 'objectTypeID' => $objectType?->objectTypeID, 'context' => $parameters->context, 'chunks' => \str_repeat('0', $numberOfChunks), + 'exifData' => $parameters->exifData, ], ]); @@ -110,5 +111,7 @@ public function __construct( /** @var non-empty-string */ public readonly string $context, + + public readonly ?string $exifData = null, ) {} } From a898b0c4fe7ed2596d2987785f5d43436825a9f9 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 18 Jul 2025 16:55:54 +0200 Subject: [PATCH 05/25] Export resized images as WebP and strip EXIF --- ts/WoltLabSuite/Core/Component/File/Upload.ts | 5 ++--- .../files/js/WoltLabSuite/Core/Component/File/Upload.js | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/File/Upload.ts b/ts/WoltLabSuite/Core/Component/File/Upload.ts index ff85e79dd1..e4c5b3cae1 100644 --- a/ts/WoltLabSuite/Core/Component/File/Upload.ts +++ b/ts/WoltLabSuite/Core/Component/File/Upload.ts @@ -172,7 +172,7 @@ async function resizeImage(element: WoltlabCoreFileUploadElement, file: File): P const resizeConfiguration = JSON.parse(element.dataset.resizeConfiguration!) as ResizeConfiguration; const resizer = new ImageResizer(); - const { image, exif } = await resizer.loadFile(file); + const { image } = await resizer.loadFile(file); const maxHeight = resizeConfiguration.maxHeight === -1 ? image.height : resizeConfiguration.maxHeight; let maxWidth = resizeConfiguration.maxWidth === -1 ? image.width : resizeConfiguration.maxWidth; @@ -196,14 +196,13 @@ async function resizeImage(element: WoltlabCoreFileUploadElement, file: File): P let fileType: string = resizeConfiguration.fileType; if (fileType === "image/jpeg" || fileType === "image/webp") { - fileType = "image/jpeg"; + fileType = "image/webp"; } else { fileType = file.type; } const resizedFile = await resizer.saveFile( { - exif, image: canvas, }, file.name, diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js index 63294d4969..d7960d7061 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js @@ -94,7 +94,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol }); const resizeConfiguration = JSON.parse(element.dataset.resizeConfiguration); const resizer = new Resizer_1.default(); - const { image, exif } = await resizer.loadFile(file); + const { image } = await resizer.loadFile(file); const maxHeight = resizeConfiguration.maxHeight === -1 ? image.height : resizeConfiguration.maxHeight; let maxWidth = resizeConfiguration.maxWidth === -1 ? image.width : resizeConfiguration.maxWidth; if (window.devicePixelRatio >= 2) { @@ -114,13 +114,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol } let fileType = resizeConfiguration.fileType; if (fileType === "image/jpeg" || fileType === "image/webp") { - fileType = "image/jpeg"; + fileType = "image/webp"; } else { fileType = file.type; } const resizedFile = await resizer.saveFile({ - exif, image: canvas, }, file.name, fileType, resizeConfiguration.quality); return resizedFile; From 06c0a354eaaa88c553941189c7d260582d04d137 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 18 Jul 2025 17:01:10 +0200 Subject: [PATCH 06/25] Always set the export quality to 80% 80% is the sweet spot for most images, producing the smallest possible file sizes without introducing too much visual reductions. --- com.woltlab.wcf/option.xml | 10 ++-------- constants.php | 1 - wcfsetup/install/files/lib/system/WCF.class.php | 3 +++ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/com.woltlab.wcf/option.xml b/com.woltlab.wcf/option.xml index c4e7174106..bc319fd6e2 100644 --- a/com.woltlab.wcf/option.xml +++ b/com.woltlab.wcf/option.xml @@ -926,14 +926,6 @@ redis:wcf.acp.option.cache_source_type.redis keep:wcf.acp.option.attachment_image_autoscale_file_type.keep image/jpeg:wcf.acp.option.attachment_image_autoscale_file_type.jpeg - +