nx_csv_writer.nx source
↩ module page · 150 lines · 4849 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
29// nx_safety_envelope:
30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
31// sil_target: SIL1
32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
33// verdict: NOT_YET_EVALUATED
34
35import "nx_syscalls.nx"
36
37const CW_ERR_SHORT: i64 = -1
38
39struct CsvWriter {
40 // 1 if the next cell written should be preceded by a comma,
41 // 0 for first cell in a row.
42 in_row: i64,
43}
44
45func csvw_init(w: *CsvWriter) -> i64 {
46 w.in_row = 0
47 return 0
48}
49
50// Does this cell need quoting? Quote if any of ',', '\"', LF, CR.
51func cw_needs_quoting(data: *u8, n: i64) -> i64 {
52 var i: i64 = 0
53 while i < n {
54 let b: i64 = data[i]
55 if b == 0x2C { return 1 } // ','
56 if b == 0x22 { return 1 } // '\"'
57 if b == 0x0A { return 1 } // LF
58 if b == 0x0D { return 1 } // CR
59 i = i + 1
60 }
61 return 0
62}
63
64// Write one cell. Inserts ',' separator if mid-row. Returns
65// new offset.
66func csvw_field(out: *u8, cap: i64, off: i64, w: *CsvWriter,
67 data: *u8, n: i64) -> i64 {
68 var cur: i64 = off
69 if w.in_row == 1 {
70 if cur >= cap { return CW_ERR_SHORT }
71 out[cur] = 0x2C
72 cur = cur + 1
73 }
74 if cw_needs_quoting(data, n) == 0 {
75 if cur + n > cap { return CW_ERR_SHORT }
76 var i: i64 = 0
77 while i < n {
78 out[cur + i] = data[i]
79 i = i + 1
80 }
81 cur = cur + n
82 } else {
83 // Quoted, with \" doubled.
84 if cur >= cap { return CW_ERR_SHORT }
85 out[cur] = 0x22
86 cur = cur + 1
87 var i: i64 = 0
88 while i < n {
89 if cur >= cap { return CW_ERR_SHORT }
90 out[cur] = data[i]
91 cur = cur + 1
92 if data[i] == 0x22 {
93 if cur >= cap { return CW_ERR_SHORT }
94 out[cur] = 0x22 // second quote
95 cur = cur + 1
96 }
97 i = i + 1
98 }
99 if cur >= cap { return CW_ERR_SHORT }
100 out[cur] = 0x22
101 cur = cur + 1
102 }
103 w.in_row = 1
104 return cur
105}
106
107// End-of-row: writes CRLF and resets in_row.
108func csvw_end_row(out: *u8, cap: i64, off: i64, w: *CsvWriter) -> i64 {
109 if off + 2 > cap { return CW_ERR_SHORT }
110 out[off] = 0x0D
111 out[off + 1] = 0x0A
112 w.in_row = 0
113 return off + 2
114}
115
116// Compile-only smoke.
117func main() -> i64 {
118 let out: *u8 = sys_mmap(256)
119 let w_raw: *u8 = sys_mmap(16)
120 let w: *CsvWriter = w_raw as *CsvWriter
121 csvw_init(w)
122 var off: i64 = 0
123
124 off = csvw_field(out, 256, off, w, "a", 1)
125 off = csvw_field(out, 256, off, w, "b", 1)
126 off = csvw_field(out, 256, off, w, "hi,lo", 5) // needs quote
127 off = csvw_end_row(out, 256, off, w)
128
129 off = csvw_field(out, 256, off, w, "c", 1)
130 off = csvw_field(out, 256, off, w, "say \"hi\"", 8) // embedded \"
131 off = csvw_end_row(out, 256, off, w)
132
133 // Expected:
134 // a,b,\"hi,lo\"\\r\\n
135 // c,\"say \"\"hi\"\"\"\\r\\n
136 // Length: 3 + 7 + 2 = 12, then 2 + 12 + 2 = 16. Total 28.
137 if off != 28 { return 1 }
138 if out[0] != 0x61 { return 2 } // 'a'
139 if out[1] != 0x2C { return 3 } // ','
140 if out[2] != 0x62 { return 4 } // 'b'
141 if out[3] != 0x2C { return 5 }
142 // \"hi,lo\" quoted.
143 if out[4] != 0x22 { return 6 } // opening '\"'
144 if out[5] != 0x68 { return 7 } // 'h'
145 // CRLF after first row.
146 if out[10] != 0x22 { return 8 } // closing '\"'
147 if out[11] != 0x0D { return 9 } // CR
148 if out[12] != 0x0A { return 10 } // LF
149 return 0
150}