csv.nx source
↩ module page · 194 lines · 6598 B
1// csv.nx -- RFC 4180 comma-separated values parser.
2//
3// Companion to tsv.nx. CSV is messier than TSV because fields
4// may be quoted, quotes inside quotes are doubled, and the line
5// separator is CRLF per spec (most real-world files use plain
6// LF -- we accept both).
7//
8// RFC 4180 grammar (simplified):
9// file = header (CRLF record)* CRLF?
10// header/rec = field (, field)*
11// field = non_escaped | escaped
12// non_escaped = TEXTDATA*
13// escaped = DQUOTE (TEXTDATA | , | CR | LF | DQUOTE DQUOTE)* DQUOTE
14// DQUOTE = 0x22
15//
16// We emit a FIELD CALLBACK at each cell boundary and a ROW
17// CALLBACK at each newline. This avoids allocating a 2D table
18// and lets caller stream megabyte+ files.
19//
20// Invariants:
21// CSV1 Quoted fields handle embedded commas + newlines.
22// CSV2 Doubled quotes inside quoted fields ("") decode to one
23// literal DQUOTE -- caller handles via the callback (we
24// emit the raw quoted range; decoder below does the
25// unescape).
26// CSV3 Trailing CR before LF is stripped.
27// CSV4 Empty file -> 0 rows, 0 cells.
28
29import "syscalls.nx"
30
31const CSV_ERR_OVERFLOW: i64 = -1
32const CSV_ERR_UNTERM: i64 = -2
33
34// Caller-friendly output: two arrays populated by the parser.
35// cells[i] = (start, end) offsets into buf
36// rows[j] contains the first cell index for row j + 1 (sentinel)
37// Row j spans cells[rows[j] .. rows[j+1] - 1].
38struct CsvCell {
39 start: i64,
40 end: i64,
41 // Whether cell was quoted -- caller may need to unescape "".
42 quoted: i64,
43}
44
45// Parse an entire CSV blob. Fills cells[] + rows[].
46// On success returns number of rows; rows[] length is result+1
47// (last entry is total cell count). Negative on error.
48func csv_parse(buf: *u8, n: i64,
49 cells: *CsvCell, cells_cap: i64,
50 rows: *i64, rows_cap: i64) -> i64 {
51 var cell_count: i64 = 0
52 var row_count: i64 = 0
53 // Always record row 0 start at cell 0.
54 if rows_cap > 0 { rows[0] = 0 }
55 var i: i64 = 0
56
57 while i < n {
58 // Start of a new cell.
59 if cell_count >= cells_cap { return CSV_ERR_OVERFLOW }
60 let cell: *CsvCell = cells + cell_count * 24
61
62 if buf[i] == 0x22 {
63 // Quoted field. Consume opening quote.
64 i = i + 1
65 cell.start = i
66 cell.quoted = 1
67 while i < n {
68 if buf[i] == 0x22 {
69 // Either end of field OR escaped "".
70 if i + 1 < n {
71 if buf[i + 1] == 0x22 {
72 // "" escape; consume both, continue.
73 i = i + 2
74 continue
75 }
76 }
77 // End of quoted field.
78 cell.end = i
79 i = i + 1
80 break
81 }
82 i = i + 1
83 }
84 if i > n { return CSV_ERR_UNTERM }
85 } else {
86 // Non-escaped field: runs until ',' CR LF.
87 cell.start = i
88 cell.quoted = 0
89 while i < n {
90 if buf[i] == 0x2C { break } // ','
91 if buf[i] == 0x0A { break } // LF
92 if buf[i] == 0x0D { break } // CR
93 i = i + 1
94 }
95 cell.end = i
96 }
97 cell_count = cell_count + 1
98
99 // After the cell we expect: comma, or newline, or EOF.
100 if i < n {
101 if buf[i] == 0x2C {
102 i = i + 1
103 continue
104 }
105 }
106 // End of row. Record row-boundary + advance past line break.
107 row_count = row_count + 1
108 if row_count >= rows_cap { return CSV_ERR_OVERFLOW }
109 rows[row_count] = cell_count
110
111 if i < n {
112 if buf[i] == 0x0D {
113 i = i + 1
114 }
115 }
116 if i < n {
117 if buf[i] == 0x0A {
118 i = i + 1
119 }
120 }
121 }
122 // If file didn't end with newline, cell_count > rows[row_count]
123 // means there's a trailing row we haven't committed.
124 if cell_count > 0 {
125 if row_count == 0 {
126 row_count = 1
127 if rows_cap > 1 { rows[1] = cell_count }
128 }
129 if rows[row_count] != cell_count {
130 row_count = row_count + 1
131 if row_count >= rows_cap { return CSV_ERR_OVERFLOW }
132 rows[row_count] = cell_count
133 }
134 }
135 return row_count
136}
137
138// Decode a quoted cell's "" escapes into a destination buffer.
139// Returns number of bytes written or CSV_ERR_OVERFLOW.
140func csv_decode_quoted(buf: *u8, start: i64, end: i64,
141 out: *u8, cap: i64) -> i64 {
142 var i: i64 = start
143 var w: i64 = 0
144 while i < end {
145 if buf[i] == 0x22 {
146 if i + 1 < end {
147 if buf[i + 1] == 0x22 {
148 if w >= cap { return CSV_ERR_OVERFLOW }
149 out[w] = 0x22
150 w = w + 1
151 i = i + 2
152 continue
153 }
154 }
155 }
156 if w >= cap { return CSV_ERR_OVERFLOW }
157 out[w] = buf[i]
158 w = w + 1
159 i = i + 1
160 }
161 return w
162}
163
164// Compile-only smoke.
165func main() -> i64 {
166 let cells_raw: *u8 = sys_mmap(1024)
167 let cells: *CsvCell = cells_raw as *CsvCell
168 let rows: *i64 = sys_mmap(128) as *i64
169
170 // "a,b,c\r\n1,\"hel,lo\",3\n"
171 let input: *u8 = "a,b,c\r\n1,\"hel,lo\",3\n"
172 let n_rows: i64 = csv_parse(input, 20, cells, 32, rows, 16)
173 if n_rows != 2 { return 1 }
174
175 // Row 0: [a, b, c] -- cells 0..2.
176 if rows[0] != 0 { return 2 }
177 if rows[1] != 3 { return 3 }
178 // Row 1: [1, "hel,lo", 3] -- cells 3..5.
179 if rows[2] != 6 { return 4 }
180
181 // Verify cell[4] is quoted and "hel,lo" content.
182 let c4: *CsvCell = cells + 4 * 24
183 if c4.quoted != 1 { return 5 }
184 if input[c4.start] != 0x68 { return 6 } // 'h'
185 if c4.end - c4.start != 6 { return 7 } // "hel,lo"
186
187 // Decode a quoted cell with "" escape: "he""llo"
188 let q: *u8 = "he\"\"llo" // raw bytes inside the quoted field
189 let tmp: *u8 = sys_mmap(32)
190 let wlen: i64 = csv_decode_quoted(q, 0, 7, tmp, 32)
191 if wlen != 6 { return 8 } // he"llo = 6 bytes
192 if tmp[2] != 0x22 { return 9 }
193 return 0
194}