1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\I18n\Translator\Loader; 11 12use Zend\I18n\Exception; 13use Zend\I18n\Translator\Plural\Rule as PluralRule; 14use Zend\I18n\Translator\TextDomain; 15use Zend\Stdlib\ErrorHandler; 16 17/** 18 * Gettext loader. 19 */ 20class Gettext extends AbstractFileLoader 21{ 22 /** 23 * Current file pointer. 24 * 25 * @var resource 26 */ 27 protected $file; 28 29 /** 30 * Whether the current file is little endian. 31 * 32 * @var bool 33 */ 34 protected $littleEndian; 35 36 /** 37 * load(): defined by FileLoaderInterface. 38 * 39 * @see FileLoaderInterface::load() 40 * @param string $locale 41 * @param string $filename 42 * @return TextDomain 43 * @throws Exception\InvalidArgumentException 44 */ 45 public function load($locale, $filename) 46 { 47 $resolvedFile = $this->resolveFile($filename); 48 if (!$resolvedFile) { 49 throw new Exception\InvalidArgumentException(sprintf( 50 'Could not find or open file %s for reading', 51 $filename 52 )); 53 } 54 55 $textDomain = new TextDomain(); 56 57 ErrorHandler::start(); 58 $this->file = fopen($resolvedFile, 'rb'); 59 $error = ErrorHandler::stop(); 60 if (false === $this->file) { 61 throw new Exception\InvalidArgumentException(sprintf( 62 'Could not open file %s for reading', 63 $filename 64 ), 0, $error); 65 } 66 67 // Verify magic number 68 $magic = fread($this->file, 4); 69 70 if ($magic == "\x95\x04\x12\xde") { 71 $this->littleEndian = false; 72 } elseif ($magic == "\xde\x12\x04\x95") { 73 $this->littleEndian = true; 74 } else { 75 fclose($this->file); 76 throw new Exception\InvalidArgumentException(sprintf( 77 '%s is not a valid gettext file', 78 $filename 79 )); 80 } 81 82 // Verify major revision (only 0 and 1 supported) 83 $majorRevision = ($this->readInteger() >> 16); 84 85 if ($majorRevision !== 0 && $majorRevision !== 1) { 86 fclose($this->file); 87 throw new Exception\InvalidArgumentException(sprintf( 88 '%s has an unknown major revision', 89 $filename 90 )); 91 } 92 93 // Gather main information 94 $numStrings = $this->readInteger(); 95 $originalStringTableOffset = $this->readInteger(); 96 $translationStringTableOffset = $this->readInteger(); 97 98 // Usually there follow size and offset of the hash table, but we have 99 // no need for it, so we skip them. 100 fseek($this->file, $originalStringTableOffset); 101 $originalStringTable = $this->readIntegerList(2 * $numStrings); 102 103 fseek($this->file, $translationStringTableOffset); 104 $translationStringTable = $this->readIntegerList(2 * $numStrings); 105 106 // Read in all translations 107 for ($current = 0; $current < $numStrings; $current++) { 108 $sizeKey = $current * 2 + 1; 109 $offsetKey = $current * 2 + 2; 110 $originalStringSize = $originalStringTable[$sizeKey]; 111 $originalStringOffset = $originalStringTable[$offsetKey]; 112 $translationStringSize = $translationStringTable[$sizeKey]; 113 $translationStringOffset = $translationStringTable[$offsetKey]; 114 115 $originalString = array(''); 116 if ($originalStringSize > 0) { 117 fseek($this->file, $originalStringOffset); 118 $originalString = explode("\0", fread($this->file, $originalStringSize)); 119 } 120 121 if ($translationStringSize > 0) { 122 fseek($this->file, $translationStringOffset); 123 $translationString = explode("\0", fread($this->file, $translationStringSize)); 124 125 if (count($originalString) > 1 && count($translationString) > 1) { 126 $textDomain[$originalString[0]] = $translationString; 127 128 array_shift($originalString); 129 130 foreach ($originalString as $string) { 131 if (! isset($textDomain[$string])) { 132 $textDomain[$string] = ''; 133 } 134 } 135 } else { 136 $textDomain[$originalString[0]] = $translationString[0]; 137 } 138 } 139 } 140 141 // Read header entries 142 if (array_key_exists('', $textDomain)) { 143 $rawHeaders = explode("\n", trim($textDomain[''])); 144 145 foreach ($rawHeaders as $rawHeader) { 146 list($header, $content) = explode(':', $rawHeader, 2); 147 148 if (trim(strtolower($header)) === 'plural-forms') { 149 $textDomain->setPluralRule(PluralRule::fromString($content)); 150 } 151 } 152 153 unset($textDomain['']); 154 } 155 156 fclose($this->file); 157 158 return $textDomain; 159 } 160 161 /** 162 * Read a single integer from the current file. 163 * 164 * @return int 165 */ 166 protected function readInteger() 167 { 168 if ($this->littleEndian) { 169 $result = unpack('Vint', fread($this->file, 4)); 170 } else { 171 $result = unpack('Nint', fread($this->file, 4)); 172 } 173 174 return $result['int']; 175 } 176 177 /** 178 * Read an integer from the current file. 179 * 180 * @param int $num 181 * @return int 182 */ 183 protected function readIntegerList($num) 184 { 185 if ($this->littleEndian) { 186 return unpack('V' . $num, fread($this->file, 4 * $num)); 187 } 188 189 return unpack('N' . $num, fread($this->file, 4 * $num)); 190 } 191} 192