Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.59% covered (success)
91.59%
98 / 107
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
PHP
91.59% covered (success)
91.59%
98 / 107
70.00% covered (warning)
70.00%
7 / 10
39.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 close
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 read
81.48% covered (warning)
81.48%
22 / 27
0.00% covered (danger)
0.00%
0 / 1
9.51
 readInt31
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
 readInt32
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 find
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
8.02
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 firstkey
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 nextkey
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 */
18
19namespace Cdb\Reader;
20
21use Cdb\Exception;
22use Cdb\Reader;
23use Cdb\Util;
24
25/**
26 * CDB reader class
27 *
28 * This is a port of D.J. Bernstein's CDB to PHP. It's based on the copy that
29 * appears in PHP 5.3.
30 */
31class PHP extends Reader {
32    /**
33     * The file name of the CDB file.
34     * @var string
35     */
36    protected $fileName;
37
38    /**
39     * The file handle
40     */
41    protected $handle;
42
43    /**
44     * @var string
45     * First 2048 bytes of CDB file, containing pointers to hash table.
46     */
47    protected $index;
48
49    /**
50     * Offset in file where value of found key starts.
51     * @var int
52     */
53    protected $dataPos;
54
55    /**
56     * Byte length of found key's value.
57     * @var int
58     */
59    protected $dataLen;
60
61    /**
62     * File position indicator when iterating over keys.
63     * @var int
64     */
65    protected $keyIterPos = 2048;
66
67    /**
68     * Offset in file where hash tables start.
69     * @var int
70     */
71    protected $keyIterStop;
72
73    /**
74     * Read buffer for CDB file.
75     * @var string
76     */
77    protected $buf;
78
79    /**
80     * File offset where read buffer starts.
81     * @var int
82     */
83    protected $bufStart;
84
85    /**
86     * File handle position indicator.
87     * @var int
88     */
89    protected $filePos = 2048;
90
91    /**
92     * @param string $fileName
93     * @throws Exception If CDB file cannot be opened or if it contains fewer
94     *   than 2048 bytes of data.
95     */
96    public function __construct( string $fileName ) {
97        $this->fileName = $fileName;
98        $this->handle = fopen( $fileName, 'rb' );
99        if ( !$this->handle ) {
100            throw new Exception( 'Unable to open CDB file "' . $this->fileName . '".' );
101        }
102        $this->index = fread( $this->handle, 2048 );
103        if ( strlen( $this->index ) !== 2048 ) {
104            throw new Exception( 'CDB file contains fewer than 2048 bytes of data.' );
105        }
106    }
107
108    /**
109     * Close the handle on the CDB file.
110     */
111    public function close(): void {
112        if ( $this->handle ) {
113            fclose( $this->handle );
114        }
115        $this->handle = null;
116    }
117
118    /**
119     * Get the value of a key.
120     *
121     * @param string|int $key
122     * @return string|false The key's value or false if not found
123     */
124    public function get( $key ) {
125        if ( $this->find( (string)$key ) ) {
126            return $this->read( $this->dataPos, $this->dataLen );
127        }
128
129        return false;
130    }
131
132    /**
133     * Read data from the CDB file.
134     *
135     * @param int $start Start reading from this position
136     * @param int $len Number of bytes to read
137     * @return string Read data.
138     */
139    protected function read( $start, $len ) {
140        $end = $start + $len;
141
142        // The first 2048 bytes are the lookup table, which is read into
143        // memory on initialization.
144        if ( $end <= 2048 ) {
145            return substr( $this->index, $start, $len );
146        }
147
148        // Read data from the internal buffer first.
149        $bytes = '';
150        if ( $this->buf && $start >= $this->bufStart ) {
151            $bytes .= substr( $this->buf, $start - $this->bufStart, $len );
152            $bytesRead = strlen( $bytes );
153            $len -= $bytesRead;
154            $start += $bytesRead;
155        } else {
156            $bytesRead = 0;
157        }
158
159        if ( !$len ) {
160            return $bytes;
161        }
162
163        // Many reads are sequential, so the file position indicator may
164        // already be in the right place, in which case we can avoid the
165        // call to fseek().
166        if ( $start !== $this->filePos ) {
167            if ( fseek( $this->handle, $start ) === -1 ) {
168                // This can easily happen if the internal pointers are incorrect
169                throw new Exception(
170                    'Seek failed, file "' . $this->fileName . '" may be corrupted.' );
171            }
172        }
173
174        $buf = fread( $this->handle, max( $len, 1024 ) );
175        if ( $buf === false ) {
176            $buf = '';
177        }
178
179        $bytes .= substr( $buf, 0, $len );
180        if ( strlen( $bytes ) !== $len + $bytesRead ) {
181            throw new Exception(
182                'Read from CDB file failed, file "' . $this->fileName . '" may be corrupted.' );
183        }
184
185        $this->filePos = $end;
186        $this->bufStart = $start;
187        $this->buf = $buf;
188
189        return $bytes;
190    }
191
192    /**
193     * Unpack an unsigned integer and throw an exception if it needs more than 31 bits.
194     *
195     * @param int $pos Position to read from.
196     * @throws Exception When the integer cannot be represented in 31 bits.
197     * @return int
198     */
199    protected function readInt31( $pos = 0 ) {
200        $uint31 = $this->readInt32( $pos );
201        if ( $uint31 > 0x7fffffff ) {
202            throw new Exception(
203                'Error in CDB file "' . $this->fileName . '", integer too big.' );
204        }
205
206        return $uint31;
207    }
208
209    /**
210     * Unpack a 32-bit integer.
211     *
212     * @param int $pos
213     * @return int
214     */
215    protected function readInt32( $pos = 0 ) {
216        static $lookups;
217
218        if ( !$lookups ) {
219            $lookups = [];
220            for ( $i = 1; $i < 256; $i++ ) {
221                $lookups[ chr( $i ) ] = $i;
222            }
223        }
224
225        $buf = $this->read( $pos, 4 );
226
227        $rv = 0;
228
229        if ( $buf[0] !== "\x0" ) {
230            $rv = $lookups[ $buf[0] ];
231        }
232        if ( $buf[1] !== "\x0" ) {
233            $rv |= ( $lookups[ $buf[1] ] << 8 );
234        }
235        if ( $buf[2] !== "\x0" ) {
236            $rv |= ( $lookups[ $buf[2] ] << 16 );
237        }
238        if ( $buf[3] !== "\x0" ) {
239            $rv |= ( $lookups[ $buf[3] ] << 24 );
240        }
241
242        return $rv;
243    }
244
245    /**
246     * Search the CDB file for a key.
247     *
248     * Sets `dataLen` and `dataPos` properties if successful.
249     *
250     * @param string $key
251     * @return bool Whether the key was found.
252     */
253    protected function find( $key ) {
254        $keyLen = strlen( $key );
255
256        $u = Util::hash( $key );
257        $upos = ( $u << 3 ) & 2047;
258        $hashSlots = $this->readInt31( $upos + 4 );
259        if ( !$hashSlots ) {
260            return false;
261        }
262        $hashPos = $this->readInt31( $upos );
263        $keyHash = $u;
264        $u = Util::unsignedShiftRight( $u, 8 );
265        $u = Util::unsignedMod( $u, $hashSlots );
266        $u <<= 3;
267        $keyPos = $hashPos + $u;
268
269        for ( $i = 0; $i < $hashSlots; $i++ ) {
270            $hash = $this->readInt32( $keyPos );
271            $pos = $this->readInt31( $keyPos + 4 );
272            if ( !$pos ) {
273                return false;
274            }
275            $keyPos += 8;
276            if ( $keyPos == $hashPos + ( $hashSlots << 3 ) ) {
277                $keyPos = $hashPos;
278            }
279            if ( $hash === $keyHash ) {
280                if ( $keyLen === $this->readInt31( $pos ) ) {
281                    $dataLen = $this->readInt31( $pos + 4 );
282                    $dataPos = $pos + 8 + $keyLen;
283                    $foundKey = $this->read( $pos + 8, $keyLen );
284                    if ( $foundKey === $key ) {
285                        // Found
286                        $this->dataLen = $dataLen;
287                        $this->dataPos = $dataPos;
288
289                        return true;
290                    }
291                }
292            }
293        }
294
295        return false;
296    }
297
298    /**
299     * Check if a key exists in the CDB file.
300     *
301     * @param string|int $key
302     * @return bool Whether the key exists.
303     */
304    public function exists( $key ): bool {
305        return $this->find( (string)$key );
306    }
307
308    /**
309     * Get the first key from the CDB file and reset the key iterator.
310     *
311     * @return string|bool Key, or false if no keys in file.
312     */
313    public function firstkey() {
314        $this->keyIterPos = 4;
315
316        if ( !$this->keyIterStop ) {
317            $pos = INF;
318            for ( $i = 0; $i < 2048; $i += 8 ) {
319                $pos = min( $this->readInt31( $i ), $pos );
320            }
321            $this->keyIterStop = $pos;
322        }
323
324        $this->keyIterPos = 2048;
325        return $this->nextkey();
326    }
327
328    /**
329     * Get the next key from the CDB file.
330     *
331     * @return string|bool Key, or false if no more keys.
332     */
333    public function nextkey() {
334        if ( $this->keyIterPos >= $this->keyIterStop ) {
335            return false;
336        }
337        $keyLen = $this->readInt31( $this->keyIterPos );
338        $dataLen = $this->readInt31( $this->keyIterPos + 4 );
339        $key = $this->read( $this->keyIterPos + 8, $keyLen );
340        $this->keyIterPos += 8 + $keyLen + $dataLen;
341
342        return $key;
343    }
344}