Skip to content

ADBM-3574: Fix JSON support for pgbackrest recovery filters - #113

Open
VoidZeroNull0 wants to merge 5 commits into
2.54-cifrom
ADBM-3574
Open

VoidZeroNull0 wants to merge 5 commits into
2.54-cifrom
ADBM-3574

Conversation

@VoidZeroNull0

@VoidZeroNull0 VoidZeroNull0 commented Oct 28, 2026 •

Copy link
Copy Markdown

What happened?
Restore failed when a --filter value contained a "\uXXXX" escape
outside the ASCII range - e.g. a surrogate pair like "\uD83E\uDD52".
The parser only supported "\u00XX" and threw on anything else.

How do we fix this?
Decode \uXXXX escapes into UTF-8, including surrogate pairs: combine
high/low surrogates into one code point and encode it as 1–4 UTF-8
bytes. Extracted the logic into jsonDecodeUnicode() function.

Tests?
One success test for the surrogate-pair path was added. One test per
error branch was also added.

Ticket: ADBM-3574

Try the first solution.
This test now passes, it is not needed.
Was wrong this->json++.
Get back this->json++, add this->json--.
Make a separate function, add tests for error branches.
@VoidZeroNull0
VoidZeroNull0 marked this pull request as ready for review October 29, 2026 14:19
@VoidZeroNull0

Copy link
Copy Markdown
Author

Tests seem to fail because of missing docker image. Nothing related to the patch.

Comment thread src/common/type/json.c
Comment on lines +972 to +989
else if (codepoint <= 0x7FF)
{
strCatChr(result, (char)(0xC0 | (codepoint >> 6)));
strCatChr(result, (char)(0x80 | (codepoint & 0x3F)));
}
else if (codepoint <= 0xFFFF)
{
strCatChr(result, (char)(0xE0 | (codepoint >> 12)));
strCatChr(result, (char)(0x80 | ((codepoint >> 6) & 0x3F)));
strCatChr(result, (char)(0x80 | (codepoint & 0x3F)));
}
else
{
strCatChr(result, (char)(0xF0 | (codepoint >> 18)));
strCatChr(result, (char)(0x80 | ((codepoint >> 12) & 0x3F)));
strCatChr(result, (char)(0x80 | ((codepoint >> 6) & 0x3F)));
strCatChr(result, (char)(0x80 | (codepoint & 0x3F)));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these lines are not covered by tests

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is precisely why the CI is red: it checks the coverage of the ENTIRE codebase.

Comment thread src/common/type/json.h
// Unicode escape
FN_EXTERN void jsonDecodeUnicode(JsonRead *this, String *result);

// Convert JSON to Variant

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you make this function public?

Comment thread src/common/type/json.c
Comment on lines +941 to +944
FN_EXTERN void
jsonDecodeUnicode(JsonRead *const this, String *const result)
{
this->json++; // get past /u

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add FUNCTION_TEST_BEGIN/PARAM/RETURN and ASSERT which CODING.md requires

Comment thread src/common/type/json.c
strCatChr(result, (char)cvtZSubNToUIntBase(this->json, 0, 2, 16));
this->json++;

jsonDecodeUnicode(this, result);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now jsonReadStr does not validate the input data, and jsonDecodeUnicode does not check it either.

@RekGRpth

Copy link
Copy Markdown
Member

Restore failed when a --filter value contained a "\uXXXX" escape
outside the ASCII range - e.g. a surrogate pair like "\uD83E\uDD52".

There is no test for the original bug: \uD83E\uDD52 via jsonReadSkip/jsonValidate, and in partialRestoreTest.c.

Comment thread src/common/type/json.c
Comment on lines +940 to +993
/**********************************************************************************************************************************/
FN_EXTERN void
jsonDecodeUnicode(JsonRead *const this, String *const result)
{
this->json++; // get past /u
const unsigned int high = cvtZSubNToUIntBase(this->json, 0, 4, 16);
unsigned int codepoint = high;
this->json += 4;

// Check for supplementary planes
if (high >= 0xD800 && high <= 0xDBFF)
{
if (*this->json != '\\' || *(this->json + 1) != 'u')
THROW_FMT(JsonFormatError, "missing low surrogate for high surrogate at: %s", this->json - 6);

this->json += 2;
const unsigned int low = cvtZSubNToUIntBase(this->json, 0, 4, 16);
this->json += 4;

if (low < 0xDC00 || low > 0xDFFF)
THROW_FMT(JsonFormatError, "invalid low surrogate in unicode escape at: %s", this->json - 6);

codepoint = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00);
}
else if (high >= 0xDC00 && high <= 0xDFFF)
THROW_FMT(JsonFormatError, "unexpected low surrogate without high surrogate at: %s", this->json - 4);

// Append 1-4 bytes to the result
if (codepoint <= 0x7F)
{
strCatChr(result, (char)codepoint);
}
else if (codepoint <= 0x7FF)
{
strCatChr(result, (char)(0xC0 | (codepoint >> 6)));
strCatChr(result, (char)(0x80 | (codepoint & 0x3F)));
}
else if (codepoint <= 0xFFFF)
{
strCatChr(result, (char)(0xE0 | (codepoint >> 12)));
strCatChr(result, (char)(0x80 | ((codepoint >> 6) & 0x3F)));
strCatChr(result, (char)(0x80 | (codepoint & 0x3F)));
}
else
{
strCatChr(result, (char)(0xF0 | (codepoint >> 18)));
strCatChr(result, (char)(0x80 | ((codepoint >> 12) & 0x3F)));
strCatChr(result, (char)(0x80 | ((codepoint >> 6) & 0x3F)));
strCatChr(result, (char)(0x80 | (codepoint & 0x3F)));
}

this->json--;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsonDecodeUnicode() lacks validation for 4 hex digits (it reads past the \0 terminator, throws FormatError instead of JsonFormatError, and strtoull accepts extraneous input).

Comment thread src/common/type/json.c
for (; digitIdx < 4; digitIdx++)
{
if (!isdigit(this->json[digitIdx]))
if (!isxdigit(this->json[digitIdx]))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A \ before \0 in jsonReadSkipStr causes the pointer to move past the end of the string (fixed upstream in pgbackrest/pgbackrest@b623071a2).

@RekGRpth

Copy link
Copy Markdown
Member

\u0000 results in a NUL byte in the string (while upstream now rejects this).

Comment thread src/common/type/json.c
codepoint = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00);
}
else if (high >= 0xDC00 && high <= 0xDFFF)
THROW_FMT(JsonFormatError, "unexpected low surrogate without high surrogate at: %s", this->json - 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
THROW_FMT(JsonFormatError, "unexpected low surrogate without high surrogate at: %s", this->json - 4);
THROW_FMT(JsonFormatError, "unexpected low surrogate without high surrogate at: %s", this->json - 6);

to add \u in error message like above

@KnightMurloc

Copy link
Copy Markdown

Do we really need to actually decode UTF-16 strings? During WAL filtering, we do not use text table names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants