From 44b6b990472406a08925a0f5faec0cae56a06037 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Fri, 1 May 2026 14:43:14 +0000 Subject: [PATCH] feat: add player name redaction pass Adds three lexical-context regexes (after-SteamID, ChatMessage author, Combat/Safety pvp subsystem) and wires the player-name branch in redact(). Includes six PHPUnit tests covering all three contexts plus the toggle-off and no-anchor-no-touch cases. Co-Authored-By: Claude Sonnet 4.6 --- .../ProjectZomboid/ProjectZomboidRedactor.php | 16 +++- .../ProjectZomboidRedactorPlayerNameTest.php | 90 +++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 test/tests/Util/Redactor/ProjectZomboidRedactorPlayerNameTest.php diff --git a/src/Util/ProjectZomboid/ProjectZomboidRedactor.php b/src/Util/ProjectZomboid/ProjectZomboidRedactor.php index b0dd91c..7e270b0 100644 --- a/src/Util/ProjectZomboid/ProjectZomboidRedactor.php +++ b/src/Util/ProjectZomboid/ProjectZomboidRedactor.php @@ -30,6 +30,18 @@ class ProjectZomboidRedactor implements RedactorInterface /** Zeroed-out SteamID64 placeholder; syntactically valid but refers to no real account. */ public const string STEAM_ID_REPLACEMENT = '76561198000000000'; + /** Generic placeholder substituted for every matched player display name. */ + public const string PLAYER_NAME_REPLACEMENT = ''; + + /** Matches a double-quoted player name that immediately follows the redacted Steam ID placeholder (cmd.txt / admin.txt shape); relies on the Steam ID pass having run first. */ + public const string PLAYER_AFTER_STEAMID_REGEX = '/(?<=76561198000000000) "(?[^"]+)"/u'; + + /** Matches the author value inside a ChatMessage{...} envelope, using a fixed-length lookbehind on ", author='" and a lookahead on the closing "'" so only the bare name is replaced. */ + public const string PLAYER_IN_CHATMESSAGE_REGEX = '/(?<=, author=\')(?[^\']+)(?=\')/u'; + + /** Matches the first double-quoted player name following a Combat: or Safety: subsystem token (pvp.txt shape); does NOT redact the second name after "hit" — deferred to v2. */ + public const string PLAYER_IN_PVP_SUBSYSTEM_REGEX = '/(?<=(?:Combat|Safety): )"(?[^"]+)"/u'; + private bool $redactSteamIds = true; private bool $redactPlayerNames = true; private bool $redactCoordinates = true; @@ -85,7 +97,9 @@ class ProjectZomboidRedactor implements RedactorInterface $content = preg_replace(self::STEAM_ID_REGEX, self::STEAM_ID_REPLACEMENT, $content); } if ($this->redactPlayerNames) { - // Player name pass added in Task 3 + $content = preg_replace(self::PLAYER_AFTER_STEAMID_REGEX, ' "' . self::PLAYER_NAME_REPLACEMENT . '"', $content); + $content = preg_replace(self::PLAYER_IN_CHATMESSAGE_REGEX, self::PLAYER_NAME_REPLACEMENT, $content); + $content = preg_replace(self::PLAYER_IN_PVP_SUBSYSTEM_REGEX, '"' . self::PLAYER_NAME_REPLACEMENT . '"', $content); } if ($this->redactCoordinates) { // Coordinates pass added in Task 4 diff --git a/test/tests/Util/Redactor/ProjectZomboidRedactorPlayerNameTest.php b/test/tests/Util/Redactor/ProjectZomboidRedactorPlayerNameTest.php new file mode 100644 index 0000000..9463c69 --- /dev/null +++ b/test/tests/Util/Redactor/ProjectZomboidRedactorPlayerNameTest.php @@ -0,0 +1,90 @@ +" admin.broadcastMessage @ 1020,2020,0.'; + + $output = (new ProjectZomboidRedactor()) + ->redactSteamIds(false) + ->redact($input); + + $this->assertSame($expected, $output, 'Player name following the redacted Steam ID placeholder must be replaced.'); + } + + public function testRedactsChatMessageAuthor(): void + { + // The author field inside ChatMessage{...} must be replaced; the text + // payload ('hello') is not in scope for player-name redaction and must + // survive unchanged. + $input = "[16-04-26 17:05:03.280][info] Got message:ChatMessage{chat=Local, author='Player1', text='hello'}."; + $expected = "[16-04-26 17:05:03.280][info] Got message:ChatMessage{chat=Local, author='', text='hello'}."; + + $output = (new ProjectZomboidRedactor()) + ->redactSteamIds(false) + ->redact($input); + + $this->assertSame($expected, $output, 'ChatMessage author must be replaced while the text payload remains unchanged.'); + } + + public function testRedactsCombatNameInPvpLog(): void + { + // Only the FIRST quoted name (after "Combat: ") is redacted in v1. + // The second name (after "hit") is NOT yet redacted — deferred to v2. + // The weapon name ("Tire Iron (Worn)") must also survive unchanged. + $input = '[16-04-26 17:14:35.128][INFO] Combat: "Player1" (1005,2005,0) hit "Player2" (1006,2005,0) weapon="Tire Iron (Worn)" damage=0.112317.'; + $expected = '[16-04-26 17:14:35.128][INFO] Combat: "" (1005,2005,0) hit "Player2" (1006,2005,0) weapon="Tire Iron (Worn)" damage=0.112317.'; + + $output = (new ProjectZomboidRedactor()) + ->redactSteamIds(false) + ->redact($input); + + // Player1 (after "Combat: ") is replaced; Player2 (after "hit") is NOT + // replaced in v1 — that anchor is deferred. + $this->assertSame($expected, $output, 'First Combat: player name must be replaced; second name and weapon must survive.'); + } + + public function testRedactsSafetyNameInPvpLog(): void + { + $input = '[16-04-26 16:17:49.731][LOG] Safety: "Player1" (1000,2000,0) restore true.'; + $expected = '[16-04-26 16:17:49.731][LOG] Safety: "" (1000,2000,0) restore true.'; + + $output = (new ProjectZomboidRedactor()) + ->redactSteamIds(false) + ->redact($input); + + $this->assertSame($expected, $output, 'Player name following the Safety: token must be replaced.'); + } + + public function testBareQuotedStringWithoutAnchorIsNotTouched(): void + { + // "foo" is not preceded by a redacted Steam ID, not inside ChatMessage{...}, + // and not after Combat:/Safety: — it must pass through unchanged. + $input = 'option changed to "foo" successfully.'; + + $output = (new ProjectZomboidRedactor())->redact($input); + + $this->assertSame($input, $output, 'A quoted string with no matching anchor must not be redacted.'); + } + + public function testToggleOffLeavesNamesIntact(): void + { + $input = '76561198000000000 "Player1" ISLogSystem.writeLog @ 1000,2000,0.'; + + $output = (new ProjectZomboidRedactor()) + ->redactSteamIds(false) + ->redactPlayerNames(false) + ->redact($input); + + $this->assertSame($input, $output, 'With the player-name toggle disabled the original input must be returned unchanged.'); + } +}