From aa73838a5c94457552d261968f63ee6ea6e366f5 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 19 Mar 2026 13:03:30 +0900 Subject: [PATCH] test_saslprep: Test module for SASLprep() This module includes two functions: - test_saslprep(), that performs pg_saslprep on a bytea. - test_saslprep_ranges(), able to check for all valid ranges of UTF-8 codepoints pg_saslprep() handles each one of them. This provides a detailed coverage of our implementation of SASLprep() used for SCRAM, with: - ASCII characters. - Incomplete UTF-8 sequences, for 390b3cbbb2af (later backpatched). - A more advanced check for all the valid UTF-8 ranges of codepoints, to check for cases where these generate an empty password, based on an original suggestion from Heikki Linnakangas. This part consumes resources and time, so it is implemented as a TAP test under a new PG_TEST_EXTRA value. A different patch is still under discussion to tweak our internal SASLprep() implementation, and this module can be used to track any changes in behavior. Author: Michael Paquier Reviewed-by: John Naylor Discussion: https://postgr.es/m/aaEJ-El2seZHeFcG@paquier.xyz --- doc/src/sgml/regress.sgml | 10 + src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_saslprep/.gitignore | 4 + src/test/modules/test_saslprep/Makefile | 25 ++ src/test/modules/test_saslprep/README | 25 ++ .../test_saslprep/expected/test_saslprep.out | 152 ++++++++++ src/test/modules/test_saslprep/meson.build | 38 +++ .../test_saslprep/sql/test_saslprep.sql | 19 ++ .../test_saslprep/t/001_saslprep_ranges.pl | 38 +++ .../test_saslprep/test_saslprep--1.0.sql | 30 ++ .../modules/test_saslprep/test_saslprep.c | 277 ++++++++++++++++++ .../test_saslprep/test_saslprep.control | 5 + 13 files changed, 625 insertions(+) create mode 100644 src/test/modules/test_saslprep/.gitignore create mode 100644 src/test/modules/test_saslprep/Makefile create mode 100644 src/test/modules/test_saslprep/README create mode 100644 src/test/modules/test_saslprep/expected/test_saslprep.out create mode 100644 src/test/modules/test_saslprep/meson.build create mode 100644 src/test/modules/test_saslprep/sql/test_saslprep.sql create mode 100644 src/test/modules/test_saslprep/t/001_saslprep_ranges.pl create mode 100644 src/test/modules/test_saslprep/test_saslprep--1.0.sql create mode 100644 src/test/modules/test_saslprep/test_saslprep.c create mode 100644 src/test/modules/test_saslprep/test_saslprep.control diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml index 43f208df272..873387ec168 100644 --- a/doc/src/sgml/regress.sgml +++ b/doc/src/sgml/regress.sgml @@ -342,6 +342,16 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption' + + saslprep + + + Runs the TAP test suite under src/test/modules/test_saslprep. + Not enabled by default because it is resource-intensive. + + + + sepgsql diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index a1540269cf5..28ce3b35eda 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -46,6 +46,7 @@ SUBDIRS = \ test_regex \ test_resowner \ test_rls_hooks \ + test_saslprep \ test_shm_mq \ test_slru \ test_tidstore \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 7c052803c98..3ac291656c1 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -47,6 +47,7 @@ subdir('test_rbtree') subdir('test_regex') subdir('test_resowner') subdir('test_rls_hooks') +subdir('test_saslprep') subdir('test_shm_mq') subdir('test_slru') subdir('test_tidstore') diff --git a/src/test/modules/test_saslprep/.gitignore b/src/test/modules/test_saslprep/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/src/test/modules/test_saslprep/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_saslprep/Makefile b/src/test/modules/test_saslprep/Makefile new file mode 100644 index 00000000000..f74375ee4ab --- /dev/null +++ b/src/test/modules/test_saslprep/Makefile @@ -0,0 +1,25 @@ +# src/test/modules/test_saslprep/Makefile + +MODULE_big = test_saslprep +OBJS = \ + $(WIN32RES) \ + test_saslprep.o +PGFILEDESC = "test_saslprep - test SASLprep implementation" + +EXTENSION = test_saslprep +DATA = test_saslprep--1.0.sql + +REGRESS = test_saslprep + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_saslprep +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_saslprep/README b/src/test/modules/test_saslprep/README new file mode 100644 index 00000000000..3064436df01 --- /dev/null +++ b/src/test/modules/test_saslprep/README @@ -0,0 +1,25 @@ +src/test/modules/test_saslprep + +Tests for SASLprep +================== + +This repository contains a test suite for stressing the SASLprep +implementation internal to PostgreSQL. + +It provides a set of functions able to check the validity of a SASLprep +operation for a single byte as well as a range of these, acting as +wrappers around pg_saslprep(). + +Running the tests +================= + +NOTE: A portion of the tests requires --enable-tap-tests, with +PG_TEST_EXTRA=saslprep set to run the TAP test suite. + +Run + make check PG_TEST_EXTRA=saslprep +or + make installcheck PG_TEST_EXTRA=saslprep + +The SQL test suite can run with or without PG_TEST_EXTRA=saslprep +set. diff --git a/src/test/modules/test_saslprep/expected/test_saslprep.out b/src/test/modules/test_saslprep/expected/test_saslprep.out new file mode 100644 index 00000000000..f72dbffa0a1 --- /dev/null +++ b/src/test/modules/test_saslprep/expected/test_saslprep.out @@ -0,0 +1,152 @@ +-- Tests for SASLprep +CREATE EXTENSION test_saslprep; +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + test_saslprep +----------------- + (,INVALID_UTF8) +(1 row) + +-- Range of ASCII characters. +SELECT + CASE + WHEN a = 0 THEN '' + WHEN a < 32 THEN '' + WHEN a = 127 THEN '' + ELSE chr(a) END AS dat, + set_byte('\x00'::bytea, 0, a) AS byt, + test_saslprep(set_byte('\x00'::bytea, 0, a)) AS saslprep + FROM generate_series(0,127) AS a; + dat | byt | saslprep +----------+------+------------------- + | \x00 | ("\\x",SUCCESS) + | \x01 | ("\\x01",SUCCESS) + | \x02 | ("\\x02",SUCCESS) + | \x03 | ("\\x03",SUCCESS) + | \x04 | ("\\x04",SUCCESS) + | \x05 | ("\\x05",SUCCESS) + | \x06 | ("\\x06",SUCCESS) + | \x07 | ("\\x07",SUCCESS) + | \x08 | ("\\x08",SUCCESS) + | \x09 | ("\\x09",SUCCESS) + | \x0a | ("\\x0a",SUCCESS) + | \x0b | ("\\x0b",SUCCESS) + | \x0c | ("\\x0c",SUCCESS) + | \x0d | ("\\x0d",SUCCESS) + | \x0e | ("\\x0e",SUCCESS) + | \x0f | ("\\x0f",SUCCESS) + | \x10 | ("\\x10",SUCCESS) + | \x11 | ("\\x11",SUCCESS) + | \x12 | ("\\x12",SUCCESS) + | \x13 | ("\\x13",SUCCESS) + | \x14 | ("\\x14",SUCCESS) + | \x15 | ("\\x15",SUCCESS) + | \x16 | ("\\x16",SUCCESS) + | \x17 | ("\\x17",SUCCESS) + | \x18 | ("\\x18",SUCCESS) + | \x19 | ("\\x19",SUCCESS) + | \x1a | ("\\x1a",SUCCESS) + | \x1b | ("\\x1b",SUCCESS) + | \x1c | ("\\x1c",SUCCESS) + | \x1d | ("\\x1d",SUCCESS) + | \x1e | ("\\x1e",SUCCESS) + | \x1f | ("\\x1f",SUCCESS) + | \x20 | ("\\x20",SUCCESS) + ! | \x21 | ("\\x21",SUCCESS) + " | \x22 | ("\\x22",SUCCESS) + # | \x23 | ("\\x23",SUCCESS) + $ | \x24 | ("\\x24",SUCCESS) + % | \x25 | ("\\x25",SUCCESS) + & | \x26 | ("\\x26",SUCCESS) + ' | \x27 | ("\\x27",SUCCESS) + ( | \x28 | ("\\x28",SUCCESS) + ) | \x29 | ("\\x29",SUCCESS) + * | \x2a | ("\\x2a",SUCCESS) + + | \x2b | ("\\x2b",SUCCESS) + , | \x2c | ("\\x2c",SUCCESS) + - | \x2d | ("\\x2d",SUCCESS) + . | \x2e | ("\\x2e",SUCCESS) + / | \x2f | ("\\x2f",SUCCESS) + 0 | \x30 | ("\\x30",SUCCESS) + 1 | \x31 | ("\\x31",SUCCESS) + 2 | \x32 | ("\\x32",SUCCESS) + 3 | \x33 | ("\\x33",SUCCESS) + 4 | \x34 | ("\\x34",SUCCESS) + 5 | \x35 | ("\\x35",SUCCESS) + 6 | \x36 | ("\\x36",SUCCESS) + 7 | \x37 | ("\\x37",SUCCESS) + 8 | \x38 | ("\\x38",SUCCESS) + 9 | \x39 | ("\\x39",SUCCESS) + : | \x3a | ("\\x3a",SUCCESS) + ; | \x3b | ("\\x3b",SUCCESS) + < | \x3c | ("\\x3c",SUCCESS) + = | \x3d | ("\\x3d",SUCCESS) + > | \x3e | ("\\x3e",SUCCESS) + ? | \x3f | ("\\x3f",SUCCESS) + @ | \x40 | ("\\x40",SUCCESS) + A | \x41 | ("\\x41",SUCCESS) + B | \x42 | ("\\x42",SUCCESS) + C | \x43 | ("\\x43",SUCCESS) + D | \x44 | ("\\x44",SUCCESS) + E | \x45 | ("\\x45",SUCCESS) + F | \x46 | ("\\x46",SUCCESS) + G | \x47 | ("\\x47",SUCCESS) + H | \x48 | ("\\x48",SUCCESS) + I | \x49 | ("\\x49",SUCCESS) + J | \x4a | ("\\x4a",SUCCESS) + K | \x4b | ("\\x4b",SUCCESS) + L | \x4c | ("\\x4c",SUCCESS) + M | \x4d | ("\\x4d",SUCCESS) + N | \x4e | ("\\x4e",SUCCESS) + O | \x4f | ("\\x4f",SUCCESS) + P | \x50 | ("\\x50",SUCCESS) + Q | \x51 | ("\\x51",SUCCESS) + R | \x52 | ("\\x52",SUCCESS) + S | \x53 | ("\\x53",SUCCESS) + T | \x54 | ("\\x54",SUCCESS) + U | \x55 | ("\\x55",SUCCESS) + V | \x56 | ("\\x56",SUCCESS) + W | \x57 | ("\\x57",SUCCESS) + X | \x58 | ("\\x58",SUCCESS) + Y | \x59 | ("\\x59",SUCCESS) + Z | \x5a | ("\\x5a",SUCCESS) + [ | \x5b | ("\\x5b",SUCCESS) + \ | \x5c | ("\\x5c",SUCCESS) + ] | \x5d | ("\\x5d",SUCCESS) + ^ | \x5e | ("\\x5e",SUCCESS) + _ | \x5f | ("\\x5f",SUCCESS) + ` | \x60 | ("\\x60",SUCCESS) + a | \x61 | ("\\x61",SUCCESS) + b | \x62 | ("\\x62",SUCCESS) + c | \x63 | ("\\x63",SUCCESS) + d | \x64 | ("\\x64",SUCCESS) + e | \x65 | ("\\x65",SUCCESS) + f | \x66 | ("\\x66",SUCCESS) + g | \x67 | ("\\x67",SUCCESS) + h | \x68 | ("\\x68",SUCCESS) + i | \x69 | ("\\x69",SUCCESS) + j | \x6a | ("\\x6a",SUCCESS) + k | \x6b | ("\\x6b",SUCCESS) + l | \x6c | ("\\x6c",SUCCESS) + m | \x6d | ("\\x6d",SUCCESS) + n | \x6e | ("\\x6e",SUCCESS) + o | \x6f | ("\\x6f",SUCCESS) + p | \x70 | ("\\x70",SUCCESS) + q | \x71 | ("\\x71",SUCCESS) + r | \x72 | ("\\x72",SUCCESS) + s | \x73 | ("\\x73",SUCCESS) + t | \x74 | ("\\x74",SUCCESS) + u | \x75 | ("\\x75",SUCCESS) + v | \x76 | ("\\x76",SUCCESS) + w | \x77 | ("\\x77",SUCCESS) + x | \x78 | ("\\x78",SUCCESS) + y | \x79 | ("\\x79",SUCCESS) + z | \x7a | ("\\x7a",SUCCESS) + { | \x7b | ("\\x7b",SUCCESS) + | | \x7c | ("\\x7c",SUCCESS) + } | \x7d | ("\\x7d",SUCCESS) + ~ | \x7e | ("\\x7e",SUCCESS) + | \x7f | ("\\x7f",SUCCESS) +(128 rows) + +DROP EXTENSION test_saslprep; diff --git a/src/test/modules/test_saslprep/meson.build b/src/test/modules/test_saslprep/meson.build new file mode 100644 index 00000000000..2fcc403ca07 --- /dev/null +++ b/src/test/modules/test_saslprep/meson.build @@ -0,0 +1,38 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_saslprep_sources = files( + 'test_saslprep.c', +) + +if host_system == 'windows' + test_saslprep_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_saslprep', + '--FILEDESC', 'test_saslprep - test SASLprep implementation',]) +endif + +test_saslprep = shared_module('test_saslprep', + test_saslprep_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_saslprep + +test_install_data += files( + 'test_saslprep.control', + 'test_saslprep--1.0.sql', +) + +tests += { + 'name': 'test_saslprep', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_saslprep', + ], + }, + 'tap': { + 'tests': [ + 't/001_saslprep_ranges.pl', + ], + }, +} diff --git a/src/test/modules/test_saslprep/sql/test_saslprep.sql b/src/test/modules/test_saslprep/sql/test_saslprep.sql new file mode 100644 index 00000000000..00bad48eca7 --- /dev/null +++ b/src/test/modules/test_saslprep/sql/test_saslprep.sql @@ -0,0 +1,19 @@ +-- Tests for SASLprep + +CREATE EXTENSION test_saslprep; + +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + +-- Range of ASCII characters. +SELECT + CASE + WHEN a = 0 THEN '' + WHEN a < 32 THEN '' + WHEN a = 127 THEN '' + ELSE chr(a) END AS dat, + set_byte('\x00'::bytea, 0, a) AS byt, + test_saslprep(set_byte('\x00'::bytea, 0, a)) AS saslprep + FROM generate_series(0,127) AS a; + +DROP EXTENSION test_saslprep; diff --git a/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl b/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl new file mode 100644 index 00000000000..b353017c065 --- /dev/null +++ b/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl @@ -0,0 +1,38 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test all ranges of valid UTF-8 codepoints under SASLprep. +# +# This test is expensive and enabled with PG_TEST_EXTRA, which is +# why it exists as a TAP test. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use Test::More; + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bsaslprep\b/) +{ + plan skip_all => "test saslprep not enabled in PG_TEST_EXTRA"; +} + +# Initialize node +my $node = PostgreSQL::Test::Cluster->new('main'); + +$node->init; +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_saslprep;'); + +# Among all the valid UTF-8 codepoint ranges, our implementation of +# SASLprep should never return an empty password if the operation is +# considered a success. +# The only exception is currently the nul character, prohibited in +# input of CREATE/ALTER ROLE. +my $result = $node->safe_psql( + 'postgres', qq[SELECT * FROM test_saslprep_ranges() + WHERE status = 'SUCCESS' AND res IN (NULL, '') +]); + +is($result, 'U+0000|SUCCESS|\x00|\x', "valid codepoints returning an empty password"); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_saslprep/test_saslprep--1.0.sql b/src/test/modules/test_saslprep/test_saslprep--1.0.sql new file mode 100644 index 00000000000..01e5244809e --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep--1.0.sql @@ -0,0 +1,30 @@ +/* src/test/modules/test_saslprep/test_saslprep--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_saslprep" to load this file. \quit + +-- +-- test_saslprep(bytea) +-- +-- Tests single byte sequence in SASLprep. +-- +CREATE FUNCTION test_saslprep(IN src bytea, + OUT res bytea, + OUT status text) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- test_saslprep_ranges +-- +-- Tests all possible ranges of byte sequences in SASLprep. +-- +CREATE FUNCTION test_saslprep_ranges( + OUT codepoint text, + OUT status text, + OUT src bytea, + OUT res bytea) +RETURNS SETOF record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; diff --git a/src/test/modules/test_saslprep/test_saslprep.c b/src/test/modules/test_saslprep/test_saslprep.c new file mode 100644 index 00000000000..b4deab1b52f --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep.c @@ -0,0 +1,277 @@ +/*-------------------------------------------------------------------------- + * + * test_saslprep.c + * Test harness for the SASLprep implementation. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_saslprep/test_saslprep.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "common/saslprep.h" +#include "fmgr.h" +#include "funcapi.h" +#include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +static const char * +saslprep_status_to_text(pg_saslprep_rc rc) +{ + const char *status = "???"; + + switch (rc) + { + case SASLPREP_OOM: + status = "OOM"; + break; + case SASLPREP_SUCCESS: + status = "SUCCESS"; + break; + case SASLPREP_INVALID_UTF8: + status = "INVALID_UTF8"; + break; + case SASLPREP_PROHIBITED: + status = "PROHIBITED"; + break; + } + + return status; +} + +/* + * Simple function to test SASLprep with arbitrary bytes as input. + * + * This takes a bytea in input, returning in output the generating data as + * bytea with the status returned by pg_saslprep(). + */ +PG_FUNCTION_INFO_V1(test_saslprep); +Datum +test_saslprep(PG_FUNCTION_ARGS) +{ + bytea *string = PG_GETARG_BYTEA_PP(0); + char *src; + Size src_len; + char *input_data; + char *result; + Size result_len; + bytea *result_bytea = NULL; + const char *status = NULL; + Datum *values; + bool *nulls; + TupleDesc tupdesc; + pg_saslprep_rc rc; + + /* determine result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + values = palloc0_array(Datum, tupdesc->natts); + nulls = palloc0_array(bool, tupdesc->natts); + + src_len = VARSIZE_ANY_EXHDR(string); + src = VARDATA_ANY(string); + + /* + * Copy the input given, to make SASLprep() act on a sanitized string. + */ + input_data = palloc0(src_len + 1); + strlcpy(input_data, src, src_len + 1); + + rc = pg_saslprep(input_data, &result); + status = saslprep_status_to_text(rc); + + if (result) + { + result_len = strlen(result); + result_bytea = palloc(result_len + VARHDRSZ); + SET_VARSIZE(result_bytea, result_len + VARHDRSZ); + memcpy(VARDATA(result_bytea), result, result_len); + values[0] = PointerGetDatum(result_bytea); + } + else + nulls[0] = true; + + values[1] = CStringGetTextDatum(status); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* Context structure for set-returning function with ranges */ +typedef struct +{ + int current_range; + char32_t current_codepoint; +} pg_saslprep_test_context; + +/* + * UTF-8 code point ranges. + */ +typedef struct +{ + char32_t start_codepoint; + char32_t end_codepoint; +} pg_utf8_codepoint_range; + +static const pg_utf8_codepoint_range pg_utf8_test_ranges[] = { + /* 1, 2, 3 bytes */ + {0x0000, 0xD7FF}, /* Basic Multilingual Plane, before surrogates */ + {0xE000, 0xFFFF}, /* Basic Multilingual Plane, after surrogates */ + /* 4 bytes */ + {0x10000, 0x1FFFF}, /* Supplementary Multilingual Plane */ + {0x20000, 0x2FFFF}, /* Supplementary Ideographic Plane */ + {0x30000, 0x3FFFF}, /* Tertiary Ideographic Plane */ + {0x40000, 0xDFFFF}, /* Unassigned planes */ + {0xE0000, 0xEFFFF}, /* Supplementary Special-purpose Plane */ + {0xF0000, 0xFFFFF}, /* Private Use Area A */ + {0x100000, 0x10FFFF}, /* Private Use Area B */ +}; + +#define PG_UTF8_TEST_RANGES_LEN \ + (sizeof(pg_utf8_test_ranges) / sizeof(pg_utf8_test_ranges[0])) + + +/* + * test_saslprep_ranges + * + * Test SASLprep across various UTF-8 ranges. + */ +PG_FUNCTION_INFO_V1(test_saslprep_ranges); +Datum +test_saslprep_ranges(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + pg_saslprep_test_context *ctx; + HeapTuple tuple; + Datum result; + + /* First call setup */ + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + TupleDesc tupdesc; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + funcctx->tuple_desc = tupdesc; + + /* Allocate context with range setup */ + ctx = (pg_saslprep_test_context *) palloc(sizeof(pg_saslprep_test_context)); + ctx->current_range = 0; + ctx->current_codepoint = pg_utf8_test_ranges[0].start_codepoint; + funcctx->user_fctx = ctx; + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + ctx = (pg_saslprep_test_context *) funcctx->user_fctx; + + while (ctx->current_range < PG_UTF8_TEST_RANGES_LEN) + { + char32_t codepoint = ctx->current_codepoint; + unsigned char utf8_buf[5]; + char input_str[6]; + char *output = NULL; + pg_saslprep_rc rc; + int utf8_len; + const char *status; + bytea *input_bytea; + bytea *output_bytea; + char codepoint_str[16]; + Datum values[4] = {0}; + bool nulls[4] = {0}; + const pg_utf8_codepoint_range *range = + &pg_utf8_test_ranges[ctx->current_range]; + + CHECK_FOR_INTERRUPTS(); + + /* Switch to next range if finished with the previous one */ + if (ctx->current_codepoint > range->end_codepoint) + { + ctx->current_range++; + if (ctx->current_range < PG_UTF8_TEST_RANGES_LEN) + ctx->current_codepoint = + pg_utf8_test_ranges[ctx->current_range].start_codepoint; + continue; + } + + codepoint = ctx->current_codepoint; + + /* Convert code point to UTF-8 */ + utf8_len = unicode_utf8len(codepoint); + if (utf8_len == 0) + { + ctx->current_codepoint++; + continue; + } + unicode_to_utf8(codepoint, utf8_buf); + + /* Create null-terminated string */ + memcpy(input_str, utf8_buf, utf8_len); + input_str[utf8_len] = '\0'; + + /* Test with pg_saslprep */ + rc = pg_saslprep(input_str, &output); + + /* Prepare output values */ + memset(nulls, false, sizeof(nulls)); + + /* codepoint as text U+XXXX format */ + if (codepoint <= 0xFFFF) + snprintf(codepoint_str, sizeof(codepoint_str), "U+%04X", codepoint); + else + snprintf(codepoint_str, sizeof(codepoint_str), "U+%06X", codepoint); + values[0] = CStringGetTextDatum(codepoint_str); + + /* status */ + status = saslprep_status_to_text(rc); + values[1] = CStringGetTextDatum(status); + + /* input_bytes */ + input_bytea = (bytea *) palloc(VARHDRSZ + utf8_len); + SET_VARSIZE(input_bytea, VARHDRSZ + utf8_len); + memcpy(VARDATA(input_bytea), utf8_buf, utf8_len); + values[2] = PointerGetDatum(input_bytea); + + /* output_bytes */ + if (output != NULL) + { + int output_len = strlen(output); + + output_bytea = (bytea *) palloc(VARHDRSZ + output_len); + SET_VARSIZE(output_bytea, VARHDRSZ + output_len); + memcpy(VARDATA(output_bytea), output, output_len); + values[3] = PointerGetDatum(output_bytea); + pfree(output); + } + else + { + nulls[3] = true; + values[3] = (Datum) 0; + } + + /* Build and return tuple */ + tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); + result = HeapTupleGetDatum(tuple); + + /* Move to next code point */ + ctx->current_codepoint++; + + SRF_RETURN_NEXT(funcctx, result); + } + + /* All done */ + SRF_RETURN_DONE(funcctx); +} diff --git a/src/test/modules/test_saslprep/test_saslprep.control b/src/test/modules/test_saslprep/test_saslprep.control new file mode 100644 index 00000000000..13015c43880 --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep.control @@ -0,0 +1,5 @@ +# test_saslprep extension +comment = 'Test SASLprep implementation' +default_version = '1.0' +module_pathname = '$libdir/test_saslprep' +relocatable = true