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