diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out index 313c50394650a..32312c9fdec0e 100644 --- a/src/test/regress/expected/strings.out +++ b/src/test/regress/expected/strings.out @@ -2914,6 +2914,96 @@ SELECT decode('abc-_w==', 'base64url'); -- should decode to \x69b73eff \x69b73eff (1 row) +-- +-- base64/base64url property-style regression tests +-- +-- These guard the truncation rules: base64 requires '=' padding while +-- base64url accepts unpadded input (see commits e1d917182 and the +-- follow-up "Fix truncation rules for base64 encoding"). +-- +-- Round-trip identity encode(decode(encode(x))) = x for both variants +-- across input lengths 0..8, covering every padding phase (len % 3 in +-- {0,1,2}). Uses a deterministic byte pattern so output is stable. +DO $$ +DECLARE + variant text; + n int; + input bytea; + rt bytea; +BEGIN + FOREACH variant IN ARRAY ARRAY['base64', 'base64url'] LOOP + FOR n IN 0..8 LOOP + input := decode(COALESCE( + (SELECT string_agg(lpad(to_hex((i * 37 + 11) % 256), 2, '0'), '' ORDER BY i) + FROM generate_series(0, n - 1) AS i), ''), 'hex'); + rt := decode(encode(input, variant), variant); + IF rt IS DISTINCT FROM input THEN + RAISE EXCEPTION '% round-trip failed at length %: in=% out=%', + variant, n, input, rt; + END IF; + END LOOP; + END LOOP; + RAISE NOTICE 'base64/base64url round-trip identity OK'; +END; +$$; +NOTICE: base64/base64url round-trip identity OK +-- Padding/truncation rejection matrix: each unpadded trailing-group +-- length (1, 2, 3 chars) x variant. For base64 every unpadded partial +-- group is rejected; for base64url only a 1-char remainder is invalid, +-- while 2- and 3-char remainders decode without padding. +SELECT decode('A', 'base64'); -- ERROR: 1-char remainder +ERROR: invalid base64 end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +SELECT decode('AQ', 'base64'); -- ERROR: 2-char remainder needs '==' +ERROR: invalid base64 end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +SELECT decode('AQI', 'base64'); -- ERROR: 3-char remainder needs '=' +ERROR: invalid base64 end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +SELECT decode('A', 'base64url'); -- ERROR: 1-char remainder +ERROR: invalid base64url end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +SELECT decode('AQ', 'base64url'); -- ok: \x01 + decode +-------- + \x01 +(1 row) + +SELECT decode('AQI', 'base64url'); -- ok: \x0102 + decode +-------- + \x0102 +(1 row) + +-- Correctly padded base64 is accepted for the same remainders. +SELECT decode('AQ==', 'base64'); -- ok: \x01 + decode +-------- + \x01 +(1 row) + +SELECT decode('AQI=', 'base64'); -- ok: \x0102 + decode +-------- + \x0102 +(1 row) + +-- Truncated second group (stray 1-char remainder) is rejected in both. +SELECT decode('AQIDA', 'base64'); -- ERROR +ERROR: invalid base64 end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +SELECT decode('AQIDA', 'base64url'); -- ERROR +ERROR: invalid base64url end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +-- Misplaced '=' padding is rejected in both variants. +SELECT decode('=AAA', 'base64'); -- ERROR: '=' at group start +ERROR: unexpected "=" while decoding base64 sequence +SELECT decode('A=AA', 'base64'); -- ERROR: '=' after 1 char +ERROR: unexpected "=" while decoding base64 sequence +SELECT decode('=AAA', 'base64url'); -- ERROR +ERROR: unexpected "=" while decoding base64url sequence +SELECT decode('A=AA', 'base64url'); -- ERROR +ERROR: unexpected "=" while decoding base64url sequence -- -- get_bit/set_bit etc -- diff --git a/src/test/regress/expected/uuid.out b/src/test/regress/expected/uuid.out index 36d54d2f33e33..b987719ef6757 100644 --- a/src/test/regress/expected/uuid.out +++ b/src/test/regress/expected/uuid.out @@ -290,6 +290,27 @@ SELECT uuid_extract_timestamp(uuidv7('1000 years'::interval)) > now() + '999 yea t (1 row) +-- uuidv7: strictly increasing interval shifts produce strictly increasing +-- (sortable) UUID values, matching the RFC 9562 sortability guarantee. We +-- compare the uuid values themselves (not just the extracted timestamps). +DO $$ +DECLARE + prev uuid := NULL; + cur uuid; + s int; +BEGIN + FOR s IN 0..30 LOOP + cur := uuidv7((s * 100 || ' years')::interval); + IF prev IS NOT NULL AND NOT (cur > prev) THEN + RAISE EXCEPTION 'uuidv7 not monotone at shift % years: % <= %', + s * 100, cur, prev; + END IF; + prev := cur; + END LOOP; + RAISE NOTICE 'uuidv7 monotone ordering OK'; +END; +$$; +NOTICE: uuidv7 monotone ordering OK -- extract functions -- version SELECT uuid_extract_version('11111111-1111-5111-8111-111111111111'); -- 5 diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql index 38946e8954df2..72b3b3c0ab2fb 100644 --- a/src/test/regress/sql/strings.sql +++ b/src/test/regress/sql/strings.sql @@ -946,6 +946,62 @@ SELECT decode('=QQQ', 'base64url'); -- valid base64 padding in base64url (optional, but accepted) SELECT decode('abc-_w==', 'base64url'); -- should decode to \x69b73eff +-- +-- base64/base64url property-style regression tests +-- +-- These guard the truncation rules: base64 requires '=' padding while +-- base64url accepts unpadded input (see commits e1d917182 and the +-- follow-up "Fix truncation rules for base64 encoding"). +-- + +-- Round-trip identity encode(decode(encode(x))) = x for both variants +-- across input lengths 0..8, covering every padding phase (len % 3 in +-- {0,1,2}). Uses a deterministic byte pattern so output is stable. +DO $$ +DECLARE + variant text; + n int; + input bytea; + rt bytea; +BEGIN + FOREACH variant IN ARRAY ARRAY['base64', 'base64url'] LOOP + FOR n IN 0..8 LOOP + input := decode(COALESCE( + (SELECT string_agg(lpad(to_hex((i * 37 + 11) % 256), 2, '0'), '' ORDER BY i) + FROM generate_series(0, n - 1) AS i), ''), 'hex'); + rt := decode(encode(input, variant), variant); + IF rt IS DISTINCT FROM input THEN + RAISE EXCEPTION '% round-trip failed at length %: in=% out=%', + variant, n, input, rt; + END IF; + END LOOP; + END LOOP; + RAISE NOTICE 'base64/base64url round-trip identity OK'; +END; +$$; + +-- Padding/truncation rejection matrix: each unpadded trailing-group +-- length (1, 2, 3 chars) x variant. For base64 every unpadded partial +-- group is rejected; for base64url only a 1-char remainder is invalid, +-- while 2- and 3-char remainders decode without padding. +SELECT decode('A', 'base64'); -- ERROR: 1-char remainder +SELECT decode('AQ', 'base64'); -- ERROR: 2-char remainder needs '==' +SELECT decode('AQI', 'base64'); -- ERROR: 3-char remainder needs '=' +SELECT decode('A', 'base64url'); -- ERROR: 1-char remainder +SELECT decode('AQ', 'base64url'); -- ok: \x01 +SELECT decode('AQI', 'base64url'); -- ok: \x0102 +-- Correctly padded base64 is accepted for the same remainders. +SELECT decode('AQ==', 'base64'); -- ok: \x01 +SELECT decode('AQI=', 'base64'); -- ok: \x0102 +-- Truncated second group (stray 1-char remainder) is rejected in both. +SELECT decode('AQIDA', 'base64'); -- ERROR +SELECT decode('AQIDA', 'base64url'); -- ERROR +-- Misplaced '=' padding is rejected in both variants. +SELECT decode('=AAA', 'base64'); -- ERROR: '=' at group start +SELECT decode('A=AA', 'base64'); -- ERROR: '=' after 1 char +SELECT decode('=AAA', 'base64url'); -- ERROR +SELECT decode('A=AA', 'base64url'); -- ERROR + -- -- get_bit/set_bit etc -- diff --git a/src/test/regress/sql/uuid.sql b/src/test/regress/sql/uuid.sql index 944be956dff1c..c49bd5859acf0 100644 --- a/src/test/regress/sql/uuid.sql +++ b/src/test/regress/sql/uuid.sql @@ -156,6 +156,27 @@ SELECT uuidv7('9000 years'::interval); -- uuidv7: a large but in-range forward shift is accepted SELECT uuid_extract_timestamp(uuidv7('1000 years'::interval)) > now() + '999 years'::interval; +-- uuidv7: strictly increasing interval shifts produce strictly increasing +-- (sortable) UUID values, matching the RFC 9562 sortability guarantee. We +-- compare the uuid values themselves (not just the extracted timestamps). +DO $$ +DECLARE + prev uuid := NULL; + cur uuid; + s int; +BEGIN + FOR s IN 0..30 LOOP + cur := uuidv7((s * 100 || ' years')::interval); + IF prev IS NOT NULL AND NOT (cur > prev) THEN + RAISE EXCEPTION 'uuidv7 not monotone at shift % years: % <= %', + s * 100, cur, prev; + END IF; + prev := cur; + END LOOP; + RAISE NOTICE 'uuidv7 monotone ordering OK'; +END; +$$; + -- extract functions -- version