csv_writer.nx source
↩ module page · 144 lines · 4739 B
1// csv_writer.nx -- RFC 4180 CSV serialiser.
2//
3// Companion to csv.nx (reader). Writes cells with quoting that
4// csv.nx can roundtrip:
5// - Cells without any of \",\\n\\r,\" are emitted raw
6// - Cells containing any of those are wrapped in \"...\" with
7// embedded \" doubled to \"\"
8// - Lines end with CRLF per RFC (not bare LF)
9//
10// Stream API:
11// csvw_field(out, cap, off, data, n) -> off' (writes cell)
12// csvw_end_row(out, cap, off) -> off' (writes CRLF)
13//
14// Caller tracks \"is this the first cell on the line\" by comparing
15// the returned offset to the offset before writing; csvw_field
16// inserts a \",\" separator when writing into the middle of a row.
17// To simplify, we expose csvw_begin_row which resets the comma-
18// separator flag stored on a CsvWriter state struct.
19//
20// Invariants:
21// CW1 Roundtrip safe: csv.nx parsing our output yields the
22// original cell bytes (after csv_decode_quoted for quoted
23// cells).
24// CW2 Empty string fields are OK -- they write as a bare
25// \",\" boundary (or \"\"\"\" at end of row for unambiguous
26// parsing per RFC 4180 ยง2.4).
27// CW3 Output uses CRLF (0x0D 0x0A) line endings.
28
29import "syscalls.nx"
30
31const CW_ERR_SHORT: i64 = -1
32
33struct CsvWriter {
34 // 1 if the next cell written should be preceded by a comma,
35 // 0 for first cell in a row.
36 in_row: i64,
37}
38
39func csvw_init(w: *CsvWriter) -> i64 {
40 w.in_row = 0
41 return 0
42}
43
44// Does this cell need quoting? Quote if any of ',', '\"', LF, CR.
45func cw_needs_quoting(data: *u8, n: i64) -> i64 {
46 var i: i64 = 0
47 while i < n {
48 let b: i64 = data[i]
49 if b == 0x2C { return 1 } // ','
50 if b == 0x22 { return 1 } // '\"'
51 if b == 0x0A { return 1 } // LF
52 if b == 0x0D { return 1 } // CR
53 i = i + 1
54 }
55 return 0
56}
57
58// Write one cell. Inserts ',' separator if mid-row. Returns
59// new offset.
60func csvw_field(out: *u8, cap: i64, off: i64, w: *CsvWriter,
61 data: *u8, n: i64) -> i64 {
62 var cur: i64 = off
63 if w.in_row == 1 {
64 if cur >= cap { return CW_ERR_SHORT }
65 out[cur] = 0x2C
66 cur = cur + 1
67 }
68 if cw_needs_quoting(data, n) == 0 {
69 if cur + n > cap { return CW_ERR_SHORT }
70 var i: i64 = 0
71 while i < n {
72 out[cur + i] = data[i]
73 i = i + 1
74 }
75 cur = cur + n
76 } else {
77 // Quoted, with \" doubled.
78 if cur >= cap { return CW_ERR_SHORT }
79 out[cur] = 0x22
80 cur = cur + 1
81 var i: i64 = 0
82 while i < n {
83 if cur >= cap { return CW_ERR_SHORT }
84 out[cur] = data[i]
85 cur = cur + 1
86 if data[i] == 0x22 {
87 if cur >= cap { return CW_ERR_SHORT }
88 out[cur] = 0x22 // second quote
89 cur = cur + 1
90 }
91 i = i + 1
92 }
93 if cur >= cap { return CW_ERR_SHORT }
94 out[cur] = 0x22
95 cur = cur + 1
96 }
97 w.in_row = 1
98 return cur
99}
100
101// End-of-row: writes CRLF and resets in_row.
102func csvw_end_row(out: *u8, cap: i64, off: i64, w: *CsvWriter) -> i64 {
103 if off + 2 > cap { return CW_ERR_SHORT }
104 out[off] = 0x0D
105 out[off + 1] = 0x0A
106 w.in_row = 0
107 return off + 2
108}
109
110// Compile-only smoke.
111func main() -> i64 {
112 let out: *u8 = sys_mmap(256)
113 let w_raw: *u8 = sys_mmap(16)
114 let w: *CsvWriter = w_raw as *CsvWriter
115 csvw_init(w)
116 var off: i64 = 0
117
118 off = csvw_field(out, 256, off, w, "a", 1)
119 off = csvw_field(out, 256, off, w, "b", 1)
120 off = csvw_field(out, 256, off, w, "hi,lo", 5) // needs quote
121 off = csvw_end_row(out, 256, off, w)
122
123 off = csvw_field(out, 256, off, w, "c", 1)
124 off = csvw_field(out, 256, off, w, "say \"hi\"", 8) // embedded \"
125 off = csvw_end_row(out, 256, off, w)
126
127 // Expected:
128 // a,b,\"hi,lo\"\\r\\n
129 // c,\"say \"\"hi\"\"\"\\r\\n
130 // Length: 3 + 7 + 2 = 12, then 2 + 12 + 2 = 16. Total 28.
131 if off != 28 { return 1 }
132 if out[0] != 0x61 { return 2 } // 'a'
133 if out[1] != 0x2C { return 3 } // ','
134 if out[2] != 0x62 { return 4 } // 'b'
135 if out[3] != 0x2C { return 5 }
136 // \"hi,lo\" quoted.
137 if out[4] != 0x22 { return 6 } // opening '\"'
138 if out[5] != 0x68 { return 7 } // 'h'
139 // CRLF after first row.
140 if out[10] != 0x22 { return 8 } // closing '\"'
141 if out[11] != 0x0D { return 9 } // CR
142 if out[12] != 0x0A { return 10 } // LF
143 return 0
144}