code wiki / (root) / csv_writer.nx

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}