Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions lz4.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ static ZEND_FUNCTION(lz4_compress);
static ZEND_FUNCTION(lz4_uncompress);
static ZEND_FUNCTION(lz4_compress_frame);
static ZEND_FUNCTION(lz4_uncompress_frame);
static ZEND_FUNCTION(lz4_compress_raw);
static ZEND_FUNCTION(lz4_uncompress_raw);

ZEND_BEGIN_ARG_INFO_EX(arginfo_lz4_compress, 0, 0, 1)
ZEND_ARG_INFO(0, data)
Expand All @@ -98,6 +100,16 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_lz4_uncompress_frame, 0, 0, 1)
ZEND_ARG_INFO(0, data)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_lz4_compress_raw, 0, 0, 1)
ZEND_ARG_INFO(0, data)
ZEND_ARG_INFO(0, level)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_lz4_uncompress_raw, 0, 0, 2)
ZEND_ARG_INFO(0, data)
ZEND_ARG_INFO(0, max_size)
ZEND_END_ARG_INFO()

#if PHP_MAJOR_VERSION >= 7 && defined(HAVE_APCU_SUPPORT)
static int APC_SERIALIZER_NAME(lz4)(APC_SERIALIZER_ARGS);
static int APC_UNSERIALIZER_NAME(lz4)(APC_UNSERIALIZER_ARGS);
Expand All @@ -108,6 +120,8 @@ static zend_function_entry lz4_functions[] = {
ZEND_FE(lz4_uncompress, arginfo_lz4_uncompress)
ZEND_FE(lz4_compress_frame, arginfo_lz4_compress_frame)
ZEND_FE(lz4_uncompress_frame, arginfo_lz4_uncompress_frame)
ZEND_FE(lz4_compress_raw, arginfo_lz4_compress_raw)
ZEND_FE(lz4_uncompress_raw, arginfo_lz4_uncompress_raw)
ZEND_FE_END
};

Expand Down Expand Up @@ -285,6 +299,93 @@ static int php_lz4_uncompress(const char* in, const int in_len,
return SUCCESS;
}

/**
* Raw LZ4 block compression (no size header)
* Compatible with Python lz4.block, Rust lz4_flex, Go pierrec/lz4
*/
static int php_lz4_compress_raw(char* in, const int in_len,
char** out, int* out_len,
const int level)
{
int max_len;

/* Calculate maximum compressed size (LZ4 worst-case bound) */
max_len = LZ4_compressBound(in_len);

/* Allocate output buffer (NO header space, just compressed data) */
*out = (char*)emalloc(max_len);
if (!*out) {
zend_error(E_WARNING, "lz4_compress_raw : memory error");
*out_len = 0;
return FAILURE;
}

/* Compress directly into output buffer (no offset) */
if (level == 0) {
*out_len = LZ4_compress_default(in, *out, in_len, max_len);
} else if (level > 0 && level <= PHP_LZ4_CLEVEL_MAX) {
*out_len = LZ4_compress_HC(in, *out, in_len, max_len, level);
} else {
zend_error(E_WARNING,
"lz4_compress_raw: compression level (%d) must be within 1..%d",
level, PHP_LZ4_CLEVEL_MAX);
efree(*out);
*out = NULL;
*out_len = 0;
return FAILURE;
}

/* Check for compression errors */
if (*out_len <= 0) {
zend_error(E_WARNING, "lz4_compress_raw : compression failed");
efree(*out);
*out = NULL;
*out_len = 0;
return FAILURE;
}

/* NOTE: *out_len is the actual compressed size (no header added) */
return SUCCESS;
}

/**
* Raw LZ4 block decompression (no size header)
* Requires max_size parameter (from ByteStorage envelope)
*/
static int php_lz4_uncompress_raw(const char* in, const int in_len,
const int max_size,
char** out, int* out_len)
{
/* Validate max_size parameter (required for raw decompression) */
if (max_size <= 0) {
zend_error(E_WARNING,
"lz4_uncompress_raw : max_size parameter is required and must be positive");
return FAILURE;
}

/* Allocate output buffer based on provided max_size */
*out = (char*)malloc(max_size + 1);
if (!*out) {
zend_error(E_WARNING, "lz4_uncompress_raw : memory error");
return FAILURE;
}

/* Decompress from start of input (no offset) */
*out_len = LZ4_decompress_safe(in, *out, in_len, max_size);

/* Check decompression result */
if (*out_len <= 0) {
zend_error(E_WARNING,
"lz4_uncompress_raw : decompression failed (corrupted data or wrong max_size)");
free(*out);
*out = NULL;
*out_len = 0;
return FAILURE;
}

return SUCCESS;
}

