Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.40% covered (warning)
81.40%
70 / 86
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
PHP
81.40% covered (warning)
81.40%
70 / 86
0.00% covered (danger)
0.00%
0 / 7
34.42
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
 set
82.35% covered (warning)
82.35%
14 / 17
0.00% covered (danger)
0.00%
0 / 1
4.09
 close
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
 write
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 posplus
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
 finish
97.50% covered (success)
97.50%
39 / 40
0.00% covered (danger)
0.00%
0 / 1
11
 throwException
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
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\Writer;
20
21use Cdb\Exception;
22use Cdb\Util;
23use Cdb\Writer;
24
25/**
26 * CDB writer 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 Writer {
32    /**
33     * @var resource|false|null The file handle
34     */
35    protected $handle;
36
37    /** @var int[][] */
38    protected $hplist = [];
39
40    /** @var int */
41    protected $numentries = 0;
42
43    /** @var int */
44    protected $pos;
45
46    /**
47     * Create the object and open the file.
48     *
49     * @param string $fileName
50     */
51    public function __construct( string $fileName ) {
52        $this->realFileName = $fileName;
53        $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff );
54        $this->handle = fopen( $this->tmpFileName, 'wb' );
55        if ( !$this->handle ) {
56            $this->throwException(
57                'Unable to open CDB file "' . $this->tmpFileName . '" for write.' );
58        }
59        $this->pos = 2048; // leaving space for the pointer array, 256 * 8
60        if ( fseek( $this->handle, $this->pos ) == -1 ) {
61            $this->throwException( 'fseek failed in file "' . $this->tmpFileName . '".' );
62        }
63    }
64
65    /**
66     * @param string $key
67     * @param string $value
68     */
69    public function set( $key, $value ): void {
70        $key = (string)$key;
71        if ( $key === '' ) {
72            // DBA cross-check hack
73            return;
74        }
75
76        // Based on cdb_make_addbegin
77        $keylen = strlen( $key );
78        $datalen = strlen( $value );
79        if ( $keylen > 0x7fffffff ) {
80            $this->throwException( 'Key length too long in file "' . $this->tmpFileName . '".' );
81        }
82        if ( $datalen > 0x7fffffff ) {
83            $this->throwException( 'Data length too long in file "' . $this->tmpFileName . '".' );
84        }
85        $begin = pack( 'VV', $keylen, $datalen );
86
87        $this->write( $begin . $key . $value );
88
89        // Based on cdb_make_addend
90        $this->hplist[] = [
91            'h' => Util::hash( $key ),
92            'p' => $this->pos
93        ];
94        $this->numentries++;
95        $this->posplus( 8 + $keylen + $datalen );
96    }
97
98    public function close(): void {
99        if ( $this->handle ) {
100            $this->finish();
101            fclose( $this->handle );
102
103            if ( $this->isWindows() && file_exists( $this->realFileName ) ) {
104                unlink( $this->realFileName );
105            }
106            if ( !rename( $this->tmpFileName, $this->realFileName ) ) {
107                $this->throwException( 'Unable to move the new CDB file into place.' );
108            }
109        }
110        $this->handle = null;
111    }
112
113    /**
114     * @param string $buf
115     */
116    protected function write( $buf ): void {
117        $len = fwrite( $this->handle, $buf );
118        if ( $len !== strlen( $buf ) ) {
119            $this->throwException( 'Error writing to CDB file "' . $this->tmpFileName . '".' );
120        }
121    }
122
123    /**
124     * @param int $len
125     */
126    protected function posplus( $len ) {
127        $newpos = $this->pos + $len;
128        if ( $newpos > 0x7fffffff ) {
129            $this->throwException(
130                'A value in the CDB file "' . $this->tmpFileName . '" is too large.' );
131        }
132        $this->pos = $newpos;
133    }
134
135    protected function finish(): void {
136        // Hack for DBA cross-check
137        $this->hplist = array_reverse( $this->hplist );
138
139        // Calculate the number of items that will be in each hashtable
140        $counts = array_fill( 0, 256, 0 );
141        foreach ( $this->hplist as $item ) {
142            ++$counts[255 & $item['h']];
143        }
144
145        // Fill in $starts with the *end* indexes
146        $starts = [];
147        $pos = 0;
148        for ( $i = 0; $i < 256; ++$i ) {
149            $pos += $counts[$i];
150            $starts[$i] = $pos;
151        }
152
153        // Excessively clever and indulgent code to simultaneously fill $packedTables
154        // with the packed hashtables, and adjust the elements of $starts
155        // to actually point to the starts instead of the ends.
156        if ( $this->numentries > 0 ) {
157            $packedTables = array_fill( 0, $this->numentries, false );
158        } else {
159            // array_fill(): Number of elements must be positive
160            $packedTables = [];
161        }
162        foreach ( $this->hplist as $item ) {
163            $packedTables[--$starts[255 & $item['h']]] = $item;
164        }
165
166        $final = '';
167        for ( $i = 0; $i < 256; ++$i ) {
168            $count = $counts[$i];
169
170            // The size of the hashtable will be double the item count.
171            // The rest of the slots will be empty.
172            $len = $count + $count;
173            $final .= pack( 'VV', $this->pos, $len );
174
175            $hashtable = array_fill( 0, $len, [ 'h' => 0, 'p' => 0 ] );
176
177            // Fill the hashtable, using the next empty slot if the hashed slot
178            // is taken.
179            for ( $u = 0; $u < $count; ++$u ) {
180                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
181                $hp = $packedTables[$starts[$i] + $u];
182                $where = Util::unsignedMod(
183                    Util::unsignedShiftRight( $hp['h'], 8 ), $len );
184                while ( $hashtable[$where]['p'] ) {
185                    if ( ++$where == $len ) {
186                        $where = 0;
187                    }
188                }
189                $hashtable[$where] = $hp;
190            }
191
192            // Write the hashtable
193            $buf = '';
194            for ( $u = 0; $u < $len; ++$u ) {
195                $buf .= pack( 'vvV',
196                    $hashtable[$u]['h'] & 0xffff,
197                    Util::unsignedShiftRight( $hashtable[$u]['h'], 16 ),
198                    $hashtable[$u]['p'] );
199            }
200            $this->write( $buf );
201            $this->posplus( strlen( $buf ) );
202        }
203
204        // Write the pointer array at the start of the file
205        rewind( $this->handle );
206        if ( ftell( $this->handle ) != 0 ) {
207            $this->throwException( 'Error rewinding to start of file "' . $this->tmpFileName . '".' );
208        }
209        $this->write( $final );
210    }
211
212    /**
213     * Clean up the temp file and throw an exception
214     *
215     * @param string $msg
216     * @return never
217     * @throws Exception
218     */
219    protected function throwException( $msg ) {
220        if ( $this->handle ) {
221            fclose( $this->handle );
222            unlink( $this->tmpFileName );
223        }
224        throw new Exception( $msg );
225    }
226}