Repository navigation
Decode pg_read_binary_file log tail in the connected database's server_encoding (#4062) - #4106
Merged
Merged
Conversation
…r_encoding (#4062) Adds PgServerEncoding: an explicit map from PostgreSQL server_encoding to a .NET Encoding, built once with CodePagesEncodingProvider so every mapped name resolves to a lenient (replacement-fallback) encoding. Only exact charsets are mapped; anything else (EUC_TW, EUC_JIS_2004, LATIN6, LATIN8, LATIN10, MULE_INTERNAL, or a code page this runtime cannot resolve) is left unmapped, with no UTF-8 fallback. PgReadBinaryFileCapability's probe now reports server_encoding plus the raw privilege check, with no CASE gate; C# decides whether the encoding is served. IsGrantedAsync combines granted-and-mapped into the returned bool, and TryGetCachedEncoding exposes the mapped Encoding from the same cache entry so callers pay no second round trip. CollectorContext gains PgLogEncoding, set by DarlingCollectorRunner.ResolvePgReadBinaryFileGrantAsync alongside the grant flag. PgBinaryTailText.DecodeWhole/UnescapeAndDecode now take an Encoding parameter instead of hard-coding UTF-8; PgLogEventsCollector, PgDeadlocksCollector and PgPlanCaptureCollector pass context.PgLogEncoding. DarlingWorker's fault sentence and Darling/README.md now describe the mapped-encoding list instead of the old UTF8/SQL_ASCII-only gate. Part of #4058. Closes #4062.
…ches code page 1252
…ory (it resolves now), and route the WIN1252 live test's teardown through LiveStoreCleanup
erikdarlingdata
marked this pull request as ready for review
September 24, 2026 02:19
erikdarlingdata
enabled auto-merge (squash)
September 24, 2026 02:19
erikdarlingdata
disabled auto-merge
September 24, 2026 02:31
Owner
Author
Security review round 1 (automated reviewer)Verdict: no High findings. Fix the Medium first (small change) or accept it in writing. The rest can merge as is or go to a follow-up. Scope: the diff Can a planted byte make the decode throw? No.
Medium: a planted DBCS lead byte eats the next byte, including
|
… byte cannot eat the newline that follows it Medium finding: DBCSCodePageEncoding pairs an unmapped lead byte with whatever byte follows, including an ASCII newline. DecodeWhole and UnescapeAndDecode now split the raw bytes on 0x0A first, decode each segment, and join with '\n' — 0x0A is never a valid EUC-JP/EUC-KR/GBK trail byte, so this changes nothing for valid text. Adds DecodeWhole/UnescapeAndDecode tests for EUC_JP/EUC_CN/EUC_KR with a planted 0xA4 right before a real newline.
…approximate, not exact Low findings: the mapped code pages used DecoderFallback.ReplacementFallback, whose replacement string is a plain '?', while every comment on this path (and the served sentence at #4062) claimed U+FFFD. They now use new DecoderReplacementFallback("\uFFFD") so an undecodable or planted byte is actually distinguishable from a real '?'. Also reworded the type's header comment: EUC_CN->936 is GBK (a GB2312 superset), EUC_JP->51932 does not decode 3-byte JIS X 0212, and EUC_KR->51949 goes through the same DBCS table path — none of the three throws, but none is exact. The other mapped entries (LATIN1-4/5/7/9, ISO_8859_5..8, WIN866/874/125x, KOI8R/U) are exact and are called out as such. Fixed a dangling cref this left in PgServerEncodingTests.cs (DecoderFallback.ReplacementFallback is no longer used in product code).
…oding 'which this database's encoding is not one this collector maps' -> 'but this collector does not map this database's encoding', matching the review's suggested wording. No pin in PostgresFaultOutcomeTests referenced the old wording (ran the class: still 90/90 green).
erikdarlingdata
enabled auto-merge (squash)
September 24, 2026 02:55
This was referenced Sep 24, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 24, 2026
* Add 48 buffered CHANGELOG entries in one splice Adds 48 entries and 48 link refs (#4057, #4077, #4079, #4081, #4082, #4083, #4084, #4085, #4086, #4087, #4088, #4089, #4090, #4091, #4092, #4093, #4095, #4096, #4099, #4100, #4101, #4103, #4105, #4106, #4107, #4108, #4109, #4110, #4111, #4113, #4114, #4115, #4116, #4117, #4118, #4119, #4120, #4121, #4122, #4123, #4124, #4125, #4126, #4127, #4131, #4133, #4136, #4141). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran. Entries found under a bare section line (none unless the old script ran again) move into the matching ### section, below the new entries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC * Changelog splice: add #4137's entry, which merged before the splice but was missed #4137 (pg_plan_capture reads csvlog) merged at 05:57Z with a CHANGELOG entry section in its body. It was neither spliced nor listed as needing no entry. Its entry goes under Fixed, right after #4136's, and #4136's last sentence now points to it instead of saying plan capture is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #4058. Closes #4062.
What changed
The binary log route (
pg_read_binary_file→bytea) previously decoded the tail as UTF-8 unconditionallyand
PgReadBinaryFileCapability's probe gated the whole route to UTF8 and SQL_ASCII databases via aCASEin the SQL text. On any other server encoding, the text route (
pg_read_file) could still be blinded by oneplanted byte (EUC_* → 22021, WIN1252 → 22P05), and there was no fix available even with the grant.
PerformanceMonitor.Collectors/PgServerEncoding.cs: an explicit map of PostgreSQL SERVER encodingname → .NET code page (UTF8, SQL_ASCII, every LATIN/ISO_8859 variant listed in the brief, WIN866/874/1250–1258,
KOI8R/KOI8U, EUC_JP/EUC_CN/EUC_KR).
EUC_TW,EUC_JIS_2004,LATIN6,LATIN8,LATIN10,MULE_INTERNAL,and anything this runtime cannot resolve stay unmapped — no UTF-8 fallback. Every mapped
Encodingis builtwith
EncoderFallback.ReplacementFallback/DecoderFallback.ReplacementFallback(never throws), andCodePagesEncodingProvider.Instanceis registered in the static constructor.PgReadBinaryFileCapability.cs:ProbeSqlnow selectscurrent_setting('server_encoding')plus the rawhas_function_privilegecheck, noCASEgate — C# decides.IsGrantedAsynccombines granted-and-mapped intothe returned bool; a new
TryGetCachedEncodingexposes the mappedEncodingfrom the same cache entry (nosecond round trip).
IsCachedAsUnsupportedEncodingnow means "granted, but the encoding is onePgServerEncodingdoes not map."CollectorContext.PgLogEncoding(new, besidePgReadBinaryFileGranted): set inDarlingCollectorRunner.ResolvePgReadBinaryFileGrantAsync, always null unless the grant flag is true.PgBinaryTailText.DecodeWhole/UnescapeAndDecodetake anEncodingparameter instead of hard-codingUTF-8.
PgLogEventsCollector,PgDeadlocksCollector,PgPlanCaptureCollectorpasscontext.PgLogEncoding ?? Encoding.UTF8(the?? UTF8is defensive; the grant/encoding invariant means it'snever actually needed at the call sites, since
PgReadBinaryFileGrantedis only true whenPgLogEncodingisset).
Darling/README.md:774's paragraph now names the mapped-encoding list and the unmapped exceptionsinstead of "UTF8 or SQL_ASCII only."
DarlingWorker.LogTailUndecodableByteExplanation's "grant does not help"sentence now names the unmapped case by encoding families rather than by issue number (no
#nnnnin servedtext, per house rule — dropped the
#4062reference the sentence used to carry, and updated the two teststhat asserted on it).
Tests
PgServerEncodingTests: every mapped name resolves; an unmapped name (includingnull) returns false;WIN1252 byte 0x81 decodes without throwing (see note below); LATIN1 0xE9 → é; EUC_JP two-byte round-trip.
PgBinaryTailTextTests: kept the UTF-8 cases as-is (now passingEncoding.UTF8explicitly), added WIN1252 andEUC_JP cases for both
DecodeWholeandUnescapeAndDecode.PgReadBinaryFileCapabilityTests: replaced the "NULL for non-UTF8" pins with granted+mapped / granted+unmapped/ ungranted-regardless-of-encoding cases; added
TryGetCachedEncodingcoverage; fixed the probe-shape assertion(no
CASE, still asks for both the encoding and the privilege).PgReadBinaryFileAdvisoryTestsandPostgresFaultOutcomeTests: updated to the new probe scalar shape(
"<encoding>:<bool>") and the no-issue-number sentence.PgReadBinaryFileCapabilityLiveTestsper the brief) was NOT added. I ran out of the30-minute budget before writing it — see "Not verified" below. The unit-level coverage above exercises the same
paths (
PgServerEncoding.TryGet, the probe scalar parsing, the cache) without a live database.One correction to the brief's own test list
PgServerEncodingTests's WIN1252-0x81 case does not assert a U+FFFD outcome, unlike the brief's phrasing.Checked against a throwaway .NET program: .NET's Windows-1252 code page maps byte 0x81 to U+0081 (a reserved-but-mapped
C1 control point), not to the replacement character — there is no byte value in a single-byte .NET code page that
lacks some mapping, so
DecoderReplacementFallbackis never actually invoked for a single stray byte in WIN1252.The test instead asserts the byte decodes without throwing and produces exactly one character, which is the
property #4062 actually needs (a captured log line must never fail the whole read over one byte). Both new tests
(
PgServerEncodingTests.Win1252UnassignedByte_DecodesWithoutThrowing,PgBinaryTailTextTests.DecodeWhole_Win1252UnassignedByte_DecodesWithoutThrowing) are named and commented to say so.Known limitation (per the brief — not fixed here)
One server log file mixes lines from backends in different databases, each in its own database's encoding.
Decoding with the connected database's encoding can still mis-decode (replace, never fail) lines from a database
with a different encoding. The text route has the same property. Documented in
Darling/README.md.What I ran and what it showed
dotnet buildonPerformanceMonitor.Collectors/PerformanceMonitor.Collectors.csprojalone: clean, no warnings(confirmed
System.Text.Encoding.CodePagesneeds no explicitPackageReferenceunder net10.0 — the SDK alreadysupplies it; NU1510 fired when I added one, so I removed it and the Directory.Packages.props entry).
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -p:EnableWindowsTargeting=true: builds the wholesolution clean, 0 errors, 0 warnings.
PgServerEncodingTests,PgBinaryTailTextTests,PgReadBinaryFileCapabilityTests,PgReadBinaryFileAdvisoryTests,PostgresFaultOutcomeTests,PgServerLogTailBinaryRouteTests,PgLogEventsPipelineTests,PgPlanCaptureLiveTests): 234 total, 0 failed,2 skipped (live, no
DARLING_TEST_PG).Microsoft.WindowsDesktop.Appstripped from the test runtimeconfig per_COMMON.md:Total: 13356, Errors: 0, Failed: 217, Skipped: 714. dev's known baseline on this Mac is 214 Windows-onlyfailures; the extra 3 above baseline were
PgReadBinaryFileAdvisoryTeststests using the OLDboolscalarshape against the NEW probe's
"encoding:bool"string scalar — fixed in this branch, then reconfirmed 0 failedon that class alone (7/7 passed). I did not re-run the full 13356-test suite a second time after that fix (time
budget); the class-level rerun is the verification for that specific change, and every other file I touched was
already covered by the full run before the fix.
PgReadBinaryFileCapabilityLiveTestsextension the brief asked for (creating aWIN1252-encoded database, connecting, and reading a planted 0x81 byte through
pg_read_binary_file) was notwritten — ran out of time before landing it. The lane's docker container (
lane4062-pg, port 55890) was neverstarted, so no live proof against a real server ran. This is the one item I'd flag as a fast follow if the
coordinator wants it in-lane rather than as a separate ticket; it's a genuine gap against the brief's own test
list, not a design question.
scrubcheckongit diff origin/dev...HEAD: 0 hits, 17/17 controls fired.Coordinator verification (branch head after 1929d37)
PgReadBinaryFileCapabilityLiveTests.IsGrantedAsync_OnAWin1252Database_GrantsTheRoute_AndCachesItsEncoding: on a WIN1252 database, the probe grants the binary route and caches code page 1252, andpg_read_binary_filethen decodes. Before this PR the probe's CASE gate answered NULL there. The lane didn't write this test; the coordinator did.PgPlanCaptureGuardedCastLiveTests3/3 andPgDeadlockCandidatePatternLiveTests2/2.PgServerEncodingTests40,PgBinaryTailTextTests15,PgReadBinaryFileCapabilityTests31,PgReadBinaryFileAdvisoryTests7,PostgresFaultOutcomeTests90,PgLogEventsPipelineTests49,PgServerLogTailBinaryRouteTests7. All pass.DocCommentHygieneTests×2:DecoderReplacementFallbacknow resolves, so its entry leaves the unresolved-cref inventory.LiveCleanupConversionRatchetTests: the new live test now tears down throughLiveStoreCleanup.CHANGELOG entry
pg_read_binary_filelog-tail route now decodes bytes in the connected database's ownserver_encoding(every LATIN/ISO_8859 variant, WIN866/874/1250–1258, KOI8R/KOI8U, EUC_JP/EUC_CN/EUC_KR, plusUTF8/SQL_ASCII) instead of hard-coded UTF-8, so a database in one of those encodings no longer needs to fall
back to the text route's byte-blinding failure mode. (Postgres log readers: decode the byte route in the database's own encoding, so EUC and WIN1252 targets are not blindable #4062)
Round-1 review fixes
Review: #4106 (comment)
PerformanceMonitor.Collectors/PgBinaryTailText.cs:DecodeWholeandUnescapeAndDecodenow split the raw bytes on 0x0A before decoding, decode each segment with the targetencoding, and join with
'\n'. This is safe for every mapped encoding — 0x0A is never a trail byte in EUC-JP, EUC-KR or GBK,and single-byte encodings are unaffected — so valid text decodes to exactly the same string as before. Commit:
4062 review: split on 0x0A before decoding so a planted EUC/DBCS lead byte cannot eat the newline that follows it.New tests:
DecodeWhole_PlantedEucLeadByteBeforeNewline_DoesNotSwallowTheLineBreakandUnescapeAndDecode_PlantedEucLeadByteBeforeNewline_DoesNotSwallowTheLineBreak, both[Theory]over EUC_JP/EUC_CN/EUC_KRwith
0xA4 0x0A 'x'(and the escape()-reversed form\244\nx) — the result contains'\n'and'x'survives on the next line.?, not U+FFFD.PerformanceMonitor.Collectors/PgServerEncoding.csnow builds every mappedcode page with
new DecoderReplacementFallback("\uFFFD")instead of the defaultDecoderFallback.ReplacementFallback(whose string is a plain
"?"). UTF8/SQL_ASCII stayedUTF8Encoding(false, false), which already yields U+FFFD. Commit:4062 review: replacement char is U+FFFD, not '?', and the EUC map is approximate, not exact. Fixed a dangling cref this leftin
PgServerEncodingTests.cs(the removedDecoderFallback.ReplacementFallbacksymbol was still cref'd in a test comment) —caught by
DocCommentHygieneTests, now green again.PgServerEncoding.cs's header comment: it no longer claims every mapped charset isexact.
EUC_CN → 936is GBK (a GB2312 superset: an unmapped lead byte can pair with an ASCII byte in 0x40–0x7E and swallowit as one CJK character);
EUC_JP → 51932does not decode 3-byte JIS X 0212 sequences;EUC_KR → 51949goes through the sameDBCS table path and is approximate for the same reason. None of the three throws. The other mapped entries (LATIN1–4/5/7/9,
ISO_8859_5..8, WIN866/874/125x, KOI8R/KOI8U) are called out as exact. The map itself is unchanged. Same commit as item 2.
Darling/PerformanceMonitor.Darling.Service/DarlingWorker.cs~9146: "which thisdatabase's encoding is not one this collector maps" → "but this collector does not map this database's encoding", per the
review's suggested wording. No issue refs in served text.
PostgresFaultOutcomeTestsdid not pin the old wording; ran theclass after the change — still 90/90 green. Commit:
4062 review: reword the garbled served sentence about an unmapped encoding.PgBinaryTailTextTests(21/21, up from 15 — this lane's 6 new cases),PgServerEncodingTests(40/40),PgReadBinaryFileCapabilityTests(31/31),PostgresFaultOutcomeTests(90/90),DocCommentHygieneTests(77/77). Then thefull Darling suite once (on this Mac,
dotnet Darling.Tests.dllwithMicrosoft.WindowsDesktop.Appstripped fromruntimeconfig, restored after): 13363 total, 214 failed, 715 skipped — 214 is exactly dev's stated Windows-only baseline
for this rig; no failure is in a class this lane touched or added.
databases with different encodings could share a stale encoding for up to an hour. Wrong text, not an exception.
escape()'s cost: legitimate non-ASCII text on EUC/WIN/LATIN databases now expands up to 4× every cycle throughencode(…,'escape'), not only when bytes are planted. Still bounded (~16 MB) and under the Postgres log readers: the planted-input paths the #4051 review left open #4058 item 1 guard.Granted = null, read as "unsupported encoding" — harmless today, worth acomment rather than a behavior change.
PR stays un-armed (draft/ready state untouched); the coordinator readies it.