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