1: <?php declare(strict_types=1);
2:
3: /**
4: * Clansuite Server Query
5: *
6: * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
7: * SPDX-License-Identifier: MIT
8: *
9: * For the full copyright and license information, please view
10: * the LICENSE file that was distributed with this source code.
11: */
12:
13: namespace Clansuite\Capture\Storage;
14:
15: use const GLOB_ONLYDIR;
16: use const JSON_PRETTY_PRINT;
17: use function base64_decode;
18: use function base64_encode;
19: use function dirname;
20: use function file_exists;
21: use function file_get_contents;
22: use function file_put_contents;
23: use function glob;
24: use function is_array;
25: use function is_bool;
26: use function is_dir;
27: use function is_int;
28: use function is_string;
29: use function json_decode;
30: use function json_encode;
31: use function mkdir;
32: use function serialize;
33: use function sprintf;
34: use function str_replace;
35: use function strtolower;
36: use function unserialize;
37: use Clansuite\Capture\CaptureResult;
38: use Clansuite\Capture\ServerInfo;
39: use Override;
40:
41: /**
42: * Stores and retrieves capture results as JSON fixtures for testing and development purposes.
43: */
44: final readonly class JsonFixtureStorage implements FixtureStorageInterface
45: {
46: /**
47: * Constructor.
48: */
49: public function __construct(private string $fixturesDir)
50: {
51: }
52:
53: /**
54: * save method.
55: */
56: #[Override]
57: public function save(string $protocol, string $version, string $ip, int $port, CaptureResult $result): string
58: {
59: $path = $this->buildPath($protocol, $version, $ip, $port);
60: $dir = dirname($path);
61:
62: if (!is_dir($dir)) {
63: mkdir($dir, 0o755, true);
64: }
65: $data = [
66: 'metadata' => $result->metadata,
67: 'packets' => base64_encode(serialize($result->rawPackets)), // Assuming rawPackets is array of packets
68: 'server_info' => $result->serverInfo->toArray(),
69: ];
70: file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT));
71:
72: return $path;
73: }
74:
75: /**
76: * load method.
77: */
78: #[Override]
79: public function load(string $protocol, string $version, string $ip, int $port): ?CaptureResult
80: {
81: $path = $this->buildPath($protocol, $version, $ip, $port);
82:
83: if (!file_exists($path)) {
84: return null;
85: }
86:
87: $contents = file_get_contents($path);
88:
89: if ($contents === false) {
90: return null;
91: }
92:
93: $data = json_decode($contents, true);
94:
95: if (!is_array($data)) {
96: return null;
97: }
98:
99: $serverInfoData = $data['server_info'] ?? null;
100:
101: if (!is_array($serverInfoData)) {
102: return null;
103: }
104:
105: $encodedPackets = $data['packets'] ?? null;
106:
107: if (!is_string($encodedPackets)) {
108: return null;
109: }
110:
111: $decodedPackets = base64_decode($encodedPackets, true);
112:
113: if ($decodedPackets === false) {
114: return null;
115: }
116:
117: $rawPackets = @unserialize($decodedPackets);
118:
119: if ($rawPackets === false && $decodedPackets !== serialize(false)) {
120: return null;
121: }
122:
123: $metadata = $data['metadata'] ?? [];
124:
125: if (!is_array($metadata)) {
126: $metadata = [];
127: }
128:
129: $serverInfoData = [
130: 'address' => isset($serverInfoData['address']) && is_string($serverInfoData['address']) ? $serverInfoData['address'] : null,
131: 'queryport' => isset($serverInfoData['queryport']) && is_int($serverInfoData['queryport']) ? $serverInfoData['queryport'] : null,
132: 'online' => isset($serverInfoData['online']) && is_bool($serverInfoData['online']) ? $serverInfoData['online'] : false,
133: 'gamename' => isset($serverInfoData['gamename']) && is_string($serverInfoData['gamename']) ? $serverInfoData['gamename'] : null,
134: 'gameversion' => isset($serverInfoData['gameversion']) && is_string($serverInfoData['gameversion']) ? $serverInfoData['gameversion'] : null,
135: 'servertitle' => isset($serverInfoData['servertitle']) && is_string($serverInfoData['servertitle']) ? $serverInfoData['servertitle'] : null,
136: 'mapname' => isset($serverInfoData['mapname']) && is_string($serverInfoData['mapname']) ? $serverInfoData['mapname'] : null,
137: 'gametype' => isset($serverInfoData['gametype']) && is_string($serverInfoData['gametype']) ? $serverInfoData['gametype'] : null,
138: 'numplayers' => isset($serverInfoData['numplayers']) && is_int($serverInfoData['numplayers']) ? $serverInfoData['numplayers'] : 0,
139: 'maxplayers' => isset($serverInfoData['maxplayers']) && is_int($serverInfoData['maxplayers']) ? $serverInfoData['maxplayers'] : 0,
140: 'rules' => isset($serverInfoData['rules']) && is_array($serverInfoData['rules']) ? $serverInfoData['rules'] : [],
141: 'players' => isset($serverInfoData['players']) && is_array($serverInfoData['players']) ? $serverInfoData['players'] : [],
142: 'channels' => isset($serverInfoData['channels']) && is_array($serverInfoData['channels']) ? $serverInfoData['channels'] : [],
143: 'errstr' => isset($serverInfoData['errstr']) && is_string($serverInfoData['errstr']) ? $serverInfoData['errstr'] : null,
144: 'password' => isset($serverInfoData['password']) && is_bool($serverInfoData['password']) ? $serverInfoData['password'] : null,
145: 'name' => isset($serverInfoData['name']) && is_string($serverInfoData['name']) ? $serverInfoData['name'] : null,
146: 'map' => isset($serverInfoData['map']) && is_string($serverInfoData['map']) ? $serverInfoData['map'] : null,
147: 'players_current' => isset($serverInfoData['players_current']) && is_int($serverInfoData['players_current']) ? $serverInfoData['players_current'] : null,
148: 'players_max' => isset($serverInfoData['players_max']) && is_int($serverInfoData['players_max']) ? $serverInfoData['players_max'] : null,
149: 'version' => isset($serverInfoData['version']) && is_string($serverInfoData['version']) ? $serverInfoData['version'] : null,
150: 'motd' => isset($serverInfoData['motd']) && is_string($serverInfoData['motd']) ? $serverInfoData['motd'] : null,
151: ];
152:
153: $serverInfo = new ServerInfo(...$serverInfoData);
154:
155: if (!is_array($rawPackets)) {
156: $rawPackets = [];
157: }
158:
159: return new CaptureResult($rawPackets, $serverInfo, $metadata);
160: }
161:
162: /**
163: * listAll method.
164: *
165: * @return array<mixed>
166: */
167: #[Override]
168: public function listAll(): array
169: {
170: $captures = [];
171: $protocolDirs = glob($this->fixturesDir . '/*', GLOB_ONLYDIR);
172:
173: if ($protocolDirs === false) {
174: $protocolDirs = [];
175: }
176:
177: foreach ($protocolDirs as $protocolDir) {
178: $versionDirs = glob($protocolDir . '/*', GLOB_ONLYDIR);
179:
180: if ($versionDirs === false) {
181: $versionDirs = [];
182: }
183:
184: foreach ($versionDirs as $versionDir) {
185: $files = glob($versionDir . '/*.json');
186:
187: if ($files === false) {
188: $files = [];
189: }
190:
191: foreach ($files as $file) {
192: $c = file_get_contents($file);
193:
194: if ($c === false) {
195: continue;
196: }
197:
198: $data = json_decode($c, true);
199:
200: if (is_array($data)) {
201: $captures[] = $data;
202: }
203: }
204: }
205: }
206:
207: return $captures;
208: }
209:
210: private function buildPath(string $protocol, string $version, string $ip, int $port): string
211: {
212: $normalizedIp = str_replace('.', '_', $ip);
213: $filename = sprintf('capture_%s_%d.json', $normalizedIp, $port);
214:
215: return $this->fixturesDir . '/' . strtolower($protocol) . '/' . $version . '/' . $filename;
216: }
217: }
218: