code wiki / (root) / csv.nx

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}