code wiki / (root) / nx_csv_writer.nx

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}