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