Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.40% |
70 / 86 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
PHP | |
81.40% |
70 / 86 |
|
0.00% |
0 / 7 |
34.42 | |
0.00% |
0 / 1 |
__construct | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
3.33 | |||
set | |
82.35% |
14 / 17 |
|
0.00% |
0 / 1 |
4.09 | |||
close | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
5.39 | |||
write | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
posplus | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
2.26 | |||
finish | |
97.50% |
39 / 40 |
|
0.00% |
0 / 1 |
11 | |||
throwException | |
0.00% |
0 / 4 |
|
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 | |
19 | namespace Cdb\Writer; |
20 | |
21 | use Cdb\Exception; |
22 | use Cdb\Util; |
23 | use 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 | */ |
31 | class 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 | } |