fix: sanitize legacy mirror text encoding

This commit is contained in:
WuYongJie 2026-08-14 20:44:10 +08:00
parent 3885802877
commit 35f6050ef7
2 changed files with 41 additions and 0 deletions

View File

@ -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) {

View File

@ -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 = [[