nit: Added link to `CONTRIBUTING.md` from the README
[nit.git] / lib / base64.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2013 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Offers the base 64 encoding and decoding algorithms
18 module base64
19
20 redef class NativeString
21 # Alphabet used by the base64 algorithm
22 private fun base64_chars : SequenceRead[Byte]
23 do
24 return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".bytes
25 end
26
27 # Reversed alphabet for base64
28 private fun inverted_base64_chars : HashMap[Byte, Byte]
29 do
30 var inv_base64_chars = new HashMap[Byte, Byte]
31 var l = base64_chars.length
32 for k in [0 .. l[ do
33 inv_base64_chars[base64_chars[k]] = k.to_b
34 end
35 return inv_base64_chars
36 end
37
38 # Encodes `self` to base64.
39 #
40 # By default, uses "=" for padding.
41 #
42 # assert "string".encode_base64 == "c3RyaW5n"
43 private fun encode_base64(length: Int, padding: nullable Byte): Bytes do
44 var base64_bytes = once base64_chars
45 if padding == null then padding = '='.ascii
46 var steps = length / 3
47 var bytes_in_last_step = length % 3
48 var result_length = steps * 4
49 if bytes_in_last_step > 0 then result_length += 4
50 var result = new Bytes.with_capacity(result_length)
51
52 var in_off = 0
53 for s in [0 .. steps[ do
54 var ind = ((self[in_off] & 0b1111_1100u8) >> 2).to_i
55 result.add base64_bytes[ind]
56 ind = ((self[in_off] & 0b0000_0011u8) << 4).to_i | ((self[in_off + 1] & 0b1111_0000u8) >> 4).to_i
57 result.add base64_bytes[ind]
58 ind = ((self[in_off + 1] & 0b0000_1111u8) << 2).to_i | ((self[in_off + 2] & 0b1100_0000u8) >> 6).to_i
59 result.add base64_bytes[ind]
60 ind = (self[in_off + 2] & 0b0011_1111u8).to_i
61 result.add base64_bytes[ind]
62 in_off += 3
63 end
64 if bytes_in_last_step == 1 then
65 result.add base64_bytes[((self[in_off] & 0b1111_1100u8) >> 2).to_i]
66 result.add base64_bytes[((self[in_off] & 0b0000_0011u8) << 4).to_i]
67 else if bytes_in_last_step == 2 then
68 result.add base64_bytes[((self[in_off] & 0b1111_1100u8) >> 2).to_i]
69 result.add base64_bytes[(((self[in_off] & 0b0000_0011u8) << 4) | ((self[in_off + 1] & 0b1111_0000u8) >> 4)).to_i]
70 result.add base64_bytes[((self[in_off + 1] & 0b0000_1111u8) << 2).to_i]
71 end
72 var rempad = if bytes_in_last_step > 0 then 3 - bytes_in_last_step else 0
73 for i in [0 .. rempad[ do result.add padding
74
75 return result
76 end
77
78 # Decodes `self` from base64
79 #
80 # assert "c3RyaW5n".decode_base64 == "string"
81 # assert "c3Rya\nW5n".decode_base64 == "string"
82 #
83 # REQUIRE: `length % 4 == 0`
84 private fun decode_base64(length: Int, padding: nullable Byte): Bytes do
85 if padding == null then padding = '='.ascii
86 var inv = once inverted_base64_chars
87 if length == 0 then return new Bytes.empty
88
89 # Remove non-base64 chars
90 var bytes = new Bytes.with_capacity(length)
91 for k in [0 .. length[ do
92 var byte = self[k]
93 if inv.has_key(byte) or byte == padding then bytes.add(byte)
94 end
95 length = bytes.length
96
97 var steps = length / 4
98 var result_length = steps * 3
99
100 var epos = length - 1
101 var padding_len = 0
102 while epos >= 0 and bytes[epos] == padding do
103 epos -= 1
104 padding_len += 1
105 end
106
107 if padding_len != 0 then steps -= 1
108 if padding_len == 1 then result_length -= 1
109 if padding_len == 2 then result_length -= 2
110
111 var result = new Bytes.with_capacity(result_length + 1)
112
113 for s in [0 .. steps[ do
114 var c0 = inv[bytes[s * 4]]
115 var c1 = inv[bytes[s * 4 + 1]]
116 var c2 = inv[bytes[s * 4 + 2]]
117 var c3 = inv[bytes[s * 4 + 3]]
118 result.add (((c0 & 0b0011_1111u8) << 2) | ((c1 & 0b0011_0000u8) >> 4))
119 result.add (((c1 & 0b0000_1111u8) << 4) | ((c2 & 0b0011_1100u8) >> 2))
120 result.add (((c2 & 0b0000_0011u8) << 6) | (c3 & 0b0011_1111u8))
121 end
122
123 var last_start = steps * 4
124 if padding_len == 1 then
125 var c0 = inv[bytes[last_start]]
126 var c1 = inv[bytes[last_start + 1]]
127 var c2 = inv[bytes[last_start + 2]]
128 result.add (((c0 & 0b0011_1111u8) << 2) | ((c1 & 0b0011_0000u8) >> 4))
129 result.add (((c1 & 0b0000_1111u8) << 4) | ((c2 & 0b0011_1100u8) >> 2))
130 else if padding_len == 2 then
131 var c0 = inv[bytes[last_start]]
132 var c1 = inv[bytes[last_start + 1]]
133 result.add (((c0 & 0b0011_1111u8) << 2) | ((c1 & 0b0011_0000u8) >> 4))
134 end
135
136 return result
137 end
138 end
139
140 redef class Bytes
141
142 # Encodes the receiver string to base64 using a custom padding character.
143 #
144 # If using the default padding character `=`, see `encode_base64`.
145 fun encode_base64(padding: nullable Byte): Bytes
146 do
147 return items.encode_base64(length, padding)
148 end
149
150 # Decodes the receiver string to base64 using a custom padding character.
151 #
152 # Default padding character `=`
153 fun decode_base64(padding : nullable Byte) : Bytes
154 do
155 return items.decode_base64(length, padding)
156 end
157 end
158
159 redef class String
160
161 # Encodes the receiver string to base64 using a custom padding character.
162 #
163 # If using the default padding character `=`, see `encode_base64`.
164 fun encode_base64(padding: nullable Byte): String
165 do
166 return to_cstring.encode_base64(bytelen, padding).to_s
167 end
168
169 # Decodes the receiver string to base64 using a custom padding character.
170 #
171 # Default padding character `=`
172 fun decode_base64(padding : nullable Byte) : String
173 do
174 return to_cstring.decode_base64(bytelen, padding).to_s
175 end
176 end