1<?php
2/*
3 * Copyright 2008-2010 GuardTime AS
4 *
5 * This file is part of the GuardTime PHP SDK.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 *     http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19
20/**
21 * @package util
22 */
23
24/**
25 * A generic implementation for RFC 4648 base-X encoders/decoders.
26 *
27 * @link http://tools.ietf.org/html/rfc4648 RFC 4648
28 * @see GTBase16, GTBase32, GTBase64
29 *
30 * @package util
31 */
32class GTBaseX {
33
34    private $values;
35    private $min;
36    private $max;
37    private $bits;
38    private $block;
39    private $pad;
40
41    /**
42     * Constructs an encoder/decoder using the given characters.
43     *
44     * Example for Base16:
45     *
46     * <code>
47     * $base = new BaseX('0123456789ABCDEF', false, ' ');
48     * </code>
49     *
50     * @throws GTException
51     * @param  string  $alphabet the encoding alphabet
52     * @param  boolean $caseSensitive if true both the encoder/decoder will be case sensitive
53     * @param  string  $pad the padding characters used to even out the encoded output
54     *
55     * @see GTBase16, GTBase32, GTBase64
56     */
57    public function __construct($alphabet, $caseSensitive, $pad) {
58
59        $this->bits = 1;
60
61        while ((1 << $this->bits) < strlen($alphabet)) {
62            $this->bits++;
63        }
64
65        if ((1 << $this->bits) != strlen($alphabet)) {
66            throw new GTException("The size of the encoding alphabet is not a power of 2");
67        }
68
69        $this->block = 8 / GTUtil::gcd(8, $this->bits);
70        $this->chars = GTUtil::toArray($alphabet);
71
72        $this->min = -1;
73        $this->max = -1;
74
75        if ($caseSensitive) {
76
77            $this->addMinMax($alphabet);
78
79            $this->values = array();
80
81            for ($i = 0; $i < ($this->max - $this->min) + 1; $i++) {
82                array_push($this->values, -1);
83            }
84
85            $this->addChars($alphabet);
86
87        } else {
88
89            $this->addMinMax(strtoupper($alphabet));
90            $this->addMinMax(strtolower($alphabet));
91
92            $this->values = array();
93
94            for ($i = 0; $i < ($this->max - $this->min) + 1; $i++) {
95                array_push($this->values, -1);
96            }
97
98            $this->addChars(strtolower($alphabet));
99            $this->addChars(strtoupper($alphabet));
100
101        }
102
103        if ($pad >= $this->min && $pad <= $this->max && $this->values[$pad - $this->min] != -1) {
104            throw new GTException("The padding character appears in the encoding alphabet");
105        }
106
107        $this->pad = $pad;
108
109    }
110
111    /**
112     * Encodes the given bytes into a base-X string.
113     *
114     * This method also optionally inserts a separator into the result with given frequency.
115     *
116     * @throws GTException
117     * @param  array $bytes the bytes to encode
118     * @param  int $offset the start offset in bytes array
119     * @param  int $length the number of bytes to encode
120     * @param  string $separator if separator is not null it's inserted into the encoded string at frequency
121     * @param  int $frequency if separator is not null it's inserted into the encoded string at frequency
122     * @return string string containing the encoded data
123     */
124    public function encode($bytes, $offset = null, $length = null, $separator = null, $frequency = null) {
125
126        if (empty($bytes)) {
127            throw new GTException("parameter bytes is required");
128        }
129
130        if (!is_array($bytes)) {
131            throw new GTException("parameter bytes must be an array of bytes");
132        }
133
134        if ($offset == null) {
135            $offset = 0;
136        }
137
138        if ($length == null) {
139            $length = count($bytes);
140        }
141
142        $offset = (int) $offset;
143        $length = (int) $length;
144
145        if ($offset < 0 || $offset > count($bytes)) {
146            throw new GTException("parameter offset out of bounds");
147        }
148
149        if ($length < 0 || $offset + $length > count($bytes)) {
150            throw new GTException("parameter length out of bounds");
151        }
152
153        if ($separator == null) {
154            $frequency = 0;
155
156        } else {
157
158            for ($i = 0; $i < strlen($separator); $i++) {
159                $c = $separator{$i};
160
161                if ($c >= $this->min && $c <= $this->max && $this->values[$c - $this->min] != -1) {
162                    throw new GTException("parameter separator contains characters from the encoding alphabet");
163                }
164            }
165        }
166
167        $result = "";
168
169        $i = 0; // number of output characters produced
170        $j = 0; // number of input bytes consumed
171
172        $buff = 0; // buffer of input bits not yet sent to output
173        $size = 0; // number of bits in the buffer
174        $mask = (1 << $this->bits) - 1;
175
176        while ($this->bits * $i < 8 * $length) {
177
178            if ($frequency > 0 && $i > 0 && $i % $frequency == 0) {
179                $result .= $separator;
180            }
181
182            while ($size < $this->bits) {
183
184                $next = ($j < $length ? $bytes[$offset + $j] : 0);
185                $j++;
186
187                $buff = ($buff << 8) | ($next & 0xff);
188                $size += 8;
189
190            }
191
192            $result .= $this->chars[($buff >> ($size - $this->bits)) & $mask];
193            $size -= $this->bits;
194
195            $i++;
196
197        }
198
199        // pad
200
201        while ($i % $this->block != 0) {
202            if ($frequency > 0 && $i > 0 && $i % $frequency == 0) {
203                $result .= $separator;
204            }
205
206            $result .= $this->pad;
207            $i++;
208        }
209
210        return $result;
211
212    }
213
214    /**
215     * Decodes the given base-X string into bytes.
216     *
217     * This method silently ignores any non-base-X characters.
218     *
219     * @throws GTException
220     * @param  string $string the string to decode
221     * @return array decoded bytes
222     */
223    public function decode($string) {
224
225        if (empty($string)) {
226            throw new GTException("parameter string is required");
227        }
228
229        $result = array();
230
231        $i = 0; // number of output bytes produced
232        $j = 0; // number of input  bytes consumed
233
234        $buff = 0; // buffer of input bits not yet sent to output
235        $size = 0; // number of bits in the buffer
236
237        while ($j < strlen($string)) {
238
239            $next = ord($string{$j});
240            $j++;
241
242            if ($next < $this->min || $next > $this->max) {
243                continue;
244            }
245
246            $next = $this->values[$next - $this->min];
247
248            if ($next == -1) {
249                continue;
250            }
251
252            $buff = ($buff << $this->bits) | $next;
253            $size += $this->bits;
254
255            while ($size >= 8) {
256
257                $result[$i] = (($buff >> ($size - 8)) & 0xff);
258
259                $size -= 8;
260                $i++;
261
262            }
263        }
264
265        return $result;
266
267    }
268
269    /**
270     * Updates $this->min and $this->max so that the range [$this->min .. $this->max] includes all values from $string
271     *
272     * @param  string $string characters to process
273     * @return void
274     */
275    private function addMinMax($string) {
276
277        for ($i = 0; $i < strlen($string); $i++) {
278
279            $c = ord($string{$i});
280
281            if ($this->min == -1 || $this->min > $c) {
282                $this->min = $c;
283            }
284
285            if ($this->max == -1 || $this->max < $c) {
286                $this->max = $c;
287            }
288        }
289    }
290
291    /**
292     * Adds the characters from the given string to $this->values lookup table.
293     *
294     * @throws GTException
295     * @param  string $string characters to process
296     * @return void
297     */
298    private function addChars($string) {
299
300        for ($i = 0; $i < strlen($string); $i++) {
301
302            $c = ord($string{$i}) - $this->min;
303
304            if ($this->values[$c] != -1 && $this->values[$c] != $i) {
305                throw new GTException("Duplicate character in encoding alphabet");
306            }
307
308            $this->values[$c] = $i;
309
310        }
311    }
312
313}
314
315?>
316