/**
* @param max_block_size 4: 64KB, 5: 256KB, 6: 1MB, 7: 4MB, all other values: 64KB
* @param checksums 0: none, 1: frame content, 2: each block, 3: frame content + each block
Expand Down Expand Up @@ -587,6 +688,84 @@ static ZEND_FUNCTION(lz4_uncompress_frame)
free(output);
}

static ZEND_FUNCTION(lz4_compress_raw)
{
zval *data;
char *output;
int output_len;
long level = 0;

/* Parse parameters: data (required), level (optional, default 0) */
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
"z|l", &data, &level) == FAILURE) {
RETURN_FALSE;
}

/* Validate data is a string */
if (Z_TYPE_P(data) != IS_STRING) {
zend_error(E_WARNING,
"lz4_compress_raw : expects parameter to be string.");
RETURN_FALSE;
}

/* Call internal compression function */
if (php_lz4_compress_raw(Z_STRVAL_P(data), Z_STRLEN_P(data),
&output, &output_len,
(int)level) == FAILURE) {
RETURN_FALSE;
}

/* Return compressed data */
#if ZEND_MODULE_API_NO >= 20141001
RETVAL_STRINGL(output, output_len);
#else
RETVAL_STRINGL(output, output_len, 1);
#endif

efree(output);
}

static ZEND_FUNCTION(lz4_uncompress_raw)
{
zval *data;
int output_len;
char *output;
#if ZEND_MODULE_API_NO >= 20141001
zend_long max_size;
#else
long max_size;
#endif

/* Parse parameters: data (required), max_size (required) */
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
"zl", &data, &max_size) == FAILURE) {
RETURN_FALSE;
}

/* Validate data is a string */
if (Z_TYPE_P(data) != IS_STRING) {
zend_error(E_WARNING,
"lz4_uncompress_raw : expects parameter to be string.");
RETURN_FALSE;
}

/* Call internal decompression function */
if (php_lz4_uncompress_raw(Z_STRVAL_P(data), Z_STRLEN_P(data),
(const int)max_size,
&output, &output_len) == FAILURE) {
RETURN_FALSE;
}

/* Return decompressed data */
#if ZEND_MODULE_API_NO >= 20141001
RETVAL_STRINGL(output, output_len);
#else
RETVAL_STRINGL(output, output_len, 1);
#endif

free(output);
}

