From a923da8ca4436293bb9a23ee96e253a01704db54 Mon Sep 17 00:00:00 2001
From: DoGeon Yoo <ehrjs023@gmail.com>
Date: Thu, 14 May 2026 15:46:42 +0900
Subject: [PATCH v2 2/2] Tighten pg_uhc_verifychar() to enforce CP949
 lead/trail byte ranges

Per CP949 (Windows-949), a two-byte UHC sequence requires the lead
byte to be in 0x81-0xFE and the trail byte to be in 0x41-0x5A,
0x61-0x7A, or 0x81-0xFE.

pg_uhc_verifychar() accepts any lead byte with the high bit set
(0x80-0xFF) and any trail byte other than NUL, without enforcing
those ranges.  Out-of-range pairs such as 0x80 0x41 (invalid lead)
or 0x81 0x40 (invalid trail) are accepted by the verifier; they
are rejected only later by the conversion table, with the message
"character with byte sequence ... has no equivalent in encoding
UTF8".  This makes the diagnostic misleading (the pair is not
unmappable, it is structurally invalid) and is inconsistent with
pg_euckr_verifychar(), which already enforces lead/trail ranges
explicitly.

Rewrite pg_uhc_verifychar() to check the lead and trail byte
ranges directly, following the style of pg_euckr_verifychar().
The new trail-range check also subsumes the previous
NONUTF8_INVALID sentinel check (0x8d 0x20), which is removed as
it becomes redundant -- 0x20 is not in any valid trail range, so
0x8d 0x20 is still rejected.

After this change, out-of-range pairs are rejected at the verifier
with "invalid byte sequence for encoding UHC".  The regression
test added in the previous commit captures this exactly: eight
"has no equivalent" errors become "invalid byte sequence" errors,
and no other test result changes.  The user-visible effect is the
error message format and the stage at which the byte sequence is
rejected; the accept/reject outcome for any input is unchanged.

Only client-side paths are affected since UHC is not supported as
a server encoding.

Reported-by: Henson Choi <assam258@gmail.com>
Discussion: https://www.postgresql.org/message-id/CAAAe_zBdGXsALm%3DGkUPtPx9MLcjcM5hBg3HZU%2Bnh8gKXSjXJJw%40mail.gmail.com
---
 src/common/wchar.c                | 37 ++++++++++++++++++++-----------
 src/test/regress/expected/uhc.out | 16 ++++++-------
 2 files changed, 32 insertions(+), 21 deletions(-)

diff --git a/src/common/wchar.c b/src/common/wchar.c
index 926823cabec..5f8b0333325 100644
--- a/src/common/wchar.c
+++ b/src/common/wchar.c
@@ -1387,26 +1387,37 @@ pg_gbk_verifystr(const unsigned char *s, int len)
 static int
 pg_uhc_verifychar(const unsigned char *s, int len)
 {
-	int			l,
-				mbl;
+	int			l;
+	unsigned char c1,
+				c2;
 
-	l = mbl = pg_uhc_mblen(s);
+	c1 = *s++;
 
-	if (len < l)
-		return -1;
+	if (IS_HIGHBIT_SET(c1))
+	{
+		l = 2;
+		if (l > len)
+			return -1;
 
-	if (l == 2 &&
-		s[0] == NONUTF8_INVALID_BYTE0 &&
-		s[1] == NONUTF8_INVALID_BYTE1)
-		return -1;
+		c2 = *s++;
 
-	while (--l > 0)
-	{
-		if (*++s == '\0')
+		/* CP949 lead byte must be 0x81-0xFE */
+		if (c1 < 0x81 || c1 > 0xfe)
+			return -1;
+
+		/* CP949 trail byte: 0x41-0x5A, 0x61-0x7A, or 0x81-0xFE */
+		if (!((c2 >= 0x41 && c2 <= 0x5a) ||
+			  (c2 >= 0x61 && c2 <= 0x7a) ||
+			  (c2 >= 0x81 && c2 <= 0xfe)))
 			return -1;
 	}
+	else
+		/* must be ASCII */
+	{
+		l = 1;
+	}
 
-	return mbl;
+	return l;
 }
 
 static int
diff --git a/src/test/regress/expected/uhc.out b/src/test/regress/expected/uhc.out
index cfa1c72162f..5a380e85aa7 100644
--- a/src/test/regress/expected/uhc.out
+++ b/src/test/regress/expected/uhc.out
@@ -69,22 +69,22 @@ SELECT encode(convert_to(convert_from('\xfea1', 'UHC'), 'UTF8'), 'hex');	-- high
 
 -- invalid lead byte (0x80 and 0xFF are unused in CP949)
 SELECT convert_from('\x8041', 'UHC');
-ERROR:  character with byte sequence 0x80 0x41 in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0x80 0x41
 SELECT convert_from('\xff41', 'UHC');
-ERROR:  character with byte sequence 0xff 0x41 in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0xff 0x41
 -- invalid trail byte
 SELECT convert_from('\x8140', 'UHC');	-- 0x40
-ERROR:  character with byte sequence 0x81 0x40 in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0x81 0x40
 SELECT convert_from('\x815b', 'UHC');	-- 0x5B
-ERROR:  character with byte sequence 0x81 0x5b in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0x81 0x5b
 SELECT convert_from('\x8160', 'UHC');	-- 0x60
-ERROR:  character with byte sequence 0x81 0x60 in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0x81 0x60
 SELECT convert_from('\x817b', 'UHC');	-- 0x7B
-ERROR:  character with byte sequence 0x81 0x7b in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0x81 0x7b
 SELECT convert_from('\x8180', 'UHC');	-- 0x80
-ERROR:  character with byte sequence 0x81 0x80 in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0x81 0x80
 SELECT convert_from('\x81ff', 'UHC');	-- 0xFF
-ERROR:  character with byte sequence 0x81 0xff in encoding "UHC" has no equivalent in encoding "UTF8"
+ERROR:  invalid byte sequence for encoding "UHC": 0x81 0xff
 SELECT convert_from('\x8d20', 'UHC');	-- NONUTF8_INVALID sentinel pair
 ERROR:  invalid byte sequence for encoding "UHC": 0x8d 0x20
 -- truncated two-byte character
-- 
2.43.0

