lib/binary: intro services to read/write null terminated strings
[nit.git] / lib / binary / binary.nit
1 # This file is part of NIT (http://www.nitlanguage.org).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Add reading and writing binary services
16 #
17 # ~~~
18 # var w = new FileWriter.open("/tmp/data.bin")
19 # w.write "hello"
20 # w.write_int64 123456789
21 # w.write_byte 3
22 # w.write_float 1.25
23 # w.write_double 1.234567
24 # w.write_bits(true, false, true)
25 # assert w.last_error == null
26 # w.close
27 #
28 # var r = new FileReader.open("/tmp/data.bin")
29 # assert r.read(5) == "hello"
30 # assert r.read_int64 == 123456789
31 # assert r.read_byte == 3
32 # assert r.read_float == 1.25
33 # assert r.read_double == 1.234567
34 #
35 # var bits = r.read_bits
36 # assert bits[0] and not bits[1] and bits[2]
37 #
38 # assert r.last_error == null
39 # r.close
40 # ~~~
41 module binary
42
43 in "C" `{
44 #include <inttypes.h>
45 #include <endian.h>
46
47 // Android compatibility
48 #ifndef be64toh
49 #define be64toh(val) betoh64(val)
50 #endif
51 #ifndef le64toh
52 #define le64toh(val) letoh64(val)
53 #endif
54 `}
55
56 # A stream of binary data
57 abstract class BinaryStream
58 super Stream
59
60 # Use the big-endian convention? otherwise use little-endian.
61 #
62 # By default, `true` to use big-endian.
63 var big_endian = true is writable
64 end
65
66 redef abstract class Writer
67 super BinaryStream
68
69 # Write a boolean `value` on a byte, using 0 for `false` and 1 for `true`
70 fun write_bool(value: Bool) do write_byte if value then 1 else 0
71
72 # Write up to 8 `Bool` in a byte
73 #
74 # To be used with `BinaryReader::read_bits`.
75 #
76 # Ensure: `bits.length <= 8`
77 fun write_bits(bits: Bool...)
78 do
79 assert bits.length <= 8
80
81 var int = 0
82 for b in bits.length.times do
83 if bits[b] then int += 2**b
84 end
85
86 write_byte int
87 end
88
89 # Write `text` as a null terminated string
90 #
91 # To be used with `Reader::read_string`.
92 #
93 # Require: `text` has no null bytes.
94 fun write_string(text: Text)
95 do
96 write text
97 write_byte 0x00
98 end
99
100 # Write a floating point `value` on 32 bits
101 #
102 # Using this format may result in a loss of precision as it uses less bits
103 # than Nit `Float`.
104 fun write_float(value: Float)
105 do
106 for i in [0..4[ do write_byte value.float_byte_at(i, big_endian)
107 end
108
109 # Write a floating point `value` on 64 bits
110 fun write_double(value: Float)
111 do
112 for i in [0..8[ do write_byte value.double_byte_at(i, big_endian)
113 end
114
115 # Write `value` as a signed integer on 64 bits
116 #
117 # Using this format may result in a loss of precision as the length of a
118 # Nit `Int` may be more than 64 bits on some platforms.
119 fun write_int64(value: Int)
120 do
121 for i in [0..8[ do write_byte value.int64_byte_at(i, big_endian)
122 end
123
124 # TODO:
125 #
126 # fun write_int8
127 # fun write_uint8 # No need for this one, it is the current `read_char`
128 # fun write_int16
129 # fun write_uint16
130 # fun write_int32
131 # fun write_uint32
132 # fun write_uint64
133 # fun write_long_double?
134 end
135
136 redef abstract class Reader
137 super BinaryStream
138
139 # Read a single byte and return `true` if its value is different than 0
140 fun read_bool: Bool do return read_byte != 0
141
142 # Get an `Array` of 8 `Bool` by reading a single byte
143 #
144 # To be used with `BinaryWriter::write_bits`.
145 fun read_bits: Array[Bool]
146 do
147 var int = read_byte
148 if int == null then return new Array[Bool]
149 return [for b in 8.times do int.bin_and(2**b) > 0]
150 end
151
152 # Read a null terminated string
153 #
154 # To be used with `Writer::write_string`.
155 fun read_string: String
156 do
157 var buf = new FlatBuffer
158 loop
159 var byte = read_byte
160 if byte == 0x00 then return buf.to_s
161 buf.chars.add byte.ascii
162 end
163 end
164
165 # Read a floating point on 32 bits and return it as a `Float`
166 #
167 # Using this format may result in a loss of precision as it uses less bits
168 # than Nit `Float`.
169 fun read_float: Float
170 do
171 if last_error != null then return 0.0
172
173 var b0 = read_byte
174 var b1 = read_byte
175 var b2 = read_byte
176 var b3 = read_byte
177
178 # Check for error, `last_error` is set by `read_byte`
179 if b0 == null or b1 == null or b2 == null or b3 == null then return 0.0
180
181 return native_read_float(b0, b1, b2, b3, big_endian)
182 end
183
184 # Utility for `read_float`
185 private fun native_read_float(b0, b1, b2, b3: Int, big_endian: Bool): Float `{
186 union {
187 unsigned char b[4];
188 float val;
189 uint32_t conv;
190 } u;
191
192 u.b[0] = b0;
193 u.b[1] = b1;
194 u.b[2] = b2;
195 u.b[3] = b3;
196
197 if (big_endian)
198 u.conv = be32toh(u.conv);
199 else u.conv = le32toh(u.conv);
200
201 return u.val;
202 `}
203
204 # Read a floating point on 64 bits and return it as a `Float`
205 fun read_double: Float
206 do
207 if last_error != null then return 0.0
208
209 var b0 = read_byte
210 var b1 = read_byte
211 var b2 = read_byte
212 var b3 = read_byte
213 var b4 = read_byte
214 var b5 = read_byte
215 var b6 = read_byte
216 var b7 = read_byte
217
218 # Check for error, `last_error` is set by `read_byte`
219 if b0 == null or b1 == null or b2 == null or b3 == null or
220 b4 == null or b5 == null or b6 == null or b7 == null then return 0.0
221
222 return native_read_double(b0, b1, b2, b3, b4, b5, b6, b7, big_endian)
223 end
224
225 # Utility for `read_double`
226 private fun native_read_double(b0, b1, b2, b3, b4, b5, b6, b7: Int, big_endian: Bool): Float `{
227 union {
228 unsigned char b[8];
229 double val;
230 uint64_t conv;
231 } u;
232
233 u.b[0] = b0;
234 u.b[1] = b1;
235 u.b[2] = b2;
236 u.b[3] = b3;
237 u.b[4] = b4;
238 u.b[5] = b5;
239 u.b[6] = b6;
240 u.b[7] = b7;
241
242 if (big_endian)
243 u.conv = be64toh(u.conv);
244 else u.conv = le64toh(u.conv);
245
246 return u.val;
247 `}
248
249 # Read a signed integer on 64 bits and return is an `Int`
250 #
251 # Using this format may result in a loss of precision as the length of a
252 # Nit `Int` may be less than 64 bits on some platforms.
253 fun read_int64: Int
254 do
255 if last_error != null then return 0
256
257 var b0 = read_byte
258 var b1 = read_byte
259 var b2 = read_byte
260 var b3 = read_byte
261 var b4 = read_byte
262 var b5 = read_byte
263 var b6 = read_byte
264 var b7 = read_byte
265
266 # Check for error, `last_error` is set by `read_byte`
267 if b0 == null or b1 == null or b2 == null or b3 == null or
268 b4 == null or b5 == null or b6 == null or b7 == null then return 0
269
270 return native_read_int64(b0, b1, b2, b3, b4, b5, b6, b7, big_endian)
271 end
272
273 # Utility for `read_int64`
274 private fun native_read_int64(b0, b1, b2, b3, b4, b5, b6, b7: Int, big_endian: Bool): Int `{
275 union {
276 unsigned char b[8];
277 int64_t val;
278 uint64_t conv;
279 } u;
280
281 u.b[0] = b0;
282 u.b[1] = b1;
283 u.b[2] = b2;
284 u.b[3] = b3;
285 u.b[4] = b4;
286 u.b[5] = b5;
287 u.b[6] = b6;
288 u.b[7] = b7;
289
290 if (big_endian)
291 u.conv = be64toh(u.conv);
292 else u.conv = le64toh(u.conv);
293
294 return u.val;
295 `}
296 end
297
298 redef class Int
299 # Utility for `BinaryWriter`
300 private fun int64_byte_at(index: Int, big_endian: Bool): Int `{
301 union {
302 unsigned char bytes[8];
303 int64_t val;
304 uint64_t conv;
305 } u;
306
307 u.val = recv;
308
309 if (big_endian)
310 u.conv = htobe64(u.conv);
311 else u.conv = htole64(u.conv);
312
313 return u.bytes[index];
314 `}
315 end
316
317 redef class Float
318 # Utility for `BinaryWriter`
319 private fun float_byte_at(index: Int, big_endian: Bool): Int `{
320 union {
321 unsigned char bytes[4];
322 float val;
323 uint32_t conv;
324 } u;
325
326 u.val = recv;
327
328 if (big_endian)
329 u.conv = htobe32(u.conv);
330 else u.conv = htole32(u.conv);
331
332 return u.bytes[index];
333 `}
334
335 # Utility for `BinaryWriter`
336 private fun double_byte_at(index: Int, big_endian: Bool): Int `{
337 union {
338 unsigned char bytes[8];
339 double val;
340 uint64_t conv;
341 } u;
342
343 u.val = recv;
344
345 if (big_endian)
346 u.conv = htobe64(u.conv);
347 else u.conv = htole64(u.conv);
348
349 return u.bytes[index];
350 `}
351 end