diff --git a/README.md b/README.md index 92c91ae..2ffce4e 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,11 @@ LIBZSTD\_VERSION\_STRING | libzstd version string * zstd\_uncompress — Zstandard decompression * zstd\_compress\_dict — Zstandard compression using a digested dictionary * zstd\_uncompress\_dict — Zstandard decompression using a digested dictionary +* zstd\_compress_\init — Initialize an incremental compress context +* zstd\_compress\_add — Incrementally compress data +* zstd\_uncompress_\init — Initialize an incremental uncompress context +* zstd\_uncompress\_add — Incrementally uncompress data + ### zstd\_compress — Zstandard compression @@ -209,6 +214,88 @@ Zstandard decompression using a digested dictionary. Returns the decompressed data or FALSE if an error occurred. +### zstd\_compress\_init — Initialize an incremental compress context + +#### Description + +resource **zstd\_compress\_init** ( [ int _$level_ = ZSTD_COMPRESS_LEVEL_DEFAULT ] ) + +Initialize an incremental compress context + +#### Parameters + +* _level_ + + The higher the level, the slower the compression. (Defaults to `ZSTD_COMPRESS_LEVEL_DEFAULT`) + +#### Return Values + +Returns a zstd context resource (zstd.state) on success, or FALSE on failure + + +### zstd\_compress\_add — Incrementally compress data + +#### Description + +string **zstd\_compress\_add** ( resource _$context_, string _$data_ [, bool _$end_ = false ] ) + +Incrementally compress data + +#### Parameters + +* _context_ + + A context created with `zstd_compress_init()`. + +* _data_ + + A chunk of data to compress. + +* _end_ + + Set to true to terminate with the last chunk of data. + +#### Return Values + +Returns a chunk of compressed data, or FALSE on failure. + + +### zstd\_uncompress\_init — Initialize an incremental uncompress context + +#### Description + +resource **zstd\_uncompress\_init** ( void ) + +Initialize an incremental uncompress context + +#### Return Values + +Returns a zstd context resource (zstd.state) on success, or FALSE on failure + + +### zstd\_uncompress\_add — Incrementally uncompress data + +#### Description + +string **zstd\_uncompress\_add** ( resource _$context_, string _$data_ ) + +Incrementally uncompress data + +#### Parameters + +* _context_ + + A context created with `zstd_uncompress_init()`. + +* _data_ + + A chunk of compressed data. + +#### Return Values + +Returns a chunk of uncompressed data, or FALSE on failure. + + ## Namespace ``` @@ -218,10 +305,16 @@ function compress( $data [, $level = 3 ] ) function uncompress( $data ) function compress_dict ( $data, $dict ) function uncompress_dict ( $data, $dict ) +function compress_init ( [ $level = 3 ] ) +function compress_add ( $context, $data [, $end = false ] ) +function uncompress_init () +function uncompress_add ( $context, $data ) + ``` -`zstd_compress`, `zstd_uncompress`, `zstd_compress_dict` and -`zstd_uncompress_dict` function alias. +`zstd_compress`, `zstd_uncompress`, `zstd_compress_dict`, +`zstd_uncompress_dict`, `zstd_compress_init`, `zstd_compress_add`, +`zstd_uncompress_init` and `zstd_uncompress_add` function alias. ## Streams diff --git a/tests/inc.phpt b/tests/inc.phpt new file mode 100755 index 0000000..ddd329f --- /dev/null +++ b/tests/inc.phpt @@ -0,0 +1,27 @@ +--TEST-- +Incremental compression and decompression +--FILE-- + +===Done=== +--EXPECTF-- +Hello, World! +Hello, World! +===Done=== diff --git a/tests/inc_comp.phpt b/tests/inc_comp.phpt new file mode 100755 index 0000000..da6813c --- /dev/null +++ b/tests/inc_comp.phpt @@ -0,0 +1,42 @@ +--TEST-- +Incremental compression +--FILE-- + +===Done=== +--EXPECTF-- +int(128) +resource(%d) of type (zstd.state) +int(%d) +bool(true) +bool(true) +int(512) +resource(%d) of type (zstd.state) +int(%d) +bool(true) +bool(true) +int(1024) +resource(%d) of type (zstd.state) +int(%d) +bool(true) +bool(true) +===Done=== diff --git a/tests/inc_decomp.phpt b/tests/inc_decomp.phpt new file mode 100755 index 0000000..0ad2413 --- /dev/null +++ b/tests/inc_decomp.phpt @@ -0,0 +1,36 @@ +--TEST-- +Incremental decompression +--FILE-- + +===Done=== +--EXPECTF-- +int(128) +resource(%d) of type (zstd.state) +bool(true) +int(512) +resource(%d) of type (zstd.state) +bool(true) +int(1024) +resource(%d) of type (zstd.state) +bool(true) +===Done=== diff --git a/tests/inc_ns.phpt b/tests/inc_ns.phpt new file mode 100755 index 0000000..746d520 --- /dev/null +++ b/tests/inc_ns.phpt @@ -0,0 +1,27 @@ +--TEST-- +Incremental compression and decompression (namespaces) +--FILE-- + +===Done=== +--EXPECTF-- +Hello, World! +Hello, World! +===Done=== diff --git a/zstd.c b/zstd.c index 4b1ba25..f7b783e 100644 --- a/zstd.c +++ b/zstd.c @@ -38,6 +38,8 @@ #endif #include "php_zstd.h" +int le_state; + /* zstd */ #ifdef HAVE_SYS_TYPES_H #include @@ -64,6 +66,7 @@ #define ZSTD_IS_ERROR(result) \ UNEXPECTED(ZSTD_isError(result)) +/* One-shot functions */ ZEND_BEGIN_ARG_INFO_EX(arginfo_zstd_compress, 0, 0, 1) ZEND_ARG_INFO(0, data) ZEND_ARG_INFO(0, level) @@ -84,6 +87,25 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_zstd_uncompress_dict, 0, 0, 2) ZEND_ARG_INFO(0, dictBuffer) ZEND_END_ARG_INFO() +/* Incremental functions */ +ZEND_BEGIN_ARG_INFO_EX(arginfo_zstd_compress_init, 0, 0, 0) + ZEND_ARG_INFO(0, level) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_zstd_compress_add, 0, 0, 2) + ZEND_ARG_INFO(0, context) + ZEND_ARG_INFO(0, data) + ZEND_ARG_INFO(0, end) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_zstd_uncompress_init, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_zstd_uncompress_add, 0, 0, 2) + ZEND_ARG_INFO(0, context) + ZEND_ARG_INFO(0, data) +ZEND_END_ARG_INFO() + #if PHP_VERSION_ID >= 80000 #if PHP_VERSION_ID >= 80100 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_ob_zstd_handler, 0, 2, MAY_BE_STRING|MAY_BE_FALSE) @@ -393,6 +415,194 @@ ZEND_FUNCTION(zstd_uncompress_dict) RETVAL_NEW_STR(output); } +struct _php_zstd_context { + ZSTD_CCtx* cctx; + ZSTD_DCtx* dctx; + ZSTD_CDict *cdict; + ZSTD_inBuffer input; + ZSTD_outBuffer output; +}; + +static php_zstd_context* php_zstd_output_handler_context_init(void) +{ + php_zstd_context *ctx + = (php_zstd_context *) ecalloc(1, sizeof(php_zstd_context)); + ctx->cctx = NULL; + ctx->dctx = NULL; + return ctx; +} + +static void php_zstd_output_handler_context_free(php_zstd_context *ctx) +{ + if (ctx->cctx) { + ZSTD_freeCCtx(ctx->cctx); + ctx->cctx = NULL; + } + if (ctx->dctx) { + ZSTD_freeDCtx(ctx->dctx); + ctx->dctx = NULL; + } + if (ctx->cdict) { + ZSTD_freeCDict(ctx->cdict); + ctx->cdict = NULL; + } + if (ctx->output.dst) { + efree(ctx->output.dst); + ctx->output.dst = NULL; + } +} + +static void php_zstd_state_rsrc_dtor(zend_resource *res) +{ + php_zstd_context *ctx = zend_fetch_resource(res, NULL, le_state); + if (ctx) { + php_zstd_output_handler_context_free(ctx); + efree(ctx); + } +} + +ZEND_FUNCTION(zstd_compress_init) +{ + zend_long level = DEFAULT_COMPRESS_LEVEL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(level) + ZEND_PARSE_PARAMETERS_END(); + + if (!zstd_check_compress_level(level)) { + RETURN_FALSE; + } + + php_zstd_context *ctx = php_zstd_output_handler_context_init(); + + ctx->cctx = ZSTD_createCCtx(); + if (ctx->cctx == NULL) { + efree(ctx); + ZSTD_WARNING("ZSTD_createCCtx() error"); + RETURN_FALSE; + } + ctx->cdict = NULL; + + ZSTD_CCtx_reset(ctx->cctx, ZSTD_reset_session_only); + ZSTD_CCtx_setParameter(ctx->cctx, ZSTD_c_compressionLevel, level); + + ctx->output.size = ZSTD_CStreamOutSize(); + ctx->output.dst = emalloc(ctx->output.size); + ctx->output.pos = 0; + + RETURN_RES(zend_register_resource(ctx, le_state)); +} + +ZEND_FUNCTION(zstd_compress_add) +{ + zval *resource; + php_zstd_context *ctx; + char *in_buf; + size_t in_size; + zend_bool end = 0; + smart_string out = {0}; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_RESOURCE(resource) + Z_PARAM_STRING(in_buf, in_size) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(end) + ZEND_PARSE_PARAMETERS_END(); + + ctx = zend_fetch_resource(Z_RES_P(resource), NULL, le_state); + if (ctx == NULL) { + php_error_docref(NULL, E_WARNING, + "ZStandard incremental compress resource failed"); + RETURN_FALSE; + } + + ZSTD_inBuffer in = { in_buf, in_size, 0 }; + size_t res; + + do { + ctx->output.pos = 0; + res = ZSTD_compressStream2(ctx->cctx, &ctx->output, + &in, end ? ZSTD_e_end : ZSTD_e_flush); + if (ZSTD_isError(res)) { + php_error_docref(NULL, E_WARNING, + "libzstd error %s\n", ZSTD_getErrorName(res)); + smart_string_free(&out); + RETURN_FALSE; + } + smart_string_appendl(&out, ctx->output.dst, ctx->output.pos); + } while (res > 0); + + RETVAL_STRINGL(out.c, out.len); + smart_string_free(&out); +} + +ZEND_FUNCTION(zstd_uncompress_init) +{ + php_zstd_context *ctx = php_zstd_output_handler_context_init(); + + ctx->dctx = ZSTD_createDCtx(); + if (ctx->dctx == NULL) { + efree(ctx); + ZSTD_WARNING("ZSTD_createDCtx() error"); + RETURN_FALSE; + } + ctx->cdict = NULL; + + ZSTD_DCtx_reset(ctx->dctx, ZSTD_reset_session_only); + + ctx->output.size = ZSTD_DStreamOutSize(); + ctx->output.dst = emalloc(ctx->output.size); + ctx->output.pos = 0; + + RETURN_RES(zend_register_resource(ctx, le_state)); +} + +ZEND_FUNCTION(zstd_uncompress_add) +{ + zval *resource; + php_zstd_context *ctx; + char *in_buf; + size_t in_size; + smart_string out = {0}; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_RESOURCE(resource) + Z_PARAM_STRING(in_buf, in_size) + ZEND_PARSE_PARAMETERS_END(); + + ctx = zend_fetch_resource(Z_RES_P(resource), NULL, le_state); + if (ctx == NULL) { + php_error_docref(NULL, E_WARNING, + "ZStandard incremental uncompress resource failed"); + RETURN_FALSE; + } + + ZSTD_inBuffer in = { in_buf, in_size, 0 }; + size_t res = 1; + const size_t grow = ZSTD_DStreamOutSize(); + + while (in.pos < in.size && res > 0) { + if (ctx->output.pos == ctx->output.size) { + ctx->output.size += grow; + ctx->output.dst = erealloc(ctx->output.dst, ctx->output.size); + } + + ctx->output.pos = 0; + res = ZSTD_decompressStream(ctx->dctx, &ctx->output, &in); + if (ZSTD_isError(res)) { + php_error_docref(NULL, E_WARNING, + "libzstd error %s\n", ZSTD_getErrorName(res)); + smart_string_free(&out); + RETURN_FALSE; + } + + smart_string_appendl(&out, ctx->output.dst, ctx->output.pos); + } + + RETVAL_STRINGL(out.c, out.len); + smart_string_free(&out); +} typedef struct _php_zstd_stream_data { char *bufin, *bufout; @@ -835,13 +1045,6 @@ static int APC_UNSERIALIZER_NAME(zstd)(APC_UNSERIALIZER_ARGS) #if PHP_VERSION_ID >= 80000 #define PHP_ZSTD_OUTPUT_HANDLER_NAME "zstd output compression" -struct _php_zstd_context { - ZSTD_CCtx* cctx; - ZSTD_CDict *cdict; - ZSTD_inBuffer input; - ZSTD_outBuffer output; -}; - static int php_zstd_output_encoding(void) { zval *enc; @@ -866,13 +1069,6 @@ static int php_zstd_output_encoding(void) return PHP_ZSTD_G(compression_coding); } -static php_zstd_context* php_zstd_output_handler_context_init(void) -{ - php_zstd_context *ctx - = (php_zstd_context *) ecalloc(1, sizeof(php_zstd_context)); - return ctx; -} - static void php_zstd_output_handler_load_dict(php_zstd_context *ctx, int level) { @@ -944,22 +1140,6 @@ static zend_result php_zstd_output_handler_context_start(php_zstd_context *ctx) return SUCCESS; } -static void php_zstd_output_handler_context_free(php_zstd_context *ctx) -{ - if (ctx->cctx) { - ZSTD_freeCCtx(ctx->cctx); - ctx->cctx = NULL; - } - if (ctx->cdict) { - ZSTD_freeCDict(ctx->cdict); - ctx->cdict = NULL; - } - if (ctx->output.dst) { - efree(ctx->output.dst); - ctx->output.dst = NULL; - } -} - static void php_zstd_output_handler_context_dtor(void *opaq) { php_zstd_context *ctx = (php_zstd_context *) opaq; @@ -1298,6 +1478,10 @@ ZEND_MINIT_FUNCTION(zstd) ZSTD_VERSION_STRING, CONST_CS | CONST_PERSISTENT); + le_state = zend_register_list_destructors_ex(php_zstd_state_rsrc_dtor, + NULL, "zstd.state", + module_number); + php_register_url_stream_wrapper(STREAM_NAME, &php_stream_zstd_wrapper); #if defined(HAVE_APCU_SUPPORT) @@ -1397,6 +1581,11 @@ static zend_function_entry zstd_functions[] = { ZEND_FALIAS(zstd_decompress_usingcdict, zstd_uncompress_dict, arginfo_zstd_uncompress_dict) + ZEND_FE(zstd_compress_init, arginfo_zstd_compress_init) + ZEND_FE(zstd_compress_add, arginfo_zstd_compress_add) + ZEND_FE(zstd_uncompress_init, arginfo_zstd_uncompress_init) + ZEND_FE(zstd_uncompress_add, arginfo_zstd_uncompress_add) + ZEND_NS_FALIAS(PHP_ZSTD_NS, compress, zstd_compress, arginfo_zstd_compress) ZEND_NS_FALIAS(PHP_ZSTD_NS, uncompress, @@ -1416,6 +1605,15 @@ static zend_function_entry zstd_functions[] = { ZEND_NS_FALIAS(PHP_ZSTD_NS, decompress_usingcdict, zstd_uncompress_dict, arginfo_zstd_uncompress_dict) + ZEND_NS_FALIAS(PHP_ZSTD_NS, compress_init, + zstd_compress_init, arginfo_zstd_compress_init) + ZEND_NS_FALIAS(PHP_ZSTD_NS, compress_add, + zstd_compress_add, arginfo_zstd_compress_add) + ZEND_NS_FALIAS(PHP_ZSTD_NS, uncompress_init, + zstd_uncompress_init, arginfo_zstd_uncompress_init) + ZEND_NS_FALIAS(PHP_ZSTD_NS, uncompress_add, + zstd_uncompress_add, arginfo_zstd_uncompress_add) + #if PHP_VERSION_ID >= 80000 ZEND_FE(ob_zstd_handler, arginfo_ob_zstd_handler) #endif diff --git a/zstd.stub.php b/zstd.stub.php index 9169c6f..bbccf12 100644 --- a/zstd.stub.php +++ b/zstd.stub.php @@ -10,6 +10,26 @@ function zstd_compress_dict(string $data, string $dict, int $level = DEFAULT_COM function zstd_uncompress_dict(string $data, string $dict): string|false {} + /** + * @return resource|false + */ + function zstd_compress_init(int $level= 3) {} + + /** + * @param resource $context + */ + function zstd_compress_add($context, string $data, bool $end = false): string|false {} + + /** + * @return resource|false + */ + function zstd_uncompress_init() {} + + /** + * @param resource $context + */ + function zstd_uncompress_add($context, string $data): string|false {} + } namespace Zstd { @@ -22,4 +42,24 @@ function compress_dict(string $data, string $dict, int $level = 3): string|false function uncompress_dict(string $data, string $dict): string|false {} + /** + * @return resource|false + */ + function compress_init(int $level= 3) {} + + /** + * @param resource $context + */ + function compress_add($context, string $data, bool $end = false): string|false {} + + /** + * @return resource|false + */ + function uncompress_init() {} + + /** + * @param resource $context + */ + function uncompress_add($context, string $data): string|false {} + }