lib/core: add low-level byte writing method
[nit.git] / lib / core / stream.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # This file is free software, which comes along with NIT. This software is
4 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
5 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
6 # PARTICULAR PURPOSE. You can modify it is you want, provided this header
7 # is kept unaltered, and a notification of the changes is added.
8 # You are allowed to redistribute it and sell it, alone or is a part of
9 # another product.
10
11 # Input and output streams of characters
12 module stream
13
14 import error
15 intrude import bytes
16 import codecs
17
18 in "C" `{
19 #include <unistd.h>
20 #include <string.h>
21 #include <signal.h>
22 `}
23
24 # Any kind of error that could be produced by an operation on Streams
25 class IOError
26 super Error
27 end
28
29 # Any kind of stream to read/write/both to or from a source
30 abstract class Stream
31 # Codec used to transform raw data to text
32 #
33 # Note: defaults to UTF-8
34 var codec: Codec = utf8_codec is protected writable(set_codec)
35
36 # Lookahead buffer for codecs
37 #
38 # Since some codecs are multibyte, a lookahead may be required
39 # to store the next bytes and consume them only if a valid character
40 # is read.
41 protected var lookahead: CString is noinit
42
43 # Capacity of the lookahead
44 protected var lookahead_capacity = 0
45
46 # Current occupation of the lookahead
47 protected var lookahead_length = 0
48
49 # Buffer for writing data to a stream
50 protected var write_buffer: CString is noinit
51
52 init do
53 var lcap = codec.max_lookahead
54 lookahead = new CString(lcap)
55 write_buffer = new CString(lcap)
56 lookahead_length = 0
57 lookahead_capacity = lcap
58 end
59
60 # Change the codec for this stream.
61 fun codec=(c: Codec) do
62 if c.max_lookahead > lookahead_capacity then
63 var lcap = codec.max_lookahead
64 var lk = new CString(lcap)
65 var llen = lookahead_length
66 if llen > 0 then
67 lookahead.copy_to(lk, llen, 0, 0)
68 end
69 lookahead = lk
70 lookahead_capacity = lcap
71 write_buffer = new CString(lcap)
72 end
73 set_codec(c)
74 end
75
76 # Error produced by the file stream
77 #
78 # var ifs = new FileReader.open("donotmakethisfile.binx")
79 # ifs.read_all
80 # ifs.close
81 # assert ifs.last_error != null
82 var last_error: nullable IOError = null
83
84 # close the stream
85 fun close is abstract
86
87 # Pre-work hook.
88 #
89 # Used to inform `self` that operations will start.
90 # Specific streams can use this to prepare some resources.
91 #
92 # Is automatically invoked at the beginning of `with` structures.
93 #
94 # Do nothing by default.
95 fun start do end
96
97 # Post-work hook.
98 #
99 # Used to inform `self` that the operations are over.
100 # Specific streams can use this to free some resources.
101 #
102 # Is automatically invoked at the end of `with` structures.
103 #
104 # call `close` by default.
105 fun finish do close
106 end
107
108 # A `Stream` that can be read from
109 abstract class Reader
110 super Stream
111
112 # Read a byte directly from the underlying stream, without
113 # considering any eventual buffer
114 protected fun raw_read_byte: Int is abstract
115
116 # Read at most `max` bytes from the underlying stream into `buf`,
117 # without considering any eventual buffer
118 #
119 # Returns how many bytes were read
120 protected fun raw_read_bytes(buf: CString, max: Int): Int do
121 var rd = 0
122 for i in [0 .. max[ do
123 var b = raw_read_byte
124 if b < 0 then break
125 buf[i] = b.to_b
126 rd += 1
127 end
128 return rd
129 end
130
131 # Reads a character. Returns `null` on EOF or timeout
132 fun read_char: nullable Char is abstract
133
134 # Reads a byte. Returns a negative value on error
135 fun read_byte: Int do
136 var llen = lookahead_length
137 if llen == 0 then return raw_read_byte
138 var lk = lookahead
139 var b = lk[0].to_i
140 if llen == 1 then
141 lookahead_length = 0
142 else
143 lk.lshift(1, llen - 1, 1)
144 lookahead_length -= 1
145 end
146 return b
147 end
148
149 # Reads a String of at most `i` length
150 fun read(i: Int): String do
151 var cs = new CString(i)
152 var rd = read_bytes_to_cstring(cs, i)
153 return codec.decode_string(cs, rd)
154 end
155
156 # Reads up to `max` bytes from source
157 fun read_bytes(max: Int): Bytes do
158 var cs = new CString(max)
159 var rd = read_bytes_to_cstring(cs, max)
160 return new Bytes(cs, rd, max)
161 end
162
163 # Reads up to `max` bytes from source and stores them in `bytes`
164 fun read_bytes_to_cstring(bytes: CString, max: Int): Int do
165 var llen = lookahead_length
166 if llen == 0 then return raw_read_bytes(bytes, max)
167 var rd = max.min(llen)
168 var lk = lookahead
169 lk.copy_to(bytes, rd, 0, 0)
170 if rd < llen then
171 lk.lshift(rd, llen - rd, rd)
172 lookahead_length -= rd
173 else
174 lookahead_length = 0
175 end
176 return rd + raw_read_bytes(bytes, max - rd)
177 end
178
179 # Read a string until the end of the line.
180 #
181 # The line terminator '\n' and '\r\n', if any, is removed in each line.
182 #
183 # ~~~
184 # var txt = "Hello\n\nWorld\n"
185 # var i = new StringReader(txt)
186 # assert i.read_line == "Hello"
187 # assert i.read_line == ""
188 # assert i.read_line == "World"
189 # assert i.eof
190 # ~~~
191 #
192 # Only LINE FEED (`\n`), CARRIAGE RETURN & LINE FEED (`\r\n`), and
193 # the end or file (EOF) is considered to delimit the end of lines.
194 # CARRIAGE RETURN (`\r`) alone is not used for the end of line.
195 #
196 # ~~~
197 # var txt2 = "Hello\r\n\n\rWorld"
198 # var i2 = new StringReader(txt2)
199 # assert i2.read_line == "Hello"
200 # assert i2.read_line == ""
201 # assert i2.read_line == "\rWorld"
202 # assert i2.eof
203 # ~~~
204 #
205 # NOTE: Use `append_line_to` if the line terminator needs to be preserved.
206 fun read_line: String
207 do
208 if last_error != null then return ""
209 if eof then return ""
210 var s = new FlatBuffer
211 append_line_to(s)
212 return s.to_s.chomp
213 end
214
215 # Read all the lines until the eof.
216 #
217 # The line terminator '\n' and `\r\n` is removed in each line,
218 #
219 # ~~~
220 # var txt = "Hello\n\nWorld\n"
221 # var i = new StringReader(txt)
222 # assert i.read_lines == ["Hello", "", "World"]
223 # ~~~
224 #
225 # This method is more efficient that splitting
226 # the result of `read_all`.
227 #
228 # NOTE: SEE `read_line` for details.
229 fun read_lines: Array[String]
230 do
231 var res = new Array[String]
232 while not eof do
233 res.add read_line
234 end
235 return res
236 end
237
238 # Return an iterator that read each line.
239 #
240 # The line terminator '\n' and `\r\n` is removed in each line,
241 # The line are read with `read_line`. See this method for details.
242 #
243 # ~~~
244 # var txt = "Hello\n\nWorld\n"
245 # var i = new StringReader(txt)
246 # assert i.each_line.to_a == ["Hello", "", "World"]
247 # ~~~
248 #
249 # Unlike `read_lines` that read all lines at the call, `each_line` is lazy.
250 # Therefore, the stream should no be closed until the end of the stream.
251 #
252 # ~~~
253 # i = new StringReader(txt)
254 # var el = i.each_line
255 #
256 # assert el.item == "Hello"
257 # el.next
258 # assert el.item == ""
259 # el.next
260 #
261 # i.close
262 #
263 # assert not el.is_ok
264 # # closed before "world" is read
265 # ~~~
266 fun each_line: LineIterator do return new LineIterator(self)
267
268 # Read all the stream until the eof.
269 #
270 # The content of the file is returned as a String.
271 #
272 # ~~~
273 # var txt = "Hello\n\nWorld\n"
274 # var i = new StringReader(txt)
275 # assert i.read_all == txt
276 # ~~~
277 fun read_all: String do
278 var s = read_all_bytes
279 var slen = s.length
280 if slen == 0 then return ""
281 return codec.decode_string(s.items, s.length)
282 end
283
284 # Read all the stream until the eof.
285 #
286 # The content of the file is returned verbatim.
287 fun read_all_bytes: Bytes
288 do
289 if last_error != null then return new Bytes.empty
290 var s = new Bytes.empty
291 var buf = new CString(4096)
292 while not eof do
293 var rd = read_bytes_to_cstring(buf, 4096)
294 s.append_ns(buf, rd)
295 end
296 return s
297 end
298
299 # Read a string until the end of the line and append it to `s`.
300 #
301 # Unlike `read_line` and other related methods,
302 # the line terminator '\n', if any, is preserved in each line.
303 # Use the method `Text::chomp` to safely remove it.
304 #
305 # ~~~
306 # var txt = "Hello\n\nWorld\n"
307 # var i = new StringReader(txt)
308 # var b = new FlatBuffer
309 # i.append_line_to(b)
310 # assert b == "Hello\n"
311 # i.append_line_to(b)
312 # assert b == "Hello\n\n"
313 # i.append_line_to(b)
314 # assert b == txt
315 # assert i.eof
316 # ~~~
317 #
318 # If `\n` is not present at the end of the result, it means that
319 # a non-eol terminated last line was returned.
320 #
321 # ~~~
322 # var i2 = new StringReader("hello")
323 # assert not i2.eof
324 # var b2 = new FlatBuffer
325 # i2.append_line_to(b2)
326 # assert b2 == "hello"
327 # assert i2.eof
328 # ~~~
329 #
330 # NOTE: The single character LINE FEED (`\n`) delimits the end of lines.
331 # Therefore CARRIAGE RETURN & LINE FEED (`\r\n`) is also recognized.
332 fun append_line_to(s: Buffer)
333 do
334 if last_error != null then return
335 loop
336 var x = read_char
337 if x == null then
338 if eof then return
339 else
340 s.chars.push(x)
341 if x == '\n' then return
342 end
343 end
344 end
345
346 # Is there something to read.
347 # This function returns 'false' if there is something to read.
348 fun eof: Bool is abstract
349
350 # Read the next sequence of non whitespace characters.
351 #
352 # Leading whitespace characters are skipped.
353 # The first whitespace character that follows the result is consumed.
354 #
355 # An empty string is returned if the end of the file or an error is encounter.
356 #
357 # ~~~
358 # var w = new StringReader(" Hello, \n\t World!")
359 # assert w.read_word == "Hello,"
360 # assert w.read_char == '\n'
361 # assert w.read_word == "World!"
362 # assert w.read_word == ""
363 # ~~~
364 #
365 # `Char::is_whitespace` determines what is a whitespace.
366 fun read_word: String
367 do
368 var buf = new FlatBuffer
369 var c = read_nonwhitespace
370 if c != null then
371 buf.add(c)
372 while not eof do
373 c = read_char
374 if c == null then break
375 if c.is_whitespace then break
376 buf.add(c)
377 end
378 end
379 var res = buf.to_s
380 return res
381 end
382
383 # Skip whitespace characters (if any) then return the following non-whitespace character.
384 #
385 # Returns the code point of the character.
386 # Returns `null` on end of file or error.
387 #
388 # In fact, this method works like `read_char` except it skips whitespace.
389 #
390 # ~~~
391 # var w = new StringReader(" \nab\tc")
392 # assert w.read_nonwhitespace == 'a'
393 # assert w.read_nonwhitespace == 'b'
394 # assert w.read_nonwhitespace == 'c'
395 # assert w.read_nonwhitespace == null
396 # ~~~
397 #
398 # `Char::is_whitespace` determines what is a whitespace.
399 fun read_nonwhitespace: nullable Char
400 do
401 var c: nullable Char = null
402 while not eof do
403 c = read_char
404 if c == null or not c.is_whitespace then break
405 end
406 return c
407 end
408 end
409
410 # Iterator returned by `Reader::each_line`.
411 # See the aforementioned method for details.
412 class LineIterator
413 super Iterator[String]
414
415 # The original stream
416 var stream: Reader
417
418 redef fun is_ok
419 do
420 var res = not stream.eof
421 if not res and close_on_finish then stream.close
422 return res
423 end
424
425 redef fun item
426 do
427 var line = self.line
428 if line == null then
429 line = stream.read_line
430 end
431 self.line = line
432 return line
433 end
434
435 # The last line read (cache)
436 private var line: nullable String = null
437
438 redef fun next
439 do
440 # force the read
441 if line == null then item
442 # drop the line
443 line = null
444 end
445
446 # Close the stream when the stream is at the EOF.
447 #
448 # Default is false.
449 var close_on_finish = false is writable
450
451 redef fun finish
452 do
453 if close_on_finish then stream.close
454 end
455 end
456
457 # `Reader` capable of declaring if readable without blocking
458 abstract class PollableReader
459 super Reader
460
461 # Is there something to read? (without blocking)
462 fun poll_in: Bool is abstract
463
464 end
465
466 # A `Stream` that can be written to
467 abstract class Writer
468 super Stream
469
470 # Write bytes from `s`
471 fun write_bytes(s: Bytes) do write_bytes_from_cstring(s.items, s.length)
472
473 # Write `len` bytes from `ns`
474 fun write_bytes_from_cstring(ns: CString, len: Int) is abstract
475
476 # Write a string
477 fun write(s: Text) is abstract
478
479 # Write a single byte
480 fun write_byte(value: Byte) is abstract
481
482 # Write a single char
483 fun write_char(c: Char) do
484 var ln = codec.add_char_to(c, write_buffer)
485 write_bytes_from_cstring(write_buffer, ln)
486 end
487
488 # Can the stream be used to write
489 fun is_writable: Bool is abstract
490 end
491
492 # Things that can be efficienlty written to a `Writer`
493 #
494 # The point of this interface is to allow the instance to be efficiently
495 # written into a `Writer`.
496 #
497 # Ready-to-save documents usually provide this interface.
498 interface Writable
499 # Write itself to a `stream`
500 # The specific logic it let to the concrete subclasses
501 fun write_to(stream: Writer) is abstract
502
503 # Like `write_to` but return a new String (may be quite large)
504 #
505 # This funtionality is anectodical, since the point
506 # of streamable object to to be efficienlty written to a
507 # stream without having to allocate and concatenate strings
508 fun write_to_string: String
509 do
510 var stream = new StringWriter
511 write_to(stream)
512 return stream.to_s
513 end
514 end
515
516 redef class Bytes
517 super Writable
518 redef fun write_to(s) do s.write_bytes(self)
519
520 redef fun write_to_string do return to_s
521 end
522
523 redef class Text
524 super Writable
525 redef fun write_to(stream) do stream.write(self)
526 end
527
528 # Input streams with a buffered input for efficiency purposes
529 abstract class BufferedReader
530 super Reader
531 redef fun read_char
532 do
533 if last_error != null then return null
534 if eof then
535 last_error = new IOError("Stream has reached eof")
536 return null
537 end
538 # TODO: Fix when supporting UTF-8
539 var c = _buffer[_buffer_pos].to_i.code_point
540 _buffer_pos += 1
541 return c
542 end
543
544 redef fun read_byte
545 do
546 if last_error != null then return -1
547 if eof then
548 last_error = new IOError("Stream has reached eof")
549 return -1
550 end
551 var c = _buffer[_buffer_pos]
552 _buffer_pos += 1
553 return c.to_i
554 end
555
556 # Resets the internal buffer
557 fun buffer_reset do
558 _buffer_length = 0
559 _buffer_pos = 0
560 end
561
562 # Peeks up to `n` bytes in the buffer
563 #
564 # The operation does not consume the buffer
565 #
566 # ~~~nitish
567 # var x = new FileReader.open("File.txt")
568 # assert x.peek(5) == x.read(5)
569 # ~~~
570 fun peek(i: Int): Bytes do
571 if eof then return new Bytes.empty
572 var remsp = _buffer_length - _buffer_pos
573 if i <= remsp then
574 var bf = new Bytes.with_capacity(i)
575 bf.append_ns_from(_buffer, i, _buffer_pos)
576 return bf
577 end
578 var bf = new Bytes.with_capacity(i)
579 bf.append_ns_from(_buffer, remsp, _buffer_pos)
580 _buffer_pos = _buffer_length
581 read_intern(i - bf.length, bf)
582 remsp = _buffer_length - _buffer_pos
583 var full_len = bf.length + remsp
584 if full_len > _buffer_capacity then
585 var c = _buffer_capacity
586 while c < full_len do c = c * 2 + 2
587 _buffer_capacity = c
588 end
589 var nns = new CString(_buffer_capacity)
590 bf.items.copy_to(nns, bf.length, 0, 0)
591 _buffer.copy_to(nns, remsp, _buffer_pos, bf.length)
592 _buffer = nns
593 _buffer_pos = 0
594 _buffer_length = full_len
595 return bf
596 end
597
598 redef fun read_bytes_to_cstring(buf, i)
599 do
600 if last_error != null then return 0
601 var bbf = new Bytes(buf, 0, i)
602 return read_intern(i, bbf)
603 end
604
605 # Fills `buf` with at most `i` bytes read from `self`
606 private fun read_intern(i: Int, buf: Bytes): Int do
607 if eof then return 0
608 var p = _buffer_pos
609 var bufsp = _buffer_length - p
610 if bufsp >= i then
611 _buffer_pos += i
612 buf.append_ns_from(_buffer, i, p)
613 return i
614 end
615 _buffer_pos = _buffer_length
616 var readln = _buffer_length - p
617 buf.append_ns_from(_buffer, readln, p)
618 var rd = read_intern(i - readln, buf)
619 return rd + readln
620 end
621
622 redef fun read_all_bytes
623 do
624 if last_error != null then return new Bytes.empty
625 var s = new Bytes.with_capacity(10)
626 var b = _buffer
627 while not eof do
628 var j = _buffer_pos
629 var k = _buffer_length
630 var rd_sz = k - j
631 s.append_ns_from(b, rd_sz, j)
632 _buffer_pos = k
633 fill_buffer
634 end
635 return s
636 end
637
638 redef fun append_line_to(s)
639 do
640 var lb = new Bytes.with_capacity(10)
641 loop
642 # First phase: look for a '\n'
643 var i = _buffer_pos
644 while i < _buffer_length and _buffer[i] != 0xAu8 do
645 i += 1
646 end
647
648 var eol
649 if i < _buffer_length then
650 assert _buffer[i] == 0xAu8
651 i += 1
652 eol = true
653 else
654 eol = false
655 end
656
657 # if there is something to append
658 if i > _buffer_pos then
659 # Copy from the buffer to the string
660 var j = _buffer_pos
661 while j < i do
662 lb.add(_buffer[j])
663 j += 1
664 end
665 _buffer_pos = i
666 else
667 assert end_reached
668 s.append lb.to_s
669 return
670 end
671
672 if eol then
673 # so \n is found
674 s.append lb.to_s
675 return
676 else
677 # so \n is not found
678 if end_reached then
679 s.append lb.to_s
680 return
681 end
682 fill_buffer
683 end
684 end
685 end
686
687 redef fun eof
688 do
689 if _buffer_pos < _buffer_length then return false
690 if end_reached then return true
691 fill_buffer
692 return _buffer_pos >= _buffer_length and end_reached
693 end
694
695 # The buffer
696 private var buffer: CString = new CString(0)
697
698 # The current position in the buffer
699 private var buffer_pos = 0
700
701 # Length of the current buffer (i.e. nuber of bytes in the buffer)
702 private var buffer_length = 0
703
704 # Capacity of the buffer
705 private var buffer_capacity = 0
706
707 # Fill the buffer
708 protected fun fill_buffer is abstract
709
710 # Has the last fill_buffer reached the end
711 protected fun end_reached: Bool is abstract
712
713 # Allocate a `_buffer` for a given `capacity`.
714 protected fun prepare_buffer(capacity: Int)
715 do
716 _buffer = new CString(capacity)
717 _buffer_pos = 0 # need to read
718 _buffer_length = 0
719 _buffer_capacity = capacity
720 end
721 end
722
723 # A `Stream` that can be written to and read from
724 abstract class Duplex
725 super Reader
726 super Writer
727 end
728
729 # Write to `bytes` in memory
730 #
731 # ~~~
732 # var writer = new BytesWriter
733 #
734 # writer.write "Strings "
735 # writer.write_char '&'
736 # writer.write_byte 0x20u8
737 # writer.write_bytes "bytes".to_bytes
738 #
739 # assert writer.to_s == "\\x53\\x74\\x72\\x69\\x6E\\x67\\x73\\x20\\x26\\x20\\x62\\x79\\x74\\x65\\x73"
740 # assert writer.bytes.to_s == "Strings & bytes"
741 # ~~~
742 #
743 # As with any binary data, UTF-8 code points encoded on two bytes or more
744 # can be constructed byte by byte.
745 #
746 # ~~~
747 # writer = new BytesWriter
748 #
749 # # Write just the character first half
750 # writer.write_byte 0xC2u8
751 # assert writer.to_s == "\\xC2"
752 # assert writer.bytes.to_s == "�"
753 #
754 # # Complete the character
755 # writer.write_byte 0xA2u8
756 # assert writer.to_s == "\\xC2\\xA2"
757 # assert writer.bytes.to_s == "¢"
758 # ~~~
759 class BytesWriter
760 super Writer
761
762 # Written memory
763 var bytes = new Bytes.empty
764
765 redef fun to_s do return bytes.chexdigest
766
767 redef fun write(str)
768 do
769 if closed then return
770 str.append_to_bytes bytes
771 end
772
773 redef fun write_char(c)
774 do
775 if closed then return
776 bytes.add_char c
777 end
778
779 redef fun write_byte(value)
780 do
781 if closed then return
782 bytes.add value
783 end
784
785 redef fun write_bytes_from_cstring(ns, len) do
786 if closed then return
787 bytes.append_ns(ns, len)
788 end
789
790 # Is the stream closed?
791 protected var closed = false
792
793 redef fun close do closed = true
794 redef fun is_writable do return not closed
795 end
796
797 # `Stream` writing to a `String`
798 #
799 # This class has the same behavior as `BytesWriter`
800 # except for `to_s` which decodes `bytes` to a string.
801 #
802 # ~~~
803 # var writer = new StringWriter
804 #
805 # writer.write "Strings "
806 # writer.write_char '&'
807 # writer.write_byte 0x20u8
808 # writer.write_bytes "bytes".to_bytes
809 #
810 # assert writer.to_s == "Strings & bytes"
811 # ~~~
812 class StringWriter
813 super BytesWriter
814
815 redef fun to_s do return bytes.to_s
816 end
817
818 # Read from `bytes` in memory
819 #
820 # ~~~
821 # var reader = new BytesReader(b"a…b")
822 # assert reader.read_char == 'a'
823 # assert reader.read_byte == 0xE2 # 1st byte of '…'
824 # assert reader.read_byte == 0x80 # 2nd byte of '…'
825 # assert reader.read_char == '�' # Reads the last byte as an invalid char
826 # assert reader.read_all_bytes == b"b"
827 # ~~~
828 class BytesReader
829 super Reader
830
831 # Source data to read
832 var bytes: Bytes
833
834 # The current position in `bytes`
835 private var cursor = 0
836
837 redef fun read_char
838 do
839 if cursor >= bytes.length then return null
840
841 var len = bytes.items.length_of_char_at(cursor)
842 var char = bytes.items.char_at(cursor)
843 cursor += len
844 return char
845 end
846
847 redef fun read_byte
848 do
849 if cursor >= bytes.length then return -1
850
851 var c = bytes[cursor]
852 cursor += 1
853 return c.to_i
854 end
855
856 redef fun close do bytes = new Bytes.empty
857
858 redef fun read_all_bytes
859 do
860 var res = bytes.slice_from(cursor)
861 cursor = bytes.length
862 return res
863 end
864
865 redef fun raw_read_bytes(ns, max) do
866 if cursor >= bytes.length then return 0
867
868 var copy = max.min(bytes.length - cursor)
869 bytes.items.copy_to(ns, copy, cursor, 0)
870 cursor += copy
871 return copy
872 end
873
874 redef fun eof do return cursor >= bytes.length
875 end
876
877 # `Stream` reading from a `String` source
878 #
879 # This class has the same behavior as `BytesReader`
880 # except for its constructor accepting a `String`.
881 #
882 # ~~~
883 # var reader = new StringReader("a…b")
884 # assert reader.read_char == 'a'
885 # assert reader.read_byte == 0xE2 # 1st byte of '…'
886 # assert reader.read_byte == 0x80 # 2nd byte of '…'
887 # assert reader.read_char == '�' # Reads the last byte as an invalid char
888 # assert reader.read_all == "b"
889 # ~~~
890 class StringReader
891 super BytesReader
892
893 autoinit source
894
895 # Source data to read
896 var source: String
897
898 init do bytes = source.to_bytes
899
900 redef fun close
901 do
902 source = ""
903 super
904 end
905 end