1-- A line reader reads a file through a caller's buffer and lends it back
2-- a line at a time, as a directory lends its names.
3
4import core/mem
5
6--- A line, its line ending included, does not fit the line reader's
7--- buffer.
8public line_too_long: atom
9
10lines_value: type (provider: type) = struct
11 host: provider
12 opened: file
13 buffer: []mut u8
14 start: usize
15 filled: usize
16 finished: bool
17 skipping: bool
18end lines_value
19
20--- Reads a file a line at a time through a buffer the caller supplies: a
21--- value the caller keeps and edits, passing it `inout` to `read_line`.
22--- Each line is lent from the reader until the next read, which reuses the
23--- buffer it lies in, so a program that keeps a line copies it.
24public line_reader: type (provider: type) = lines_value(provider)
25
26--- One line, lent from its reader: its bytes without the line ending, and
27--- whether the input had already ended, in which case there is no line and
28--- `bytes` is empty. An empty line is an empty `bytes` that has not
29--- `finished`.
30public line: type = struct
31 bytes: []u8
32 finished: bool
33end line
34
35--- A line reader over the caller's `buffer`, reading `opened` through
36--- `host`. It holds a copy of the reader's handle; keep the handle's
37--- state and the buffer alive while it is used. The longest line it reads
38--- is one byte shorter than the buffer, which must hold the line's `\n` as
39--- well.
40public lines_over: (provider: type is reader, host: provider, opened: file,
41 buffer: []mut u8)
42 -> (lines: line_reader(provider) from buffer, host) =
43 lines = (host: host, opened: opened, buffer: buffer, start: 0, filled: 0,
44 finished: false, skipping: false)
45end lines_over
46
47-- Move the unread bytes down to the front of the buffer.
48compact: (provider: type, inout lines: line_reader(provider))
49 -> none ! io_failed =
50 return when lines.start == 0
51 remaining: usize = lines.filled - lines.start
52 mem.copy(lines.buffer[0..<remaining],
53 lines.buffer[lines.start..<lines.filled]) else (problem)
54 _ = problem
55 fail io_failed
56 end
57 lines.start = 0
58 lines.filled = remaining
59end compact
60
61-- Read more after the unread bytes, moved to the front; the reader's zero
62-- marks the end of input.
63refill: (provider: type is reader, inout lines: line_reader(provider))
64 -> none ! io_failed =
65 try compact(lines)
66 room := lines.buffer
67 got: usize = try provider.read(lines.host, lines.opened,
68 lines.buffer[lines.filled..<lenof room])
69 if got == 0 then
70 lines.finished = true
71 else
72 lines.filled += got
73 end if
74end refill
75
76--- Read the next line, lent from the reader until the next read. A line
77--- ends at `\n`, and a `\r` just before it is part of the ending, so text
78--- written with `\r\n` reads the same as text written with `\n`; a lone
79--- `\r` is an ordinary byte. The last line need not end: input that ends
80--- after it gives it, and the next read is `finished`. End of input is a
81--- line that has `finished`, as an empty read is the end of a file.
82---
83--- A line that does not fit the buffer with its ending reports
84--- `line_too_long`, and the rest of that line is skipped, so the read after
85--- it gives the line that follows. A reader's failure is its `io_failed`,
86--- and keeps every byte already read for the next call.
87public read_line: (provider: type is reader,
88 inout lines: line_reader(provider))
89 -> (item: line from lines)
90 ! line_too_long | io_failed =
91 loop do
92 if lines.skipping then
93 waiting: usize = lines.filled - lines.start
94 at: usize = mem.find(lines.buffer[lines.start..<lines.filled], 10)
95 if at < waiting then
96 lines.start += at + 1
97 lines.skipping = false
98 else
99 lines.start = 0
100 lines.filled = 0
101 if lines.finished then
102 lines.skipping = false
103 else
104 try refill(lines)
105 end if
106 end if
107 else
108 waiting: usize = lines.filled - lines.start
109 at: usize = mem.find(lines.buffer[lines.start..<lines.filled], 10)
110 if at < waiting then
111 begins: usize = lines.start
112 mut ends: usize = begins + at
113 lines.start = ends + 1
114 if ends > begins and lines.buffer[ends - 1] == 13 then
115 dec ends
116 end if
117 item = (bytes: lines.buffer[begins..<ends], finished: false)
118 return
119 end if
120 if lines.finished then
121 begins: usize = lines.start
122 lines.start = lines.filled
123 item = (bytes: lines.buffer[begins..<lines.filled],
124 finished: waiting == 0)
125 return
126 end if
127 room := lines.buffer
128 if waiting == lenof room then
129 lines.start = 0
130 lines.filled = 0
131 lines.skipping = true
132 fail line_too_long
133 end if
134 try refill(lines)
135 end if
136 end loop
137end read_line