From 35f6050ef76c2e1f60abd187d00a6afbad9c6b01 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Fri, 14 Aug 2026 20:44:10 +0800 Subject: [PATCH] fix: sanitize legacy mirror text encoding --- .../Services/BusinessMirrorSynchronizer.php | 16 ++++++++++++ .../BusinessMirrorSynchronizerTest.php | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php b/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php index 80802317..c46b391a 100644 --- a/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php +++ b/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php @@ -226,6 +226,8 @@ class BusinessMirrorSynchronizer $row[$name] = substr($value, 0, 10); } elseif (in_array($type, ['datetime', 'datetime2', 'smalldatetime'], true)) { $row[$name] = preg_replace('/(\.\d{6})\d+/', '$1', str_replace('T', ' ', $value)); + } elseif (in_array($type, ['char', 'nchar', 'varchar', 'nvarchar', 'text', 'ntext', 'xml'], true)) { + $row[$name] = $this->normalizeText($value); } } @@ -233,6 +235,20 @@ class BusinessMirrorSynchronizer }, $rows); } + private function normalizeText(string $value): string + { + // A small number of legacy SQL Server rows contain embedded NUL bytes or + // malformed byte sequences. MariaDB correctly rejects those values as + // invalid utf8mb4. Do not guess a source encoding: preserve all valid + // UTF-8 and mark only irrecoverable bytes with the Unicode replacement + // character so one corrupt label cannot block the entire table mirror. + $value = str_replace("\0", '', $value); + + return mb_check_encoding($value, 'UTF-8') + ? $value + : mb_scrub($value, 'UTF-8'); + } + private function quote(string $identifier): string { if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $identifier) !== 1) { diff --git a/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php b/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php index 29925c62..a1205baa 100644 --- a/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php @@ -143,6 +143,31 @@ class BusinessMirrorSynchronizerTest extends TestCase $this->assertSame([], $this->query->calls); } + public function test_full_sync_sanitizes_invalid_utf8_text_without_changing_valid_text(): void + { + $this->query->batches = [[ + [ + 'Id' => 1, + 'updateTime' => '2026-08-14 10:00:00', + 'displayName' => hex2bin('e80044495900'), + ], + [ + 'Id' => 2, + 'updateTime' => '2026-08-14 11:00:00', + 'displayName' => '合法名称', + ], + ]]; + + $this->synchronizer->sync('test.dbo.records', true, 10, 1); + + $rows = DB::connection('business_mirror')->table('test_records')->orderBy('Id')->pluck('displayName')->all(); + $this->assertStringEndsWith('DIY', $rows[0]); + $this->assertNotSame(hex2bin('e80044495900'), $rows[0]); + $this->assertSame('合法名称', $rows[1]); + $this->assertTrue(mb_check_encoding($rows[0], 'UTF-8')); + $this->assertStringNotContainsString("\0", $rows[0]); + } + public function test_empty_full_read_does_not_delete_previous_mirror_rows(): void { $this->query->batches = [[