From 1634452b591af7b4fbc2beedeb2520c7921d07b3 Mon Sep 17 00:00:00 2001 From: Don MacAskill Date: Fri, 27 Dec 2024 14:25:44 -0800 Subject: [PATCH 1/3] WIP. Implement CRC-32/ISO-HDLC support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supports CRC-32/ISO-HDLC (aka “crc32”) via FFI using the crc32fast Rust library for a >10X performance increase over PHP’s native crc32() implementation. --- Makefile | 27 ++- README.md | 38 ++-- cli/calculateCrc32IsoHdlc.php | 31 +++ cli/{calculate.php => calculateCrc64Nvme.php} | 8 +- composer.json | 3 +- crc32iso-hdlc-darwin.h | 12 ++ crc32iso-hdlc-linux.h | 12 ++ crc32iso-hdlc-windows.h | 12 ++ crc64nvme-darwin.h | 2 +- crc64nvme-linux.h | 2 +- crc64nvme-windows.h | 2 +- preload.php | 11 +- src/ChecksumTrait.php | 65 +++++++ src/Crc32/IsoHdlc/Computer.php | 95 ++++++++++ src/Crc32/IsoHdlc/Ffi.php | 21 ++ src/Crc64/Ffi.php | 35 ---- src/Crc64/{Nvme.php => Nvme/Computer.php} | 67 +------ src/Crc64/Nvme/Ffi.php | 21 ++ src/CrcInterface.php | 4 +- src/FfiInterface.php | 3 + src/FfiTrait.php | 18 ++ tests/unit/Crc32/IsoHdlc/ComputerTest.php | 179 ++++++++++++++++++ tests/unit/Crc32/IsoHdlc/FfiTest.php | 172 +++++++++++++++++ .../{NvmeTest.php => Nvme/ComputerTest.php} | 53 ++++-- tests/unit/Crc64/{ => Nvme}/FfiTest.php | 38 ++-- tests/unit/Definitions.php | 26 +++ 26 files changed, 796 insertions(+), 161 deletions(-) create mode 100644 cli/calculateCrc32IsoHdlc.php rename cli/{calculate.php => calculateCrc64Nvme.php} (62%) create mode 100644 crc32iso-hdlc-darwin.h create mode 100644 crc32iso-hdlc-linux.h create mode 100644 crc32iso-hdlc-windows.h create mode 100644 src/ChecksumTrait.php create mode 100644 src/Crc32/IsoHdlc/Computer.php create mode 100644 src/Crc32/IsoHdlc/Ffi.php delete mode 100644 src/Crc64/Ffi.php rename src/Crc64/{Nvme.php => Nvme/Computer.php} (63%) create mode 100644 src/Crc64/Nvme/Ffi.php create mode 100644 tests/unit/Crc32/IsoHdlc/ComputerTest.php create mode 100644 tests/unit/Crc32/IsoHdlc/FfiTest.php rename tests/unit/Crc64/{NvmeTest.php => Nvme/ComputerTest.php} (67%) rename tests/unit/Crc64/{ => Nvme}/FfiTest.php (77%) create mode 100644 tests/unit/Definitions.php diff --git a/Makefile b/Makefile index ce03080..b2d38a8 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,5 @@ .PHONY: build -build: - @if [ ! -d "./build" ]; then git clone https://github.com/awesomized/crc64fast-nvme.git build; fi - @cd build && git fetch && git checkout 1.1.0 - @cd build && cargo build --release +build: build-crc64nvme build-crc32isohdlc .PHONY: validate validate: phpcs php-cs-fixer-check static-analysis test cli @@ -46,14 +43,32 @@ phpunit: build .PHONY: cli cli: build - @echo "Should result in f8046e40c403f1d0:" - @php cli/calculate.php 'hello, world!' + @echo "CRC-64/NVME should result in f8046e40c403f1d0:" + @php cli/calculateCrc64Nvme.php 'hello, world!' + @echo "CRC-32/ISO-HDLC should result in 58988d13:" + @php cli/calculateCrc32IsoHdlc.php 'hello, world!' .PHONY: composer composer: # Psalm v5.26.1 doesn't like PHP-8.4 composer install --ignore-platform-req=php+ +.PHONY: build-directory +build-directory: + @if [ ! -d "./build" ]; then mkdir build; fi + +.PHONY: build-crc64nvme +build-crc64nvme: build-directory + @cd build && (if [ ! -d "./crc64fast-nvme" ]; then git clone https://github.com/awesomized/crc64fast-nvme.git; fi || true) + @cd build/crc64fast-nvme && git fetch && git checkout 1.1.0 + @cd build/crc64fast-nvme && cargo build --release + +.PHONY: build-crc32isohdlc +build-crc32ieee: build-directory + @cd build && (if [ ! -d "./crc32fast-lib-rust" ]; then git clone https://github.com/awesomized/crc32fast-lib-rust.git; fi || true) + @cd build/crc32fast-lib-rust && git fetch && git checkout 1.0.0 + @cd build/crc32fast-lib-rust && cargo build --release + .PHONY: clean clean: rm -rf build diff --git a/README.md b/README.md index 5df9b1f..d16108f 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,25 @@ [![Static Analysis](https://github.com/awesomized/crc-fast-php/actions/workflows/static-analysis.yml/badge.svg?branch=main)](https://github.com/awesomized/crc-fast-php/actions/workflows/static-analysis.yml) [![Unit Tests](https://github.com/awesomized/crc-fast-php/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/awesomized/crc-fast-php/actions/workflows/unit-tests.yml) -Fast, SIMD-accelerated CRC computation in PHP via FFI using Rust. Currently supports `CRC-64/NVME`, but will likely support other popular checksums over time, especially `CRC32`. +Fast, SIMD-accelerated CRC computation in PHP via FFI using Rust. Currently supports [CRC-64/NVME](https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-64-nvme) and [CRC-32/ISO-HDLC aka "crc32"](https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-32-iso-hdlc). Other implementations welcome via PR. ## CRC-64/NVME Uses the [crc64fast-nvme](https://github.com/awesomized/crc64fast-nvme) Rust package and its C-compatible shared library. It's capable of generating checksums at >20-50 GiB/s, depending on the CPU. It is much, much faster (>100X) than the native [crc32](https://www.php.net/manual/en/function.crc32.php), crc32b, and crc32c [implementations](https://www.php.net/manual/en/function.hash-algos.php) in PHP. -`CRC-64/NVME` is in use in a variety of large-scale and mission-critical systems, software, and hardware, such as: +[CRC-64/NVME](https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-64-nvme) is in use in a variety of large-scale and mission-critical systems, software, and hardware, such as: - AWS S3's [recommended checksum](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html) - The [Linux kernel](https://github.com/torvalds/linux/blob/786c8248dbd33a5a7a07f7c6e55a7bfc68d2ca48/lib/crc64.c#L66-L73) - The [NVMe specification](https://nvmexpress.org/wp-content/uploads/NVM-Express-NVM-Command-Set-Specification-1.0d-2023.12.28-Ratified.pdf) +## CRC-32/ISO-HDLC (aka "crc32") +Uses the [crc32fast-lib]() Rust package (which exposes the [crc32fast](https://github.com/srijs/rust-crc32fast) Rust library as a C-compatible shared library). + +It's >10X faster than PHP's native [crc32](https://www.php.net/manual/en/function.crc32.php) implementation. + +[CRC-32/ISO-HDLC](https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-32-iso-hdlc) is the de-facto "crc32" checksum, though there are [many other 32-bit variants](https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat-bits.32). + ## Changes See the [change log](CHANGELOG.md). @@ -34,16 +41,19 @@ composer require awesomized/crc-fast ## Usage +Examples are for `CRC-64/NVME`, but `CRC-32/ISO-HDLC` is nearly identical, just in a different namespace (`Awesomized\Checksums\Crc32\IsoHdlc`). + ### Creating the CRC-64/NVME FFI object A [helper FFI Class](src/Ffi.php) is provided, which supplies many ways to easily create an FFI object for the [crc64fast-nvme](https://github.com/awesomized/crc64fast-nvme) shared library: #### - Via [preloaded](https://www.php.net/manual/en/ffi.examples-complete.php) shared library (recommended for any long-running workloads, such as web requests): + ```php -use Awesomized\Checksums\Crc64; +use Awesomized\Checksums\Crc64\Nvme; // uses the opcache preloaded shared library and PHP Class(es) -$crc64Fast = Crc64\Ffi::fromPreloadedScope( +$crc64Fast = Nvme\Ffi::fromPreloadedScope( scope: 'CRC64NVME', // optional, this is the default ); ``` @@ -52,20 +62,21 @@ $crc64Fast = Crc64\Ffi::fromPreloadedScope( Uses a C header file to define the functions and point to the shared library (`.so` on Linux, `.dll` on Windows, `.dylib` on macOS, etc). ```php -use Awesomized\Checksums\Crc64; +use Awesomized\Checksums\Crc64\Nvme; // uses the FFI_LIB and FFI_SCOPE definitions in the header file -$crc64Fast = Crc64\Ffi::fromHeaderFile( +$crc64Fast = Nvme\Ffi::fromHeaderFile( headerFile: 'path/to/crc64fast_nvme.h', // optional, can likely be inferred from the OS ); ``` #### - Via C definitions + library: + ```php -use Awesomized\Checksums\Crc64; +use Awesomized\Checksums\Crc64\Nvme; // uses the supplied C definitions and name/location of the shared library -$crc64Fast = Crc64\Ffi::fromCode( +$crc64Fast = Nvme\Ffi::fromCode( code: 'typedef struct DigestHandle DigestHandle; DigestHandle* digest_new(void); void digest_write(DigestHandle* handle, const char* data, size_t len); @@ -79,31 +90,32 @@ $crc64Fast = Crc64\Ffi::fromCode( #### Calculate CRC-64/NVME checksums: ```php -use Awesomized\Checksums\Crc64; +use Awesomized\Checksums\Crc64\Nvme; /** @var \FFI $crc64Fast */ // calculate the checksum of a string -$checksum = Crc64\Nvme::calculate( +$checksum = Nvme\Computer::calculate( ffi: $crc64Fast, string: 'hello, world!' ); // f8046e40c403f1d0 // calculate the checksum of a file, which will chunk through the file optimally, // limiting RAM usage and maximizing throughput -$checksum = Crc64\Nvme::calculateFile( +$checksum = Nvme\Computer::calculateFile( ffi: $crc64Fast, filename: 'path/to/hello-world' ); // f8046e40c403f1d0 ``` #### Calculate CRC-64/NVME checksums with a Digest for intermittent / streaming / etc workloads: + ```php -use Awesomized\Checksums\Crc64; +use Awesomized\Checksums\Crc64\Nvme; /** @var \FFI $crc64FastNvme */ -$crc64Digest = new Crc64\Nvme( +$crc64Digest = new Nvme\Computer( crc64Nvme: $crc64FastNvme, ); diff --git a/cli/calculateCrc32IsoHdlc.php b/cli/calculateCrc32IsoHdlc.php new file mode 100644 index 0000000..07c4d31 --- /dev/null +++ b/cli/calculateCrc32IsoHdlc.php @@ -0,0 +1,31 @@ +' . PHP_EOL; + + exit(1); +} + +require __DIR__ . '/../vendor/autoload.php'; + +$ffi = Crc32\IsoHdlc\Ffi::fromHeaderFile(); + +if (is_readable($argv[1])) { + echo Crc32\IsoHdlc\Computer::calculateFile( + ffi: $ffi, + filename: $argv[1], + ) . PHP_EOL; + + exit(0); +} + +echo Crc32\IsoHdlc\Computer::calculate( + ffi: $ffi, + string: $argv[1], +) . PHP_EOL; + +$contents = file_get_contents('/Users/onethumb/Downloads/frankenphp-mac-arm64'); diff --git a/cli/calculate.php b/cli/calculateCrc64Nvme.php similarity index 62% rename from cli/calculate.php rename to cli/calculateCrc64Nvme.php index 5d398c3..7cedf79 100644 --- a/cli/calculate.php +++ b/cli/calculateCrc64Nvme.php @@ -5,17 +5,17 @@ use Awesomized\Checksums\Crc64; if (!isset($argv[1]) || '' === $argv[1]) { - echo 'Usage: php calculateString.php ' . PHP_EOL; + echo 'Usage: php calculateCrc64Nvme.php ' . PHP_EOL; exit(1); } require __DIR__ . '/../vendor/autoload.php'; -$ffi = Crc64\Ffi::fromHeaderFile(); +$ffi = Crc64\Nvme\Ffi::fromHeaderFile(); if (is_readable($argv[1])) { - echo Crc64\Nvme::calculateFile( + echo Crc64\Nvme\Computer::calculateFile( ffi: $ffi, filename: $argv[1], ) . PHP_EOL; @@ -23,7 +23,7 @@ exit(0); } -echo Crc64\Nvme::calculate( +echo Crc64\Nvme\Computer::calculate( ffi: $ffi, string: $argv[1], ) . PHP_EOL; diff --git a/composer.json b/composer.json index 0517c15..11ab717 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ } }, "require": { - "php": "^8.3||^8.4" + "php": "^8.3||^8.4", + "ext-ffi": "*" }, "autoload": { "psr-4": { diff --git a/crc32iso-hdlc-darwin.h b/crc32iso-hdlc-darwin.h new file mode 100644 index 0000000..53d5171 --- /dev/null +++ b/crc32iso-hdlc-darwin.h @@ -0,0 +1,12 @@ +#define FFI_SCOPE "CRC32ISOHDLC" +#define FFI_LIB "build/crc32fast-lib-rust/target/release/libcrc32fast_lib.dylib" + +typedef struct HasherHandle HasherHandle; + +HasherHandle *hasher_new(); + +void hasher_write(HasherHandle *handle, const char *data, uintptr_t len); + +uint32_t hasher_finalize(HasherHandle *handle); + +uint32_t crc32_hash(const char *data, uintptr_t len); diff --git a/crc32iso-hdlc-linux.h b/crc32iso-hdlc-linux.h new file mode 100644 index 0000000..3217984 --- /dev/null +++ b/crc32iso-hdlc-linux.h @@ -0,0 +1,12 @@ +#define FFI_SCOPE "CRC32ISOHDLC" +#define FFI_LIB "build/crc32fast-lib-rust/target/release/libcrc32fast_lib.so" + +typedef struct HasherHandle HasherHandle; + +HasherHandle *hasher_new(); + +void hasher_write(HasherHandle *handle, const char *data, uintptr_t len); + +uint32_t hasher_finalize(HasherHandle *handle); + +uint32_t crc32_hash(const char *data, uintptr_t len); diff --git a/crc32iso-hdlc-windows.h b/crc32iso-hdlc-windows.h new file mode 100644 index 0000000..a186b84 --- /dev/null +++ b/crc32iso-hdlc-windows.h @@ -0,0 +1,12 @@ +#define FFI_SCOPE "CRC32ISOHDLC" +#define FFI_LIB "build/crc32fast-lib-rust/target/release/libcrc32fast_lib.dll" + +typedef struct HasherHandle HasherHandle; + +HasherHandle *hasher_new(); + +void hasher_write(HasherHandle *handle, const char *data, uintptr_t len); + +uint32_t hasher_finalize(HasherHandle *handle); + +uint32_t crc32_hash(const char *data, uintptr_t len); diff --git a/crc64nvme-darwin.h b/crc64nvme-darwin.h index 43aa3af..077a325 100644 --- a/crc64nvme-darwin.h +++ b/crc64nvme-darwin.h @@ -1,5 +1,5 @@ #define FFI_SCOPE "CRC64NVME" -#define FFI_LIB "build/target/release/libcrc64fast_nvme.dylib" +#define FFI_LIB "build/crc64fast-nvme/target/release/libcrc64fast_nvme.dylib" typedef struct DigestHandle DigestHandle; diff --git a/crc64nvme-linux.h b/crc64nvme-linux.h index b838bfb..32b6ab6 100644 --- a/crc64nvme-linux.h +++ b/crc64nvme-linux.h @@ -1,5 +1,5 @@ #define FFI_SCOPE "CRC64NVME" -#define FFI_LIB "build/target/release/libcrc64fast_nvme.so" +#define FFI_LIB "build/crc64fast-nvme/target/release/libcrc64fast_nvme.so" typedef struct DigestHandle DigestHandle; diff --git a/crc64nvme-windows.h b/crc64nvme-windows.h index ac82649..8d56ddf 100644 --- a/crc64nvme-windows.h +++ b/crc64nvme-windows.h @@ -1,5 +1,5 @@ #define FFI_SCOPE "CRC64NVME" -#define FFI_LIB "build/target/release/libcrc64fast_nvme.dll" +#define FFI_LIB "build/crc64fast-nvme/target/release/libcrc64fast_nvme.dll" typedef struct DigestHandle DigestHandle; diff --git a/preload.php b/preload.php index 44e12a2..bd232c1 100644 --- a/preload.php +++ b/preload.php @@ -11,12 +11,17 @@ declare(strict_types=1); -use Awesomized\Checksums\Crc64; +use Awesomized\Checksums; require 'src/FfiInterface.php'; require 'src/FfiTrait.php'; -require 'src/Crc64/Ffi.php'; +require 'src/Crc64/Nvme/Ffi.php'; +require 'src/Crc32/IsoHdlc/Ffi.php'; \FFI::load( - Crc64\Ffi::whichHeaderFile(), + Checksums\Crc64\Nvme\Ffi::whichHeaderFile(), +); + +\FFI::load( + Checksums\Crc32\IsoHdlc\Ffi::whichHeaderFile(), ); diff --git a/src/ChecksumTrait.php b/src/ChecksumTrait.php new file mode 100644 index 0000000..fd34c46 --- /dev/null +++ b/src/ChecksumTrait.php @@ -0,0 +1,65 @@ +write( + string: $string, + ) + ->sum(); + } + + public static function calculateFile( + FFI $ffi, + string $filename, + int $readChunkSize = self::READ_CHUNK_SIZE_DEFAULT, + ): string { + $handle = fopen( + filename: $filename, + mode: 'rb', + ); + + if (false === $handle) { + throw new \InvalidArgumentException( + message: "Could not open file: {$filename}", + ); + } + + $computer = new self($ffi); + + while ( + !feof( + stream: $handle, + ) + ) { + $chunk = fread( + stream: $handle, + length: $readChunkSize, + ); + if (false !== $chunk) { + $computer->write( + string: $chunk, + ); + } + } + + fclose( + stream: $handle, + ); + + return $computer->sum(); + } +} diff --git a/src/Crc32/IsoHdlc/Computer.php b/src/Crc32/IsoHdlc/Computer.php new file mode 100644 index 0000000..921b8c1 --- /dev/null +++ b/src/Crc32/IsoHdlc/Computer.php @@ -0,0 +1,95 @@ +crc32IsoHdlc->hasher_new(); + } catch (FFI\Exception $e) { + throw new \InvalidArgumentException( + message: 'Could not create a new Hasher handle.' + . ' Is the library loaded, and has the hasher_new() method?', + previous: $e, + ); + } + + $this->hasherHandle = $digestHandle; + } + + public function write( + string $string, + ): self { + try { + /** @psalm-suppress UndefinedMethod - already checked this in the ctor */ + // @phpstan-ignore-next-line + $this->crc32IsoHdlc->hasher_write( + $this->hasherHandle, + $string, + \strlen($string), + ); + } catch (FFI\Exception $e) { + throw new \RuntimeException( + message: 'Could not write to the Digest handle. ' + . 'Is the library loaded, and has the digest_write() method?', + previous: $e, + ); + } + + return $this; + } + + public function sum(): string + { + try { + /** + * @var int $crc32 + * + * @psalm-suppress UndefinedMethod - already checked this in the ctor + */ + // @phpstan-ignore-next-line + $crc32 = $this->crc32IsoHdlc->hasher_finalize( + $this->hasherHandle, + ); + } catch (FFI\Exception $e) { + throw new \RuntimeException( + message: 'Could not calculate the CRC-32 checksum. ' + . ' Is the library loaded, and has the hasher_finalize() method?', + previous: $e, + ); + } + + return \sprintf( + '%08x', + $crc32, + ); + } +} diff --git a/src/Crc32/IsoHdlc/Ffi.php b/src/Crc32/IsoHdlc/Ffi.php new file mode 100644 index 0000000..844c0c2 --- /dev/null +++ b/src/Crc32/IsoHdlc/Ffi.php @@ -0,0 +1,21 @@ + 'crc64nvme-darwin.h', - 'Windows' => 'crc64nvme-windows.h', - default => 'crc64nvme-linux.h', - }; - } - - public static function whichLibrary(): string - { - return match (PHP_OS_FAMILY) { - 'Darwin' => 'libcrc64fast_nvme.dylib', - 'Windows' => 'libcrc64fast_nvme.dll', - default => 'libcrc64fast_nvme.so', - }; - } -} diff --git a/src/Crc64/Nvme.php b/src/Crc64/Nvme/Computer.php similarity index 63% rename from src/Crc64/Nvme.php rename to src/Crc64/Nvme/Computer.php index 65090b0..ffcab30 100644 --- a/src/Crc64/Nvme.php +++ b/src/Crc64/Nvme/Computer.php @@ -2,19 +2,22 @@ declare(strict_types=1); -namespace Awesomized\Checksums\Crc64; +namespace Awesomized\Checksums\Crc64\Nvme; use Awesomized\Checksums; use FFI; /** - * A wrapper around the CRC-64 NVMe FFI library. + * A wrapper around the CRC-64/NVME FFI library. * - * @see \Awesomized\Checksums\Crc64\Ffi + * @see \Awesomized\Checksums\Crc64\Nvme\Ffi * @link https://github.com/awesomized/crc64fast-nvme + * @link https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-64-nvme */ -final class Nvme implements Checksums\CrcInterface +final class Computer implements Checksums\CrcInterface { + use Checksums\ChecksumTrait; + private FFI\CData $digestHandle; /** @@ -44,62 +47,6 @@ public function __construct( $this->digestHandle = $digestHandle; } - public static function calculate( - FFI $ffi, - string $string, - ): string { - return (new self( - crc64Nvme: $ffi, - )) - ->write( - string: $string, - ) - ->sum(); - } - - public static function calculateFile( - FFI $ffi, - string $filename, - int $readChunkSize = self::READ_CHUNK_SIZE_DEFAULT, - ): string { - $handle = fopen( - filename: $filename, - mode: 'rb', - ); - - if (false === $handle) { - throw new \InvalidArgumentException( - message: "Could not open file: {$filename}", - ); - } - - $nvme = new self( - crc64Nvme: $ffi, - ); - - while ( - !feof( - stream: $handle, - ) - ) { - $chunk = fread( - stream: $handle, - length: $readChunkSize, - ); - if (false !== $chunk) { - $nvme->write( - string: $chunk, - ); - } - } - - fclose( - stream: $handle, - ); - - return $nvme->sum(); - } - public function write( string $string, ): self { diff --git a/src/Crc64/Nvme/Ffi.php b/src/Crc64/Nvme/Ffi.php new file mode 100644 index 0000000..4092d6b --- /dev/null +++ b/src/Crc64/Nvme/Ffi.php @@ -0,0 +1,21 @@ + self::PREFIX_HEADER . '-darwin.h', + self::OS_WINDOWS => self::PREFIX_HEADER . '-windows.h', + default => self::PREFIX_HEADER . '-linux.h', + }; + } + + public static function whichLibrary(): string + { + return match (PHP_OS_FAMILY) { + self::OS_DARWIN => self::PREFIX_LIB . '.dylib', + self::OS_WINDOWS => self::PREFIX_LIB . '.dll', + default => self::PREFIX_LIB . '.so', + }; + } } diff --git a/tests/unit/Crc32/IsoHdlc/ComputerTest.php b/tests/unit/Crc32/IsoHdlc/ComputerTest.php new file mode 100644 index 0000000..aaaa89a --- /dev/null +++ b/tests/unit/Crc32/IsoHdlc/ComputerTest.php @@ -0,0 +1,179 @@ +ffi = Crc32\IsoHdlc\Ffi::fromHeaderFile(); + } + + /** + * @throws \InvalidArgumentException + * @throws \FFI\Exception + */ + public function testConstructorInvalidLibraryShouldFail(): void + { + $this->expectException(\InvalidArgumentException::class); + + $ffi = \FFI::cdef(); + + new Crc32\IsoHdlc\Computer( + crc32IsoHdlc: $ffi, + ); + } + + /** + * @depends testConstructorInvalidLibraryShouldFail + * + * @throws \InvalidArgumentException + */ + public function testConstructorValidLibraryShouldSucceed(): void + { + $this->expectNotToPerformAssertions(); + + $ffi = new Crc32\IsoHdlc\Computer( + crc32IsoHdlc: $this->ffi, + ); + } + + /** + * @depends testConstructorValidLibraryShouldSucceed + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ + public function testCalculateHelloWorldShouldSucceed(): void + { + $crc64 = Crc32\IsoHdlc\Computer::calculate( + ffi: $this->ffi, + string: Definitions::HELLO_WORLD, + ); + + self::assertSame( + Definitions::HELLO_WORLD_CRC32_ISO_HDLC, + $crc64, + ); + } + + /** + * @depends testConstructorValidLibraryShouldSucceed + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ + public function testCalculateFileHelloWorldShouldSucceed(): void + { + $crc64 = Crc32\IsoHdlc\Computer::calculateFile( + ffi: $this->ffi, + filename: Definitions::HELLO_WORLD_FILE, + ); + + self::assertSame( + Definitions::HELLO_WORLD_CRC32_ISO_HDLC, + $crc64, + ); + } + + /** + * Ensure that binary data is calculated properly, especially null bytes (0x00), which has been problematic in the + * past. + * + * @depends testConstructorValidLibraryShouldSucceed + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @throws RandomException + */ + public function testCalculateBinaryDataShouldSucceed(): void + { + $crc64 = Crc32\IsoHdlc\Computer::calculate( + ffi: $this->ffi, + string: 0x00 . random_bytes(1024 * 1024), + ); + + self::assertNotSame('0000000000000000', $crc64); + } + + /** + * @depends testConstructorValidLibraryShouldSucceed + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ + public function testCalculateChunkedDataShouldSucceed(): void + { + $crc64Nvme = new Crc32\IsoHdlc\Computer( + crc32IsoHdlc: $this->ffi, + ); + + $crc64Nvme->write('hello, '); + $crc64Nvme->write('world!'); + + self::assertSame( + Definitions::HELLO_WORLD_CRC32_ISO_HDLC, + $crc64Nvme->sum(), + ); + } + + /** + * @depends testConstructorValidLibraryShouldSucceed + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ + public function testCalculateCheckValueShouldMatch(): void + { + $crc32 = Crc32\IsoHdlc\Computer::calculate( + ffi: $this->ffi, + string: Definitions::CHECK_INPUT, + ); + + self::assertSame( + Definitions::CHECK_RESULT_CRC32_ISO_HDLC, + $crc32, + ); + } + + /** + * @depends testConstructorValidLibraryShouldSucceed + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ + public function testComparePhpFunctionKnownValuesShouldMatch(): void + { + self::assertSame( + dechex(crc32(Definitions::HELLO_WORLD)), + Crc32\IsoHdlc\Computer::calculate( + ffi: $this->ffi, + string: Definitions::HELLO_WORLD, + ), + ); + + self::assertSame( + dechex(crc32(Definitions::CHECK_INPUT)), + Crc32\IsoHdlc\Computer::calculate( + ffi: $this->ffi, + string: Definitions::CHECK_INPUT, + ), + ); + } +} diff --git a/tests/unit/Crc32/IsoHdlc/FfiTest.php b/tests/unit/Crc32/IsoHdlc/FfiTest.php new file mode 100644 index 0000000..826d54b --- /dev/null +++ b/tests/unit/Crc32/IsoHdlc/FfiTest.php @@ -0,0 +1,172 @@ +expectException(Exception::class); + + $ffi = Crc32\IsoHdlc\Ffi::fromCode( + code: '', + library: __DIR__ + . '/../../../../build/crc32fast-lib-rust/target/release/' + . Crc32\IsoHdlc\Ffi::whichLibrary(), + ); + + /** + * @psalm-suppress UndefinedMethod - from FFI, so Psalm and PHPStan can't know if the method exists + */ + // @phpstan-ignore-next-line + $ffi->hasher_new(); + } + + /** + * @depends testFfiFromCodeInvalidCodeShouldFail + * + * @throws Exception + */ + public function testFfiFromCodeInvalidLibraryShouldFail(): void + { + $this->expectException(Exception::class); + + $code = file_get_contents( + __DIR__ . '/../../../../' . Crc32\IsoHdlc\Ffi::whichHeaderFile(), + ); + + if (false === $code) { + self::markTestSkipped('Could not read the header file ' . Crc32\IsoHdlc\Ffi::whichHeaderFile()); + } + + $ffi = Crc32\IsoHdlc\Ffi::fromCode( + code: $code, + library: 'bogus', + ); + + /** + * @psalm-suppress UndefinedMethod - from FFI, so Psalm and PHPStan can't know if the method exists + */ + // @phpstan-ignore-next-line + $ffi->hasher_new(); + } + + /** + * @depends testFfiFromCodeInvalidLibraryShouldFail + * + * @throws \InvalidArgumentException + */ + public function testFfiFromHeaderInvalidHeaderShouldFail(): void + { + $this->expectException(Exception::class); + + $ffi = Crc32\IsoHdlc\Ffi::fromHeaderFile( + headerFile: __DIR__ . '/FfiTest.php', + ); + + $this->testFfiCalculateCrc32ShouldSucceed($ffi); + } + + /** + * @depends testFfiFromHeaderInvalidHeaderShouldFail + * + * @throws Exception + */ + public function testFfiFromCodeValidInputShouldSucceed(): void + { + $code = file_get_contents(Crc32\IsoHdlc\Ffi::whichHeaderFile()); + if (false === $code) { + self::markTestSkipped('Could not read the header file ' . Crc32\IsoHdlc\Ffi::whichHeaderFile()); + } + + $ffi = Crc32\IsoHdlc\Ffi::fromCode( + code: $code, + library: __DIR__ + . '/../../../../build/crc32fast-lib-rust/target/release/' + . Crc32\IsoHdlc\Ffi::whichLibrary(), + ); + + $this->testFfiCalculateCrc32ShouldSucceed($ffi); + } + + /** + * @depends testFfiFromCodeValidInputShouldSucceed + * + * @throws Exception + * @throws \InvalidArgumentException + */ + public function testFfiFromHeaderValidHeaderShouldSucceed(): void + { + $ffi = Crc32\IsoHdlc\Ffi::fromHeaderFile(); + + $this->testFfiCalculateCrc32ShouldSucceed($ffi); + } + + /** + * @depends testFfiFromHeaderValidHeaderShouldSucceed + * + * @throws Exception + */ + public function testFfiFromPreloadScopeValidScopeShouldSucceed(): void + { + $opcachePreload = \ini_get('opcache.preload'); + if (false === $opcachePreload || '' === $opcachePreload) { + self::markTestSkipped('opcache.preload is not enabled.'); + } + + try { + $ffi = Crc32\IsoHdlc\Ffi::fromPreloadScope(); + } catch (\FFI\Exception $e) { + self::markTestSkipped("FFI instance doesn't appear to be preloaded."); + } + + $this->testFfiCalculateCrc32ShouldSucceed($ffi); + } + + /** + * @depends testFfiFromCodeValidInputShouldSucceed + */ + private function testFfiCalculateCrc32ShouldSucceed( + \FFI $ffi, + ): void { + /** + * @psalm-suppress UndefinedMethod - from FFI, so Psalm and PHPStan can't know if the method exists + */ + // @phpstan-ignore-next-line + $digest = $ffi->hasher_new(); + + self::assertInstanceOf(\FFI\CData::class, $digest); + + /** + * @psalm-suppress UndefinedMethod - from FFI, so Psalm and PHPStan can't know if the method exists + */ + // @phpstan-ignore-next-line + $ffi->hasher_write($digest, Definitions::HELLO_WORLD, Definitions::HELLO_WORLD_LENGTH); + + /** + * @psalm-suppress UndefinedMethod - from FFI, so Psalm and PHPStan can't know if the method exists + */ + // @phpstan-ignore-next-line + self::assertSame( + Definitions::HELLO_WORLD_CRC32_ISO_HDLC, + \sprintf( + '%08x', + // @phpstan-ignore-next-line + $ffi->hasher_finalize($digest), + ), + ); + } +} diff --git a/tests/unit/Crc64/NvmeTest.php b/tests/unit/Crc64/Nvme/ComputerTest.php similarity index 67% rename from tests/unit/Crc64/NvmeTest.php rename to tests/unit/Crc64/Nvme/ComputerTest.php index 01645ee..e0e6f17 100644 --- a/tests/unit/Crc64/NvmeTest.php +++ b/tests/unit/Crc64/Nvme/ComputerTest.php @@ -2,9 +2,10 @@ declare(strict_types=1); -namespace Awesomized\Checksums\tests\unit\Crc64; +namespace Awesomized\Checksums\tests\unit\Crc64\Nvme; use Awesomized\Checksums\Crc64; +use Awesomized\Checksums\tests\unit\Definitions; use FFI; use PHPUnit\Framework\TestCase; use Random\RandomException; @@ -12,13 +13,8 @@ /** * @internal */ -final class NvmeTest extends TestCase +final class ComputerTest extends TestCase { - public const string HELLO_WORLD = 'hello, world!'; - public const int HELLO_WORLD_LENGTH = 13; - public const string HELLO_WORLD_CRC64 = 'f8046e40c403f1d0'; - public const string HELLO_WORLD_FILE = __DIR__ . '/../../fixtures/hello-world.txt'; - private FFI $ffi; /** @@ -26,7 +22,7 @@ final class NvmeTest extends TestCase */ protected function setUp(): void { - $this->ffi = Crc64\Ffi::fromHeaderFile(); + $this->ffi = Crc64\Nvme\Ffi::fromHeaderFile(); } /** @@ -39,7 +35,7 @@ public function testConstructorInvalidLibraryShouldFail(): void $ffi = \FFI::cdef(); - new Crc64\Nvme( + new Crc64\Nvme\Computer( crc64Nvme: $ffi, ); } @@ -53,7 +49,7 @@ public function testConstructorValidLibraryShouldSucceed(): void { $this->expectNotToPerformAssertions(); - $crc64Nvme = new Crc64\Nvme( + $crc64Nvme = new Crc64\Nvme\Computer( crc64Nvme: $this->ffi, ); } @@ -66,13 +62,13 @@ public function testConstructorValidLibraryShouldSucceed(): void */ public function testCalculateHelloWorldShouldSucceed(): void { - $crc64 = Crc64\Nvme::calculate( + $crc64 = Crc64\Nvme\Computer::calculate( ffi: $this->ffi, - string: self::HELLO_WORLD, + string: Definitions::HELLO_WORLD, ); self::assertSame( - self::HELLO_WORLD_CRC64, + Definitions::HELLO_WORLD_CRC64_NVME, $crc64, ); } @@ -85,13 +81,13 @@ public function testCalculateHelloWorldShouldSucceed(): void */ public function testCalculateFileHelloWorldShouldSucceed(): void { - $crc64 = Crc64\Nvme::calculateFile( + $crc64 = Crc64\Nvme\Computer::calculateFile( ffi: $this->ffi, - filename: self::HELLO_WORLD_FILE, + filename: Definitions::HELLO_WORLD_FILE, ); self::assertSame( - self::HELLO_WORLD_CRC64, + Definitions::HELLO_WORLD_CRC64_NVME, $crc64, ); } @@ -108,7 +104,7 @@ public function testCalculateFileHelloWorldShouldSucceed(): void */ public function testCalculateBinaryDataShouldSucceed(): void { - $crc64 = Crc64\Nvme::calculate( + $crc64 = Crc64\Nvme\Computer::calculate( ffi: $this->ffi, string: 0x00 . random_bytes(1024 * 1024), ); @@ -124,7 +120,7 @@ public function testCalculateBinaryDataShouldSucceed(): void */ public function testCalculateChunkedDataShouldSucceed(): void { - $crc64Nvme = new Crc64\Nvme( + $crc64Nvme = new Crc64\Nvme\Computer( crc64Nvme: $this->ffi, ); @@ -132,8 +128,27 @@ public function testCalculateChunkedDataShouldSucceed(): void $crc64Nvme->write('world!'); self::assertSame( - self::HELLO_WORLD_CRC64, + Definitions::HELLO_WORLD_CRC64_NVME, $crc64Nvme->sum(), ); } + + /** + * @depends testConstructorValidLibraryShouldSucceed + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ + public function testCalculateCheckValueShouldMatch(): void + { + $crc64 = Crc64\Nvme\Computer::calculate( + ffi: $this->ffi, + string: Definitions::CHECK_INPUT, + ); + + self::assertSame( + Definitions::CHECK_RESULT_CRC64_NVME, + $crc64, + ); + } } diff --git a/tests/unit/Crc64/FfiTest.php b/tests/unit/Crc64/Nvme/FfiTest.php similarity index 77% rename from tests/unit/Crc64/FfiTest.php rename to tests/unit/Crc64/Nvme/FfiTest.php index c9cdf47..5a729e3 100644 --- a/tests/unit/Crc64/FfiTest.php +++ b/tests/unit/Crc64/Nvme/FfiTest.php @@ -2,9 +2,11 @@ declare(strict_types=1); -namespace Awesomized\Checksums\tests\unit\Crc64; +namespace Awesomized\Checksums\tests\unit\Crc64\Nvme; use Awesomized\Checksums\Crc64; +use Awesomized\Checksums\tests\unit\Definitions; +use FFI\CData; use FFI\Exception; use PHPUnit\Framework\TestCase; @@ -20,9 +22,9 @@ public function testFfiFromCodeInvalidCodeShouldFail(): void { $this->expectException(Exception::class); - $ffi = Crc64\Ffi::fromCode( + $ffi = Crc64\Nvme\Ffi::fromCode( code: '', - library: __DIR__ . '/../../../build/target/release/' . Crc64\Ffi::whichLibrary(), + library: __DIR__ . '/../../../../build/crc64fast-nvme/target/release/' . Crc64\Nvme\Ffi::whichLibrary(), ); /** @@ -42,14 +44,14 @@ public function testFfiFromCodeInvalidLibraryShouldFail(): void $this->expectException(Exception::class); $code = file_get_contents( - __DIR__ . '/../../../' . Crc64\Ffi::whichHeaderFile(), + __DIR__ . '/../../../../' . Crc64\Nvme\Ffi::whichHeaderFile(), ); if (false === $code) { - self::markTestSkipped('Could not read the header file ' . Crc64\Ffi::whichHeaderFile()); + self::markTestSkipped('Could not read the header file ' . Crc64\Nvme\Ffi::whichHeaderFile()); } - $ffi = Crc64\Ffi::fromCode( + $ffi = Crc64\Nvme\Ffi::fromCode( code: $code, library: 'bogus', ); @@ -70,7 +72,7 @@ public function testFfiFromHeaderInvalidHeaderShouldFail(): void { $this->expectException(Exception::class); - $ffi = Crc64\Ffi::fromHeaderFile( + $ffi = Crc64\Nvme\Ffi::fromHeaderFile( headerFile: __DIR__ . '/FfiTest.php', ); @@ -84,14 +86,14 @@ public function testFfiFromHeaderInvalidHeaderShouldFail(): void */ public function testFfiFromCodeValidInputShouldSucceed(): void { - $code = file_get_contents(Crc64\Ffi::whichHeaderFile()); + $code = file_get_contents(Crc64\Nvme\Ffi::whichHeaderFile()); if (false === $code) { - self::markTestSkipped('Could not read the header file ' . Crc64\Ffi::whichHeaderFile()); + self::markTestSkipped('Could not read the header file ' . Crc64\Nvme\Ffi::whichHeaderFile()); } - $ffi = Crc64\Ffi::fromCode( + $ffi = Crc64\Nvme\Ffi::fromCode( code: $code, - library: __DIR__ . '/../../../build/target/release/' . Crc64\Ffi::whichLibrary(), + library: __DIR__ . '/../../../../build/crc64fast-nvme/target/release/' . Crc64\Nvme\Ffi::whichLibrary(), ); $this->testFfiCalculateCrc64ShouldSucceed($ffi); @@ -105,7 +107,7 @@ public function testFfiFromCodeValidInputShouldSucceed(): void */ public function testFfiFromHeaderValidHeaderShouldSucceed(): void { - $ffi = Crc64\Ffi::fromHeaderFile(); + $ffi = Crc64\Nvme\Ffi::fromHeaderFile(); $this->testFfiCalculateCrc64ShouldSucceed($ffi); } @@ -123,7 +125,7 @@ public function testFfiFromPreloadScopeValidScopeShouldSucceed(): void } try { - $ffi = Crc64\Ffi::fromPreloadScope(); + $ffi = Crc64\Nvme\Ffi::fromPreloadScope(); } catch (\FFI\Exception $e) { self::markTestSkipped("FFI instance doesn't appear to be preloaded."); } @@ -143,20 +145,24 @@ private function testFfiCalculateCrc64ShouldSucceed( // @phpstan-ignore-next-line $digest = $ffi->digest_new(); - self::assertInstanceOf(\FFI\CData::class, $digest); + self::assertInstanceOf(CData::class, $digest); /** * @psalm-suppress UndefinedMethod - from FFI, so Psalm and PHPStan can't know if the method exists */ // @phpstan-ignore-next-line - $ffi->digest_write($digest, NvmeTest::HELLO_WORLD, NvmeTest::HELLO_WORLD_LENGTH); + $ffi->digest_write( + $digest, + Definitions::HELLO_WORLD, + Definitions::HELLO_WORLD_LENGTH, + ); /** * @psalm-suppress UndefinedMethod - from FFI, so Psalm and PHPStan can't know if the method exists */ // @phpstan-ignore-next-line self::assertSame( - NvmeTest::HELLO_WORLD_CRC64, + Definitions::HELLO_WORLD_CRC64_NVME, \sprintf( '%016x', // @phpstan-ignore-next-line diff --git a/tests/unit/Definitions.php b/tests/unit/Definitions.php new file mode 100644 index 0000000..7f789b6 --- /dev/null +++ b/tests/unit/Definitions.php @@ -0,0 +1,26 @@ + Date: Fri, 27 Dec 2024 15:45:27 -0800 Subject: [PATCH 2/3] Fix wrong file placement --- .github/{workflows => }/PULL_REQUEST_TEMPLATE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => }/PULL_REQUEST_TEMPLATE.md (100%) diff --git a/.github/workflows/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/workflows/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md From 5b1de20658357a296042e1da48ae633ddc9ccdc9 Mon Sep 17 00:00:00 2001 From: Don MacAskill Date: Fri, 27 Dec 2024 15:45:34 -0800 Subject: [PATCH 3/3] Fix typo --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b2d38a8..f9e0d17 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ build-crc64nvme: build-directory @cd build/crc64fast-nvme && cargo build --release .PHONY: build-crc32isohdlc -build-crc32ieee: build-directory +build-crc32isohdlc: build-directory @cd build && (if [ ! -d "./crc32fast-lib-rust" ]; then git clone https://github.com/awesomized/crc32fast-lib-rust.git; fi || true) @cd build/crc32fast-lib-rust && git fetch && git checkout 1.0.0 @cd build/crc32fast-lib-rust && cargo build --release