#if PHP_MAJOR_VERSION >= 7 && defined(HAVE_APCU_SUPPORT)
static int APC_SERIALIZER_NAME(lz4)(APC_SERIALIZER_ARGS)
{
Expand Down
26 changes: 26 additions & 0 deletions tests/raw_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Test lz4_compress_raw() and lz4_uncompress_raw() : basic functionality
--SKIPIF--
<?php if (!extension_loaded('lz4')) die('skip lz4 extension not available'); ?>
--FILE--
<?php
echo "*** Testing lz4_compress_raw() and lz4_uncompress_raw() ***\n";

$data = "Hello, World!";
echo "Original: $data\n";

$compressed = lz4_compress_raw($data);
echo "Compressed hex: " . bin2hex($compressed) . "\n";
echo "Compressed size: " . strlen($compressed) . " bytes\n";

$decompressed = lz4_uncompress_raw($compressed, strlen($data));
echo "Decompressed: $decompressed\n";
echo "Match: " . ($decompressed === $data ? "YES" : "NO") . "\n";
?>
--EXPECT--
*** Testing lz4_compress_raw() and lz4_uncompress_raw() ***
Original: Hello, World!
Compressed hex: d048656c6c6f2c20576f726c6421
Compressed size: 14 bytes
Decompressed: Hello, World!
Match: YES
33 changes: 33 additions & 0 deletions tests/raw_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
Test lz4_compress_raw() : test vectors (Python compatibility)
--SKIPIF--
<?php if (!extension_loaded('lz4')) die('skip lz4 extension not available'); ?>
--FILE--
<?php
echo "*** Testing lz4_compress_raw() with known test vectors ***\n";

$tests = [
['data' => 'Hello, World!', 'hex' => 'd048656c6c6f2c20576f726c6421'],
['data' => 'test', 'hex' => '4074657374'],
['data' => str_repeat('A', 100), 'hex' => '1f4101004b504141414141'],
];

foreach ($tests as $i => $test) {
$compressed = lz4_compress_raw($test['data']);
$actual_hex = bin2hex($compressed);

echo "Test " . ($i + 1) . ": ";
if ($actual_hex === $test['hex']) {
echo "PASS\n";
} else {
echo "FAIL\n";
echo " Expected: {$test['hex']}\n";
echo " Actual: $actual_hex\n";
}
}
?>
--EXPECT--
*** Testing lz4_compress_raw() with known test vectors ***
Test 1: PASS
Test 2: PASS
Test 3: PASS
43 changes: 43 additions & 0 deletions tests/raw_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
Test lz4_uncompress_raw() : error handling
--SKIPIF--
<?php if (!extension_loaded('lz4')) die('skip lz4 extension not available'); ?>
--FILE--
<?php
echo "*** Testing lz4_uncompress_raw() error handling ***\n";

// Zero max_size
error_reporting(E_ALL);
$result = lz4_uncompress_raw("test", 0);
echo "Zero max_size: " . ($result === false ? "REJECTED" : "ACCEPTED") . "\n";

// Negative max_size
$result = lz4_uncompress_raw("test", -1);
echo "Negative max_size: " . ($result === false ? "REJECTED" : "ACCEPTED") . "\n";

// Corrupted data
$result = lz4_uncompress_raw("corrupted_lz4_data", 100);
echo "Corrupted data: " . ($result === false ? "REJECTED" : "ACCEPTED") . "\n";

// Wrong max_size (too small)
$compressed = lz4_compress_raw("Hello, World!");
$result = lz4_uncompress_raw($compressed, 5); // Should be 13
echo "Wrong max_size: " . ($result === false ? "REJECTED" : "ACCEPTED") . "\n";

echo "Done\n";
?>
--EXPECTF--
*** Testing lz4_uncompress_raw() error handling ***

Warning: lz4_uncompress_raw : max_size parameter is required and must be positive in %s on line %d
Zero max_size: REJECTED

Warning: lz4_uncompress_raw : max_size parameter is required and must be positive in %s on line %d
Negative max_size: REJECTED

Warning: lz4_uncompress_raw : decompression failed (corrupted data or wrong max_size) in %s on line %d
Corrupted data: REJECTED

Warning: lz4_uncompress_raw : decompression failed (corrupted data or wrong max_size) in %s on line %d
Wrong max_size: REJECTED
Done
46 changes: 46 additions & 0 deletions tests/raw_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
Test lz4_compress_raw() : compression levels
--SKIPIF--
<?php if (!extension_loaded('lz4')) die('skip lz4 extension not available'); ?>
--FILE--
<?php
echo "*** Testing lz4_compress_raw() with different compression levels ***\n";

$data = str_repeat("Lorem ipsum dolor sit amet. ", 50);

// Level 0 (default)
$comp_0 = lz4_compress_raw($data, 0);
echo "Level 0 (default): " . strlen($comp_0) . " bytes\n";

// Level 1 (HC mode)
$comp_1 = lz4_compress_raw($data, 1);
echo "Level 1 (HC): " . strlen($comp_1) . " bytes\n";

// Level 9 (max)
$comp_9 = lz4_compress_raw($data, 9);
echo "Level 9 (HC max): " . strlen($comp_9) . " bytes\n";

// All should decompress correctly
$decomp_0 = lz4_uncompress_raw($comp_0, strlen($data));
$decomp_1 = lz4_uncompress_raw($comp_1, strlen($data));
$decomp_9 = lz4_uncompress_raw($comp_9, strlen($data));

echo "Level 0 roundtrip: " . ($decomp_0 === $data ? "PASS" : "FAIL") . "\n";
echo "Level 1 roundtrip: " . ($decomp_1 === $data ? "PASS" : "FAIL") . "\n";
echo "Level 9 roundtrip: " . ($decomp_9 === $data ? "PASS" : "FAIL") . "\n";

// Invalid level
$result = lz4_compress_raw($data, 999);
echo "Invalid level 999: " . ($result === false ? "REJECTED" : "ACCEPTED") . "\n";
?>
--EXPECTF--
*** Testing lz4_compress_raw() with different compression levels ***
Level 0 (default): %d bytes
Level 1 (HC): %d bytes
Level 9 (HC max): %d bytes
Level 0 roundtrip: PASS
Level 1 roundtrip: PASS
Level 9 roundtrip: PASS

Warning: lz4_compress_raw: compression level (999) must be within 1..%d in %s on line %d
Invalid level 999: REJECTED