Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.05% covered (warning)
86.05%
74 / 86
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
JsonFixtureStorage
86.05% covered (warning)
86.05%
74 / 86
60.00% covered (warning)
60.00%
3 / 5
77.83
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 load
84.31% covered (warning)
84.31%
43 / 51
0.00% covered (danger)
0.00%
0 / 1
63.84
 listAll
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
9.65
 buildPath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
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
13namespace Clansuite\Capture\Storage;
14
15use const GLOB_ONLYDIR;
16use const JSON_PRETTY_PRINT;
17use function base64_decode;
18use function base64_encode;
19use function dirname;
20use function file_exists;
21use function file_get_contents;
22use function file_put_contents;
23use function glob;
24use function is_array;
25use function is_bool;
26use function is_dir;
27use function is_int;
28use function is_string;
29use function json_decode;
30use function json_encode;
31use function mkdir;
32use function serialize;
33use function sprintf;
34use function str_replace;
35use function strtolower;
36use function unserialize;
37use Clansuite\Capture\CaptureResult;
38use Clansuite\Capture\ServerInfo;
39use Override;
40
41/**
42 * Stores and retrieves capture results as JSON fixtures for testing and development purposes.
43 */
44final 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}