1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4/**
5 * File::Gettext
6 *
7 * PHP versions 4 and 5
8 *
9 * @category  FileFormats
10 * @package   File_Gettext
11 * @author    Michael Wallner <mike@php.net>
12 * @copyright 2004-2005 Michael Wallner
13 * @license   BSD, revised
14 * @version   CVS: $Id$
15 * @link      http://pear.php.net/package/File_Gettext
16 */
17
18/**
19 * Requires File_Gettext
20 */
21require_once 'File/Gettext.php';
22
23/**
24 * File_Gettext_MO
25 *
26 * GNU MO file reader and writer.
27 *
28 * @category  FileFormats
29 * @package   File_Gettext
30 * @author    Michael Wallner <mike@php.net>
31 * @copyright 2004-2005 Michael Wallner
32 * @license   BSD, revised
33 * @link      http://pear.php.net/package/File_Gettext
34 */
35class File_Gettext_MO extends File_Gettext
36{
37    /**
38     * file handle
39     *
40     * @access  private
41     * @var     resource
42     */
43    var $_handle = null;
44
45    /**
46     * big endianess
47     *
48     * Whether to write with big endian byte order.
49     *
50     * @access  public
51     * @var     bool
52     */
53    var $writeBigEndian = false;
54
55    /**
56     * Constructor
57     *
58     * @param string $file path to GNU MO file
59     *
60     * @access  public
61     * @return  object      File_Gettext_MO
62     */
63    function File_Gettext_MO($file = '')
64    {
65        $this->file = $file;
66    }
67
68    /**
69     * _read
70     *
71     * @param int $bytes Bytes to read
72     *
73     * @access  private
74     * @return  mixed
75     */
76    function _read($bytes = 1)
77    {
78        if (0 < $bytes = abs($bytes)) {
79            return fread($this->_handle, $bytes);
80        }
81        return null;
82    }
83
84    /**
85     * _readInt
86     *
87     * @param bool $bigendian Is the data an unsigned long?
88     *
89     * @see     http://au.php.net/manual/en/function.pack.php
90     * @access  private
91     * @return  int
92     */
93    function _readInt($bigendian = false)
94    {
95        return current($array = unpack($bigendian ? 'N' : 'V', $this->_read(4)));
96    }
97
98    /**
99     * _writeInt
100     *
101     * @param int $int Int to write
102     *
103     * @access  private
104     * @return  int
105     */
106    function _writeInt($int)
107    {
108        return $this->_write(pack($this->writeBigEndian ? 'N' : 'V', (int) $int));
109    }
110
111    /**
112     * _write
113     *
114     * @param string $data Data to write to file
115     *
116     * @access  private
117     * @return  int
118     */
119    function _write($data)
120    {
121        return fwrite($this->_handle, $data);
122    }
123
124    /**
125     * _writeStr
126     *
127     * @param string $string String to write
128     *
129     * @access  private
130     * @return  int
131     */
132    function _writeStr($string)
133    {
134        return $this->_write($string . "\0");
135    }
136
137    /**
138     * Reads a series of one or more null-terminated strings from a given
139     * location in the source file. Note that MO files optimize plural pairs
140     * by storing them together under the same index entry.
141     *
142     * @param array $params associative array with offset and length
143     *                              of the string
144     *
145     * @access  private
146     * @return  string
147     */
148    function _readStrings($params)
149    {
150        fseek($this->_handle, $params['offset']);
151        $strings = $this->_read($params['length']);
152        return explode("\x00", $strings);
153    }
154
155    /**
156     * Load MO file
157     *
158     * @param string $file File path to load
159     *
160     * @access   public
161     * @return   mixed   Returns true on success or PEAR_Error on failure.
162     */
163    function load($file = null)
164    {
165        $this->strings = array();
166
167        if (!isset($file)) {
168            $file = $this->file;
169        }
170
171        // open MO file
172        if (!is_resource($this->_handle = @fopen($file, 'rb'))) {
173            return parent::raiseError($php_errormsg . ' ' . $file);
174        }
175        // lock MO file shared
176        if (!@flock($this->_handle, LOCK_SH)) {
177            @fclose($this->_handle);
178            return parent::raiseError($php_errormsg . ' ' . $file);
179        }
180
181        // read (part of) magic number from MO file header and define endianess
182        switch ($magic = current($array = unpack('c', $this->_read(4))))
183        {
184        case -34:
185            $be = false;
186            break;
187
188        case -107:
189            $be = true;
190            break;
191
192        default:
193            return parent::raiseError("No GNU mo file: $file (magic: $magic)");
194        }
195
196        // check file format revision - we currently only support 0
197        if (0 !== ($_rev = $this->_readInt($be))) {
198            return parent::raiseError('Invalid file format revision: ' . $_rev);
199        }
200
201        // count of strings in this file
202        $count = $this->_readInt($be);
203
204        // offset of hashing table of the msgids
205        $offset_original = $this->_readInt($be);
206        // offset of hashing table of the msgstrs
207        $offset_translat = $this->_readInt($be);
208
209        // move to msgid hash table
210        fseek($this->_handle, $offset_original);
211        // read lengths and offsets of msgids
212        $original = array();
213        for ($i = 0; $i < $count; $i++) {
214            $original[$i] = array(
215                'length' => $this->_readInt($be),
216                'offset' => $this->_readInt($be)
217            );
218        }
219
220        // move to msgstr hash table
221        fseek($this->_handle, $offset_translat);
222        // read lengths and offsets of msgstrs
223        $translat = array();
224        for ($i = 0; $i < $count; $i++) {
225            $translat[$i] = array(
226                'length' => $this->_readInt($be),
227                'offset' => $this->_readInt($be)
228            );
229        }
230
231        // read all
232        for ($i = 0; $i < $count; $i++) {
233            $pairs = array_combine(
234                $this->_readStrings($original[$i]),
235                $this->_readStrings($translat[$i]));
236            foreach ($pairs as $origStr => $translatedStr) {
237                $this->strings[$origStr] = $translatedStr;
238            }
239        }
240
241        // done
242        @flock($this->_handle, LOCK_UN);
243        @fclose($this->_handle);
244        $this->_handle = null;
245
246        // check for meta info
247        if (isset($this->strings[''])) {
248            $this->meta = parent::meta2array($this->strings['']);
249            unset($this->strings['']);
250        }
251
252        return true;
253    }
254
255    /**
256     * Implements a string hashing function
257     *
258     * @param string $str_param String to hash
259     *
260     * @access   private
261     * @return   int
262     */
263    private function _hashpjw($str_param)
264    {
265        $hval = 0;
266        for ($i = 0; $i<strlen($str_param); $i++) {
267            $hval <<= 4;
268            $hval += ord($str_param[$i]);
269            $g     = $hval & 0xf << 28; // $HASHWORDBITS - 4
270            if ($g != 0) {
271                $hval ^= $g >> 24; // $HASHWORDBITS - 8
272                $hval ^= $g;
273            }
274        }
275        return $hval;
276    }
277
278    /**
279     * Given an odd CANDIDATE > 1, return true if it is a prime number
280     *
281     * @param int $candidate number to check
282     *
283     * @access   private
284     * @return   int
285     */
286    private function _isPrime($candidate)
287    {
288        /* No even number and none less than 10 will be passed here.  */
289        $divn = 3;
290        $sq   = $divn * $divn;
291
292        while ($sq < $candidate && $candidate % $divn != 0) {
293            ++$divn;
294            $sq += 4 * $divn;
295            ++$divn;
296        }
297
298        return $candidate % $divn != 0;
299    }
300
301    /**
302     * Given SEED > 1, return the smallest odd prime number >= SEED
303     *
304     * @param int $seed next prime number
305     *
306     * @access   private
307     * @return   int
308     */
309    private function _nextPrime($seed)
310    {
311        /* Make it definitely odd.  */
312        $seed |= 1;
313
314        while (!self::_isPrime($seed)) {
315            $seed += 2;
316        }
317
318        return $seed;
319    }
320
321    /**
322     * Save MO file
323     *
324     * @param string $file File path to write to
325     *
326     * @access  public
327     * @return  mixed   Returns true on success or PEAR_Error on failure.
328     */
329    function save($file = null)
330    {
331        if (!isset($file)) {
332            $file = $this->file;
333        }
334
335        $tmpfile = $file . "." . getmypid();
336
337        // open MO file
338        if (!is_resource($this->_handle = @fopen($tmpfile, 'wb'))) {
339            return parent::raiseError($php_errormsg . ' ' . $file);
340        }
341        // lock MO file exclusively
342        if (!@flock($this->_handle, LOCK_EX)) {
343            @fclose($this->_handle);
344            return parent::raiseError($php_errormsg . ' ' . $file);
345        }
346
347        // write magic number
348        if ($this->writeBigEndian) {
349            $this->_write(pack('c*', 0x95, 0x04, 0x12, 0xde));
350        } else {
351            $this->_write(pack('c*', 0xde, 0x12, 0x04, 0x95));
352        }
353
354        // write file format revision
355        $this->_writeInt(0);
356
357        $count = count($this->strings) + ($meta = (count($this->meta) ? 1 : 0));
358        // write count of strings
359        $this->_writeInt($count);
360
361        $hash_tab_size = self::_nextPrime(($count * 4) / 3);
362        /* Ensure M > 2.  */
363        if ($hash_tab_size <= 2) {
364            $hash_tab_size = 3;
365        }
366
367        $offset = 28;
368        // write offset of orig. strings hash table
369        $this->_writeInt($offset);
370
371        $offset += ($count * 8);
372        // write offset transl. strings hash table
373        $this->_writeInt($offset);
374
375        // write size of hash table (we currently ommit the hash table)
376        $this->_writeInt($hash_tab_size); // orig: 0
377
378        $offset += ($count * 8);
379        // write offset of hash table
380        $this->_writeInt($offset);
381
382        $offset += ($hash_tab_size * 4);
383
384        // unshift meta info
385        if ($meta) {
386            $meta = '';
387            foreach ($this->meta as $key => $val) {
388                $meta .= $key . ': ' . $val . "\n";
389            }
390            $strings = array('' => $meta) + $this->strings;
391        } else {
392            $strings = $this->strings;
393        }
394
395        $hash_tab = array();
396        $j        = 0;
397        foreach ($strings as $key => $value) {
398            $hash_val = self::_hashpjw($key);
399            $idx      = $hash_val % $hash_tab_size;
400            if (!empty($hash_tab[$idx])) {
401                $incr = 1 + ($hash_val % ($hash_tab_size - 2));
402                do {
403                    if ($idx >= $hash_tab_size - $incr) {
404                        $idx -= $hash_tab_size - $incr;
405                    } else {
406                        $idx += $incr;
407                    }
408                } while (!empty($hash_tab[$idx]));
409            }
410
411            $hash_tab[$idx] = $j + 1;
412            $j++;
413        }
414
415        // write offsets for original strings
416        foreach (array_keys($strings) as $o) {
417            $len = strlen($o);
418            $this->_writeInt($len);
419            $this->_writeInt($offset);
420            $offset += $len + 1;
421        }
422
423        // write offsets for translated strings
424        foreach ($strings as $t) {
425            $len = strlen($t);
426            $this->_writeInt($len);
427            $this->_writeInt($offset);
428            $offset += $len + 1;
429        }
430
431        for ($j = 0; $j < $hash_tab_size; $j++) {
432            if (empty($hash_tab[$j])) {
433                $this->_writeInt(0);
434            } else {
435                $this->_writeInt($hash_tab[$j]);
436            }
437        }
438
439        // write original strings
440        foreach (array_keys($strings) as $o) {
441            $this->_writeStr($o);
442        }
443
444        // write translated strings
445        foreach ($strings as $t) {
446            $this->_writeStr($t);
447        }
448
449        // done
450        @flock($this->_handle, LOCK_UN);
451        @fclose($this->_handle);
452
453        @rename($tmpfile, $file);
454        @unlink($tmpfile);
455        return true;
456    }
457}
458?>
459