From 39eb787808d77d3cad30dc3d5ce2d02503326cba Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 13 Apr 2026 09:33:30 +0900 Subject: [PATCH] test_compression: Test module for compression methods The goal of this module is to provide tests for low-level APIs of compression methods. pglz is covered in this commit. This module includes also tests for the cases detected by fuzzing related to corrupted data, as fixed in 2b5ba2a0a141: - Control byte with match tag bit set, where no data follows. - Control byte with match tag bit set, where 1 byte follows. - Extension byte needed (len=18), where no data follows. As bonus points, tests are added for compress/decompress roundtrips, and for check_complete=false/true. Backpatch-through: 14 --- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_compression/.gitignore | 4 + src/test/modules/test_compression/Makefile | 23 +++++ .../expected/test_compression.out | 51 +++++++++++ src/test/modules/test_compression/meson.build | 33 +++++++ .../test_compression/sql/test_compression.sql | 36 ++++++++ .../test_compression--1.0.sql | 12 +++ .../test_compression/test_compression.c | 85 +++++++++++++++++++ .../test_compression/test_compression.control | 4 + 10 files changed, 250 insertions(+) create mode 100644 src/test/modules/test_compression/.gitignore create mode 100644 src/test/modules/test_compression/Makefile create mode 100644 src/test/modules/test_compression/expected/test_compression.out create mode 100644 src/test/modules/test_compression/meson.build create mode 100644 src/test/modules/test_compression/sql/test_compression.sql create mode 100644 src/test/modules/test_compression/test_compression--1.0.sql create mode 100644 src/test/modules/test_compression/test_compression.c create mode 100644 src/test/modules/test_compression/test_compression.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 0a74ab5c86f5..dbcd432f8c86 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -22,6 +22,7 @@ SUBDIRS = \ test_bloomfilter \ test_cloexec \ test_checksums \ + test_compression \ test_copy_callbacks \ test_custom_rmgrs \ test_custom_stats \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 4bca42bb3706..7cb26400d435 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -22,6 +22,7 @@ subdir('test_bitmapset') subdir('test_bloomfilter') subdir('test_cloexec') subdir('test_checksums') +subdir('test_compression') subdir('test_copy_callbacks') subdir('test_cplusplusext') subdir('test_custom_rmgrs') diff --git a/src/test/modules/test_compression/.gitignore b/src/test/modules/test_compression/.gitignore new file mode 100644 index 000000000000..5dcb3ff97235 --- /dev/null +++ b/src/test/modules/test_compression/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_compression/Makefile b/src/test/modules/test_compression/Makefile new file mode 100644 index 000000000000..82c6ace4dc8a --- /dev/null +++ b/src/test/modules/test_compression/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_compression/Makefile + +MODULE_big = test_compression +OBJS = \ + $(WIN32RES) \ + test_compression.o +PGFILEDESC = "test_compression - test code for compression methods" + +EXTENSION = test_compression +DATA = test_compression--1.0.sql + +REGRESS = test_compression + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_compression +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_compression/expected/test_compression.out b/src/test/modules/test_compression/expected/test_compression.out new file mode 100644 index 000000000000..837acc85dd49 --- /dev/null +++ b/src/test/modules/test_compression/expected/test_compression.out @@ -0,0 +1,51 @@ +CREATE EXTENSION test_compression; +-- Round-trip with pglz: compress then decompress. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, false) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + roundtrip_ok +-------------- + t +(1 row) + +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, true) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + roundtrip_ok +-------------- + t +(1 row) + +-- Decompression with rawsize too large, fails to fill the destination +-- buffer. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 500, true); +ERROR: pglz_decompress failed +-- Decompression with rawsize too small, fails with source not fully +-- consumed. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 100, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. The control byte is set with match tag bit, +-- but only 1 byte follows. +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Control byte with match tag bit set, where +-- no data follows. +SELECT length(test_pglz_decompress('\x01'::bytea, 1024, false)) AS ctrl_only_len; + ctrl_only_len +--------------- + 0 +(1 row) + +SELECT test_pglz_decompress('\x01'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. The match tag encodes len=18 (aka the +-- extension byte is needed) but there is no data. +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); +ERROR: pglz_decompress failed +DROP EXTENSION test_compression; diff --git a/src/test/modules/test_compression/meson.build b/src/test/modules/test_compression/meson.build new file mode 100644 index 000000000000..b25144ce71cd --- /dev/null +++ b/src/test/modules/test_compression/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_compression_sources = files( + 'test_compression.c', +) + +if host_system == 'windows' + test_compression_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_compression', + '--FILEDESC', 'test_compression - test code for compression methods',]) +endif + +test_compression = shared_module('test_compression', + test_compression_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_compression + +test_install_data += files( + 'test_compression.control', + 'test_compression--1.0.sql', +) + +tests += { + 'name': 'test_compression', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_compression', + ], + }, +} diff --git a/src/test/modules/test_compression/sql/test_compression.sql b/src/test/modules/test_compression/sql/test_compression.sql new file mode 100644 index 000000000000..4775b5ab582e --- /dev/null +++ b/src/test/modules/test_compression/sql/test_compression.sql @@ -0,0 +1,36 @@ +CREATE EXTENSION test_compression; + +-- Round-trip with pglz: compress then decompress. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, false) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, true) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + +-- Decompression with rawsize too large, fails to fill the destination +-- buffer. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 500, true); + +-- Decompression with rawsize too small, fails with source not fully +-- consumed. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 100, true); + +-- Corrupted compressed data. The control byte is set with match tag bit, +-- but only 1 byte follows. +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, false); +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); + +-- Corrupted compressed data. Control byte with match tag bit set, where +-- no data follows. +SELECT length(test_pglz_decompress('\x01'::bytea, 1024, false)) AS ctrl_only_len; +SELECT test_pglz_decompress('\x01'::bytea, 1024, true); + +-- Corrupted compressed data. The match tag encodes len=18 (aka the +-- extension byte is needed) but there is no data. +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); + +DROP EXTENSION test_compression; diff --git a/src/test/modules/test_compression/test_compression--1.0.sql b/src/test/modules/test_compression/test_compression--1.0.sql new file mode 100644 index 000000000000..cc789df87340 --- /dev/null +++ b/src/test/modules/test_compression/test_compression--1.0.sql @@ -0,0 +1,12 @@ +/* src/test/modules/test_compression/test_compression--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_compression" to load this file. \quit + +CREATE FUNCTION test_pglz_compress(bytea) +RETURNS bytea +AS 'MODULE_PATHNAME' LANGUAGE C STRICT; + +CREATE FUNCTION test_pglz_decompress(bytea, int4, bool) +RETURNS bytea +AS 'MODULE_PATHNAME' LANGUAGE C STRICT; diff --git a/src/test/modules/test_compression/test_compression.c b/src/test/modules/test_compression/test_compression.c new file mode 100644 index 000000000000..2a1d27999395 --- /dev/null +++ b/src/test/modules/test_compression/test_compression.c @@ -0,0 +1,85 @@ +/*-------------------------------------------------------------------------- + * + * test_compression.c + * Test harness for compression methods. + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_compression/test_compression.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/pg_lzcompress.h" +#include "fmgr.h" +#include "varatt.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_pglz_compress); +PG_FUNCTION_INFO_V1(test_pglz_decompress); + +/* + * test_pglz_compress + * + * Compress the input using pglz_compress(). Only the "always" strategy is + * currently supported. + * + * Returns the compressed data, or NULL if compression fails. + */ +Datum +test_pglz_compress(PG_FUNCTION_ARGS) +{ + bytea *input = PG_GETARG_BYTEA_PP(0); + char *source = VARDATA_ANY(input); + int32 slen = VARSIZE_ANY_EXHDR(input); + int32 maxout = PGLZ_MAX_OUTPUT(slen); + bytea *result; + int32 clen; + + result = (bytea *) palloc(maxout + VARHDRSZ); + clen = pglz_compress(source, slen, VARDATA(result), + PGLZ_strategy_always); + if (clen < 0) + PG_RETURN_NULL(); + + SET_VARSIZE(result, clen + VARHDRSZ); + PG_RETURN_BYTEA_P(result); +} + +/* + * test_pglz_decompress + * + * Decompress the input using pglz_decompress(). + * + * The second argument is the expected uncompressed data size. The third + * argument is here for the check_complete flag. + * + * Returns the decompressed data, or raises an error if decompression fails. + */ +Datum +test_pglz_decompress(PG_FUNCTION_ARGS) +{ + bytea *input = PG_GETARG_BYTEA_PP(0); + int32 rawsize = PG_GETARG_INT32(1); + bool check_complete = PG_GETARG_BOOL(2); + char *source = VARDATA_ANY(input); + int32 slen = VARSIZE_ANY_EXHDR(input); + bytea *result; + int32 dlen; + + if (rawsize < 0) + elog(ERROR, "rawsize must not be negative"); + + result = (bytea *) palloc(rawsize + VARHDRSZ); + + dlen = pglz_decompress(source, slen, VARDATA(result), + rawsize, check_complete); + if (dlen < 0) + elog(ERROR, "pglz_decompress failed"); + + SET_VARSIZE(result, dlen + VARHDRSZ); + PG_RETURN_BYTEA_P(result); +} diff --git a/src/test/modules/test_compression/test_compression.control b/src/test/modules/test_compression/test_compression.control new file mode 100644 index 000000000000..f707d4dfcf51 --- /dev/null +++ b/src/test/modules/test_compression/test_compression.control @@ -0,0 +1,4 @@ +comment = 'Test code for compression methods' +default_version = '1.0' +module_pathname = '$libdir/test_compression' +relocatable = true -- 2.53.0