From 6b106f52ad9e1933b727e05539261e51f9209075 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 27 Feb 2026 11:40:53 +0900 Subject: [PATCH v1 1/2] test_saslprep: Add test module to stress SASLprep This 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 endpoints how pg_saslprep() works. --- 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 | 150 ++++++++++ src/test/modules/test_saslprep/meson.build | 38 +++ .../test_saslprep/sql/test_saslprep.sql | 14 + .../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 + doc/src/sgml/regress.sgml | 10 + 13 files changed, 618 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/src/test/modules/Makefile b/src/test/modules/Makefile index 44c7163c1cd5..0bb5dc7e2088 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -44,6 +44,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 2634a519935a..d20a97e8af32 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -45,6 +45,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 000000000000..5dcb3ff97235 --- /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 000000000000..f74375ee4ab4 --- /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 000000000000..37e32fdc5669 --- /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 thin +wrappers standing on top of 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 000000000000..5a0ded7b4214 --- /dev/null +++ b/src/test/modules/test_saslprep/expected/test_saslprep.out @@ -0,0 +1,150 @@ +-- 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, skip nul (0) and '\' (92) as invalid bytea. +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(1,91) as a; + dat | byt | test_saslprep +----------+------+------------------- + \x01 | \x01 | ("\\x01",SUCCESS) + \x02 | \x02 | ("\\x02",SUCCESS) + \x03 | \x03 | ("\\x03",SUCCESS) + \x04 | \x04 | ("\\x04",SUCCESS) + \x05 | \x05 | ("\\x05",SUCCESS) + \x06 | \x06 | ("\\x06",SUCCESS) + \x07 | \x07 | ("\\x07",SUCCESS) + \x08 | \x08 | ("\\x08",SUCCESS) + | \x09 | ("\\x09",SUCCESS) + +| \x0a | ("\\x0a",SUCCESS) + | | + \x0B | \x0b | ("\\x0b",SUCCESS) + \x0C | \x0c | ("\\x0c",SUCCESS) + \r | \x0d | ("\\x0d",SUCCESS) + \x0E | \x0e | ("\\x0e",SUCCESS) + \x0F | \x0f | ("\\x0f",SUCCESS) + \x10 | \x10 | ("\\x10",SUCCESS) + \x11 | \x11 | ("\\x11",SUCCESS) + \x12 | \x12 | ("\\x12",SUCCESS) + \x13 | \x13 | ("\\x13",SUCCESS) + \x14 | \x14 | ("\\x14",SUCCESS) + \x15 | \x15 | ("\\x15",SUCCESS) + \x16 | \x16 | ("\\x16",SUCCESS) + \x17 | \x17 | ("\\x17",SUCCESS) + \x18 | \x18 | ("\\x18",SUCCESS) + \x19 | \x19 | ("\\x19",SUCCESS) + \x1A | \x1a | ("\\x1a",SUCCESS) + \x1B | \x1b | ("\\x1b",SUCCESS) + \x1C | \x1c | ("\\x1c",SUCCESS) + \x1D | \x1d | ("\\x1d",SUCCESS) + \x1E | \x1e | ("\\x1e",SUCCESS) + \x1F | \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) +(91 rows) + +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(93,127) as a; + dat | byt | test_saslprep +------+------+------------------- + ] | \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 | ("\\x7f",SUCCESS) +(35 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 000000000000..2fcc403ca072 --- /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 000000000000..0b6c3d3a8e61 --- /dev/null +++ b/src/test/modules/test_saslprep/sql/test_saslprep.sql @@ -0,0 +1,14 @@ +-- Tests for SASLprep + +CREATE EXTENSION test_saslprep; + +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + +-- Range of ASCII characters, skip nul (0) and '\' (92) as invalid bytea. +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(1,91) as a; +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(93,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 000000000000..b2b40e9108b6 --- /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 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', "Only nul authorized for all valid UTF8 codepoints"); + +$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 000000000000..01e5244809e7 --- /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 000000000000..c57627cc53f8 --- /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 (unlikely(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 000000000000..13015c43880f --- /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 diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml index d80dd46c5fdb..285d06195336 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 -- 2.53.0