From 115dcc129c12a1d20bbd2fd738c2422a839d0e7f Mon Sep 17 00:00:00 2001 From: Sara Golemon Date: Sat, 17 Oct 2020 01:30:56 +0000 Subject: [PATCH] Add an OOP API to HashContext --- ext/hash/hash.c | 295 +++++++++++++++++++++++--------- ext/hash/hash.stub.php | 11 +- ext/hash/hash_arginfo.h | 40 ++++- ext/hash/tests/hash-oop.phpt | 65 +++++++ ext/hash/tests/new-context.phpt | 13 -- 5 files changed, 325 insertions(+), 99 deletions(-) create mode 100644 ext/hash/tests/hash-oop.phpt delete mode 100644 ext/hash/tests/new-context.phpt diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 4e3820f35e6b4..6848255dfc03e 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -595,7 +595,7 @@ PHP_FUNCTION(hash_hmac_file) /* }}} */ /* {{{ Initialize a hashing context */ -PHP_FUNCTION(hash_init) +static void do_hash_init(INTERNAL_FUNCTION_PARAMETERS, zval *thisObj) { zend_string *algo, *key = NULL; zend_long options = 0; @@ -625,8 +625,12 @@ PHP_FUNCTION(hash_init) } } - object_init_ex(return_value, php_hashcontext_ce); - hash = php_hashcontext_from_object(Z_OBJ_P(return_value)); + if (!thisObj) { + /* Called as hash_init(), create a new object */ + object_init_ex(return_value, php_hashcontext_ce); + thisObj = return_value; + } /* otherwise, called as new HashContext() */ + hash = php_hashcontext_from_object(Z_OBJ_P(thisObj)); context = php_hash_alloc_context(ops); ops->hash_init(context); @@ -663,47 +667,10 @@ PHP_FUNCTION(hash_init) } /* }}} */ -#define PHP_HASHCONTEXT_VERIFY(hash) { \ - if (!hash->context) { \ - zend_argument_type_error(1, "must be a valid Hash Context resource"); \ - RETURN_THROWS(); \ - } \ -} - -/* {{{ Pump data into the hashing algorithm */ -PHP_FUNCTION(hash_update) -{ - zval *zhash; - php_hashcontext_object *hash; - zend_string *data; - - if (zend_parse_parameters(ZEND_NUM_ARGS(), "OS", &zhash, php_hashcontext_ce, &data) == FAILURE) { - RETURN_THROWS(); - } - - hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); - PHP_HASHCONTEXT_VERIFY(hash); - hash->ops->hash_update(hash->context, (unsigned char *) ZSTR_VAL(data), ZSTR_LEN(data)); - - RETURN_TRUE; -} -/* }}} */ - /* {{{ Pump data into the hashing algorithm from an open stream */ -PHP_FUNCTION(hash_update_stream) +static zend_long do_update_stream(php_hashcontext_object *hash, php_stream *stream, zend_long length, zend_bool throwOnError) { - zval *zhash, *zstream; - php_hashcontext_object *hash; - php_stream *stream = NULL; - zend_long length = -1, didread = 0; - - if (zend_parse_parameters(ZEND_NUM_ARGS(), "Or|l", &zhash, php_hashcontext_ce, &zstream, &length) == FAILURE) { - RETURN_THROWS(); - } - - hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); - PHP_HASHCONTEXT_VERIFY(hash); - php_stream_from_zval(stream, zstream); + zend_long didread = 0; while (length) { char buf[1024]; @@ -715,69 +682,54 @@ PHP_FUNCTION(hash_update_stream) } if ((n = php_stream_read(stream, buf, toread)) <= 0) { - RETURN_LONG(didread); + if (throwOnError && (n < 0) && !EG(exception)) { + zend_throw_exception_ex(NULL, 0, "Stream read encountered error, %zd bytes copied", didread); + } + return didread; } hash->ops->hash_update(hash->context, (unsigned char *) buf, n); length -= n; didread += n; } - RETURN_LONG(didread); + return didread; } /* }}} */ /* {{{ Pump data into the hashing algorithm from a file */ -PHP_FUNCTION(hash_update_file) +static zend_bool do_update_file(php_hashcontext_object *hash, zend_string *filename, php_stream_context *context, zend_bool throwOnError) { - zval *zhash, *zcontext = NULL; - php_hashcontext_object *hash; - php_stream_context *context = NULL; - php_stream *stream; - zend_string *filename; + php_stream *stream = php_stream_open_wrapper_ex(ZSTR_VAL(filename), "rb", throwOnError ? 0 : REPORT_ERRORS, NULL, context); char buf[1024]; - ssize_t n; - - if (zend_parse_parameters(ZEND_NUM_ARGS(), "OP|r!", &zhash, php_hashcontext_ce, &filename, &zcontext) == FAILURE) { - RETURN_THROWS(); - } + ssize_t n, didcopy = 0; - hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); - PHP_HASHCONTEXT_VERIFY(hash); - context = php_stream_context_from_zval(zcontext, 0); - - stream = php_stream_open_wrapper_ex(ZSTR_VAL(filename), "rb", REPORT_ERRORS, NULL, context); if (!stream) { /* Stream will report errors opening file */ - RETURN_FALSE; + if (throwOnError && !EG(exception)) { + zend_throw_exception_ex(NULL, 0, "Error opening '%s'", ZSTR_VAL(filename)); + } + return false; } while ((n = php_stream_read(stream, buf, sizeof(buf))) > 0) { hash->ops->hash_update(hash->context, (unsigned char *) buf, n); + didcopy += n; } php_stream_close(stream); - RETURN_BOOL(n >= 0); + if (throwOnError && (n < 0) && !EG(exception)) { + zend_throw_exception_ex(NULL, 0, "Error while reading file, %zd bytes copied", didcopy); + } + return (n >= 0); } /* }}} */ /* {{{ Output resulting digest */ -PHP_FUNCTION(hash_final) +static void do_hash_final(zval *return_value, php_hashcontext_object *hash, zend_bool raw_output) { - zval *zhash; - php_hashcontext_object *hash; - zend_bool raw_output = 0; - zend_string *digest; - size_t digest_len; - - if (zend_parse_parameters(ZEND_NUM_ARGS(), "O|b", &zhash, php_hashcontext_ce, &raw_output) == FAILURE) { - RETURN_THROWS(); - } + size_t digest_len = hash->ops->digest_size; + zend_string *digest = zend_string_alloc(digest_len, 0); - hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); - PHP_HASHCONTEXT_VERIFY(hash); - - digest_len = hash->ops->digest_size; - digest = zend_string_alloc(digest_len, 0); hash->ops->hash_final((unsigned char *) ZSTR_VAL(digest), hash->context); if (hash->options & PHP_HASH_HMAC) { size_t i, block_size; @@ -818,6 +770,97 @@ PHP_FUNCTION(hash_final) } /* }}} */ +/* {{{ Initialize a hashing context */ +PHP_FUNCTION(hash_init) +{ + do_hash_init(INTERNAL_FUNCTION_PARAM_PASSTHRU, NULL); +} +/* {{{ */ + +#define PHP_HASHCONTEXT_VERIFY(hash) { \ + if (!hash->context) { \ + zend_argument_type_error(1, "must be a valid Hash Context resource"); \ + RETURN_THROWS(); \ + } \ +} + +/* {{{ Pump data into the hashing algorithm */ +PHP_FUNCTION(hash_update) +{ + zval *zhash; + php_hashcontext_object *hash; + zend_string *data; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "OS", &zhash, php_hashcontext_ce, &data) == FAILURE) { + RETURN_THROWS(); + } + + hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); + PHP_HASHCONTEXT_VERIFY(hash); + hash->ops->hash_update(hash->context, (unsigned char *) ZSTR_VAL(data), ZSTR_LEN(data)); + + RETURN_TRUE; +} +/* }}} */ + +/* {{{ Pump data into the hashing algorithm from an open stream */ +PHP_FUNCTION(hash_update_stream) +{ + zval *zhash, *zstream; + php_hashcontext_object *hash; + php_stream *stream = NULL; + zend_long length = -1; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "Or|l", &zhash, php_hashcontext_ce, &zstream, &length) == FAILURE) { + RETURN_THROWS(); + } + + hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); + PHP_HASHCONTEXT_VERIFY(hash); + php_stream_from_zval(stream, zstream); + + RETURN_LONG(do_update_stream(hash, stream, length, 0)); +} +/* }}} */ + +/* {{{ Pump data into the hashing algorithm from a file */ +PHP_FUNCTION(hash_update_file) +{ + zval *zhash, *zcontext = NULL; + php_hashcontext_object *hash; + php_stream_context *context = NULL; + zend_string *filename; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "OP|r!", &zhash, php_hashcontext_ce, &filename, &zcontext) == FAILURE) { + RETURN_THROWS(); + } + + hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); + PHP_HASHCONTEXT_VERIFY(hash); + context = php_stream_context_from_zval(zcontext, 0); + + RETURN_BOOL(do_update_file(hash, filename, context, 0)); +} +/* }}} */ + +/* {{{ Output resulting digest */ +PHP_FUNCTION(hash_final) +{ + zval *zhash; + php_hashcontext_object *hash; + zend_bool raw_output = 0; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "O|b", &zhash, php_hashcontext_ce, &raw_output) == FAILURE) { + RETURN_THROWS(); + } + + hash = php_hashcontext_from_object(Z_OBJ_P(zhash)); + PHP_HASHCONTEXT_VERIFY(hash); + + do_hash_final(return_value, hash, raw_output); +} +/* }}} */ + /* {{{ Copy hash object */ PHP_FUNCTION(hash_copy) { @@ -1129,10 +1172,100 @@ PHP_FUNCTION(hash_equals) } /* }}} */ -/* {{{ */ -PHP_METHOD(HashContext, __construct) { - /* Normally unreachable as private/final */ - zend_throw_exception(zend_ce_error, "Illegal call to private/final constructor", 0); +/* {{{ HashContext::__construct() */ +PHP_METHOD(HashContext, __construct) +{ + do_hash_init(INTERNAL_FUNCTION_PARAM_PASSTHRU, getThis()); +} +/* }}} */ + +/* {{{ HashContext::update() */ +PHP_METHOD(HashContext, update) +{ + php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(getThis())); + zend_string *data; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &data) == FAILURE) { + RETURN_THROWS(); + } + + PHP_HASHCONTEXT_VERIFY(hash); + hash->ops->hash_update(hash->context, (unsigned char *) ZSTR_VAL(data), ZSTR_LEN(data)); + + RETURN_ZVAL(getThis(), 1, 0); +} +/* }}} */ + +/* {{{ HashContext::updateStream() */ +PHP_METHOD(HashContext, updateStream) +{ + php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(getThis())); + zval *zstream; + php_stream *stream = NULL; + zend_long length = -1; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "r|l", &zstream, &length) == FAILURE) { + RETURN_THROWS(); + } + + PHP_HASHCONTEXT_VERIFY(hash); + php_stream_from_zval(stream, zstream); + + do_update_stream(hash, stream, length, 1); + RETURN_ZVAL(getThis(), 1, 0); +} +/* }}} */ + +/* {{{ HashContext::updateFile() */ +PHP_METHOD(HashContext, updateFile) +{ + php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(getThis())); + zval *zcontext = NULL; + php_stream_context *context = NULL; + zend_string *filename; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "P|r!", &filename, &zcontext) == FAILURE) { + RETURN_THROWS(); + } + + PHP_HASHCONTEXT_VERIFY(hash); + context = php_stream_context_from_zval(zcontext, 0); + + do_update_file(hash, filename, context, 1); + RETURN_ZVAL(getThis(), 1, 0); +} +/* }}} */ + +/* {{{ HashContext::final() */ +PHP_METHOD(HashContext, final) +{ + php_hashcontext_object *hash; + zend_bool raw_output = 0; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "|b", &raw_output) == FAILURE) { + RETURN_THROWS(); + } + + hash = php_hashcontext_from_object(Z_OBJ_P(getThis())); + PHP_HASHCONTEXT_VERIFY(hash); + + do_hash_final(return_value, hash, raw_output); +} +/* }}} */ + +/* {{{ HashContext::getAlgo() */ +PHP_METHOD(HashContext, getAlgo) +{ + php_hashcontext_object *hash; + + if (zend_parse_parameters_none() == FAILURE) { + RETURN_THROWS(); + } + + hash = php_hashcontext_from_object(Z_OBJ_P(getThis())); + PHP_HASHCONTEXT_VERIFY(hash); + + RETURN_STRING(hash->ops->algo); } /* }}} */ diff --git a/ext/hash/hash.stub.php b/ext/hash/hash.stub.php index c0d4cbca7c3d5..c2c8fdc9702b9 100644 --- a/ext/hash/hash.stub.php +++ b/ext/hash/hash.stub.php @@ -48,9 +48,16 @@ function mhash(int $algo, string $data, ?string $key = null): string|false {} final class HashContext { - private function __construct() {} + public function __construct(string $algo, int $flags = 0, string $key = '') {} + public function update(string $data): HashContext {} + /** @param resource|null $stream_context */ + public function updateFile(string $filename, $stream_context = null): HashContext {} + /** @param resource $stream */ + public function updateStream($stream, int $length = -1): HashContext {} + public function final(bool $binary = false): string {} - public function __serialize(): array {} + public function getAlgo(): string {} + public function __serialize(): array {} public function __unserialize(array $data): void {} } diff --git a/ext/hash/hash_arginfo.h b/ext/hash/hash_arginfo.h index db043da97b477..a9ce743b61b3a 100644 --- a/ext/hash/hash_arginfo.h +++ b/ext/hash/hash_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 9352e0ac98e2ac53dc15d5024f9ef0c8092c4e9c */ + * Stub hash: f162e966d646f8dfaab19481c600bc5ce537a464 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_hash, 0, 2, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, algo, IS_STRING, 0) @@ -115,7 +115,31 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_mhash, 0, 2, MAY_BE_STRING|MAY_B ZEND_END_ARG_INFO() #endif -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_HashContext___construct, 0, 0, 0) +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_HashContext___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, algo, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, key, IS_STRING, 0, "\'\'") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_HashContext_update, 0, 1, HashContext, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_HashContext_updateFile, 0, 1, HashContext, 0) + ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) + ZEND_ARG_INFO_WITH_DEFAULT_VALUE(0, stream_context, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_HashContext_updateStream, 0, 1, HashContext, 0) + ZEND_ARG_INFO(0, stream) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 0, "-1") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_HashContext_final, 0, 0, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, binary, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_HashContext_getAlgo, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() #define arginfo_class_HashContext___serialize arginfo_hash_algos @@ -156,6 +180,11 @@ ZEND_FUNCTION(mhash_count); ZEND_FUNCTION(mhash); #endif ZEND_METHOD(HashContext, __construct); +ZEND_METHOD(HashContext, update); +ZEND_METHOD(HashContext, updateFile); +ZEND_METHOD(HashContext, updateStream); +ZEND_METHOD(HashContext, final); +ZEND_METHOD(HashContext, getAlgo); ZEND_METHOD(HashContext, __serialize); ZEND_METHOD(HashContext, __unserialize); @@ -196,7 +225,12 @@ static const zend_function_entry ext_functions[] = { static const zend_function_entry class_HashContext_methods[] = { - ZEND_ME(HashContext, __construct, arginfo_class_HashContext___construct, ZEND_ACC_PRIVATE) + ZEND_ME(HashContext, __construct, arginfo_class_HashContext___construct, ZEND_ACC_PUBLIC) + ZEND_ME(HashContext, update, arginfo_class_HashContext_update, ZEND_ACC_PUBLIC) + ZEND_ME(HashContext, updateFile, arginfo_class_HashContext_updateFile, ZEND_ACC_PUBLIC) + ZEND_ME(HashContext, updateStream, arginfo_class_HashContext_updateStream, ZEND_ACC_PUBLIC) + ZEND_ME(HashContext, final, arginfo_class_HashContext_final, ZEND_ACC_PUBLIC) + ZEND_ME(HashContext, getAlgo, arginfo_class_HashContext_getAlgo, ZEND_ACC_PUBLIC) ZEND_ME(HashContext, __serialize, arginfo_class_HashContext___serialize, ZEND_ACC_PUBLIC) ZEND_ME(HashContext, __unserialize, arginfo_class_HashContext___unserialize, ZEND_ACC_PUBLIC) ZEND_FE_END diff --git a/ext/hash/tests/hash-oop.phpt b/ext/hash/tests/hash-oop.phpt new file mode 100644 index 0000000000000..a500c064a578f --- /dev/null +++ b/ext/hash/tests/hash-oop.phpt @@ -0,0 +1,65 @@ +--TEST-- +Hash: OOP API +--FILE-- +getAlgo()); +$ctx->update("I can't remember anything"); +var_dump($ctx->final()); + +// Fluent usage. +var_dump('sha256'); +var_dump((new HashContext('sha256'))->update("I can't remember anything")->final()); + +// Cloned context. +$ctx = (new HashContext('sha512'))->update("I can't remember anything"); +var_dump($ctx->getAlgo()); +var_dump((clone $ctx)->final()); +var_dump($ctx->final()); + +// HMAC +var_dump('ripemd128'); +var_dump((new HashContext('ripemd128', HASH_HMAC, 'secret'))->update("I can't remember anything")->final()); + +// Update from stream. +var_dump('stream-md5'); +$fp = tmpfile(); +fwrite($fp, 'The quick brown fox jumped over the lazy dog.'); +rewind($fp); +var_dump((new HashContext('md5'))->updateStream($fp)->final()); + +// Update from file. +var_dump('file-md5'); +$filePath = __DIR__ . DIRECTORY_SEPARATOR . 'hash-oop-file.txt'; +file_put_contents($filePath, 'The quick brown fox jumped over the lazy dog.'); +var_dump((new HashContext('md5'))->updateFile($filePath)->final()); + +// Update from non-existant file. +var_dump('file-not-found'); +try { + var_dump((new HashContext('md5'))->updateFile(__DIR__ . DIRECTORY_SEPARATOR . 'does-not-exist.txt')->final()); +} catch (Throwable $ex) { + var_dump($ex->getMessage()); +} + +--EXPECTF-- +string(4) "sha1" +string(40) "29f62a228f726cd728efa7a0ac6a2aba318baf15" +string(6) "sha256" +string(64) "d3a13cf52af8e9390caed78b77b6b1e06e102204e3555d111dfd149bc5d54dba" +string(6) "sha512" +string(128) "caced3db8e9e3a5543d5b933bcbe9e7834e6667545c3f5d4087b58ec8d78b4c8a4a5500c9b88f65f7368810ba9905e51f1cff3b25a5dccf76634108fb4e7ce13" +string(128) "caced3db8e9e3a5543d5b933bcbe9e7834e6667545c3f5d4087b58ec8d78b4c8a4a5500c9b88f65f7368810ba9905e51f1cff3b25a5dccf76634108fb4e7ce13" +string(9) "ripemd128" +string(32) "7446def1c89d9b8a9bffdb6e134ccb9c" +string(10) "stream-md5" +string(32) "5c6ffbdd40d9556b73a21e63c3e0e904" +string(8) "file-md5" +string(32) "5c6ffbdd40d9556b73a21e63c3e0e904" +string(14) "file-not-found" +string(%s) "Error opening '%sdoes-not-exist.txt'" +--CLEAN-- +getMessage()}\n"; -} -?> ---EXPECT-- -Exception: Call to private HashContext::__construct() from global scope