index.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. import {normalize_columns_array} from './normalize_columns_array.js';
  2. import {init_state} from './init_state.js';
  3. import {normalize_options} from './normalize_options.js';
  4. import {CsvError} from './CsvError.js';
  5. const isRecordEmpty = function(record){
  6. return record.every((field) => field == null || field.toString && field.toString().trim() === '');
  7. };
  8. const cr = 13; // `\r`, carriage return, 0x0D in hexadécimal, 13 in decimal
  9. const nl = 10; // `\n`, newline, 0x0A in hexadecimal, 10 in decimal
  10. const boms = {
  11. // Note, the following are equals:
  12. // Buffer.from("\ufeff")
  13. // Buffer.from([239, 187, 191])
  14. // Buffer.from('EFBBBF', 'hex')
  15. 'utf8': Buffer.from([239, 187, 191]),
  16. // Note, the following are equals:
  17. // Buffer.from "\ufeff", 'utf16le
  18. // Buffer.from([255, 254])
  19. 'utf16le': Buffer.from([255, 254])
  20. };
  21. const transform = function(original_options = {}) {
  22. const info = {
  23. bytes: 0,
  24. comment_lines: 0,
  25. empty_lines: 0,
  26. invalid_field_length: 0,
  27. lines: 1,
  28. records: 0
  29. };
  30. const options = normalize_options(original_options);
  31. return {
  32. info: info,
  33. original_options: original_options,
  34. options: options,
  35. state: init_state(options),
  36. __needMoreData: function(i, bufLen, end){
  37. if(end) return false;
  38. const {encoding, escape, quote} = this.options;
  39. const {quoting, needMoreDataSize, recordDelimiterMaxLength} = this.state;
  40. const numOfCharLeft = bufLen - i - 1;
  41. const requiredLength = Math.max(
  42. needMoreDataSize,
  43. // Skip if the remaining buffer smaller than record delimiter
  44. // If "record_delimiter" is yet to be discovered:
  45. // 1. It is equals to `[]` and "recordDelimiterMaxLength" equals `0`
  46. // 2. We set the length to windows line ending in the current encoding
  47. // Note, that encoding is known from user or bom discovery at that point
  48. // recordDelimiterMaxLength,
  49. recordDelimiterMaxLength === 0 ? Buffer.from('\r\n', encoding).length : recordDelimiterMaxLength,
  50. // Skip if remaining buffer can be an escaped quote
  51. quoting ? ((escape === null ? 0 : escape.length) + quote.length) : 0,
  52. // Skip if remaining buffer can be record delimiter following the closing quote
  53. quoting ? (quote.length + recordDelimiterMaxLength) : 0,
  54. );
  55. return numOfCharLeft < requiredLength;
  56. },
  57. // Central parser implementation
  58. parse: function(nextBuf, end, push, close){
  59. const {bom, comment_no_infix, encoding, from_line, ltrim, max_record_size,raw, relax_quotes, rtrim, skip_empty_lines, to, to_line} = this.options;
  60. let {comment, escape, quote, record_delimiter} = this.options;
  61. const {bomSkipped, previousBuf, rawBuffer, escapeIsQuote} = this.state;
  62. let buf;
  63. if(previousBuf === undefined){
  64. if(nextBuf === undefined){
  65. // Handle empty string
  66. close();
  67. return;
  68. }else{
  69. buf = nextBuf;
  70. }
  71. }else if(previousBuf !== undefined && nextBuf === undefined){
  72. buf = previousBuf;
  73. }else{
  74. buf = Buffer.concat([previousBuf, nextBuf]);
  75. }
  76. // Handle UTF BOM
  77. if(bomSkipped === false){
  78. if(bom === false){
  79. this.state.bomSkipped = true;
  80. }else if(buf.length < 3){
  81. // No enough data
  82. if(end === false){
  83. // Wait for more data
  84. this.state.previousBuf = buf;
  85. return;
  86. }
  87. }else{
  88. for(const encoding in boms){
  89. if(boms[encoding].compare(buf, 0, boms[encoding].length) === 0){
  90. // Skip BOM
  91. const bomLength = boms[encoding].length;
  92. this.state.bufBytesStart += bomLength;
  93. buf = buf.slice(bomLength);
  94. // Renormalize original options with the new encoding
  95. this.options = normalize_options({...this.original_options, encoding: encoding});
  96. // Options will re-evaluate the Buffer with the new encoding
  97. ({comment, escape, quote } = this.options);
  98. break;
  99. }
  100. }
  101. this.state.bomSkipped = true;
  102. }
  103. }
  104. const bufLen = buf.length;
  105. let pos;
  106. for(pos = 0; pos < bufLen; pos++){
  107. // Ensure we get enough space to look ahead
  108. // There should be a way to move this out of the loop
  109. if(this.__needMoreData(pos, bufLen, end)){
  110. break;
  111. }
  112. if(this.state.wasRowDelimiter === true){
  113. this.info.lines++;
  114. this.state.wasRowDelimiter = false;
  115. }
  116. if(to_line !== -1 && this.info.lines > to_line){
  117. this.state.stop = true;
  118. close();
  119. return;
  120. }
  121. // Auto discovery of record_delimiter, unix, mac and windows supported
  122. if(this.state.quoting === false && record_delimiter.length === 0){
  123. const record_delimiterCount = this.__autoDiscoverRecordDelimiter(buf, pos);
  124. if(record_delimiterCount){
  125. record_delimiter = this.options.record_delimiter;
  126. }
  127. }
  128. const chr = buf[pos];
  129. if(raw === true){
  130. rawBuffer.append(chr);
  131. }
  132. if((chr === cr || chr === nl) && this.state.wasRowDelimiter === false){
  133. this.state.wasRowDelimiter = true;
  134. }
  135. // Previous char was a valid escape char
  136. // treat the current char as a regular char
  137. if(this.state.escaping === true){
  138. this.state.escaping = false;
  139. }else{
  140. // Escape is only active inside quoted fields
  141. // We are quoting, the char is an escape chr and there is a chr to escape
  142. // if(escape !== null && this.state.quoting === true && chr === escape && pos + 1 < bufLen){
  143. if(escape !== null && this.state.quoting === true && this.__isEscape(buf, pos, chr) && pos + escape.length < bufLen){
  144. if(escapeIsQuote){
  145. if(this.__isQuote(buf, pos+escape.length)){
  146. this.state.escaping = true;
  147. pos += escape.length - 1;
  148. continue;
  149. }
  150. }else{
  151. this.state.escaping = true;
  152. pos += escape.length - 1;
  153. continue;
  154. }
  155. }
  156. // Not currently escaping and chr is a quote
  157. // TODO: need to compare bytes instead of single char
  158. if(this.state.commenting === false && this.__isQuote(buf, pos)){
  159. if(this.state.quoting === true){
  160. const nextChr = buf[pos+quote.length];
  161. const isNextChrTrimable = rtrim && this.__isCharTrimable(buf, pos+quote.length);
  162. const isNextChrComment = comment !== null && this.__compareBytes(comment, buf, pos+quote.length, nextChr);
  163. const isNextChrDelimiter = this.__isDelimiter(buf, pos+quote.length, nextChr);
  164. const isNextChrRecordDelimiter = record_delimiter.length === 0 ? this.__autoDiscoverRecordDelimiter(buf, pos+quote.length) : this.__isRecordDelimiter(nextChr, buf, pos+quote.length);
  165. // Escape a quote
  166. // Treat next char as a regular character
  167. if(escape !== null && this.__isEscape(buf, pos, chr) && this.__isQuote(buf, pos + escape.length)){
  168. pos += escape.length - 1;
  169. }else if(!nextChr || isNextChrDelimiter || isNextChrRecordDelimiter || isNextChrComment || isNextChrTrimable){
  170. this.state.quoting = false;
  171. this.state.wasQuoting = true;
  172. pos += quote.length - 1;
  173. continue;
  174. }else if(relax_quotes === false){
  175. const err = this.__error(
  176. new CsvError('CSV_INVALID_CLOSING_QUOTE', [
  177. 'Invalid Closing Quote:',
  178. `got "${String.fromCharCode(nextChr)}"`,
  179. `at line ${this.info.lines}`,
  180. 'instead of delimiter, record delimiter, trimable character',
  181. '(if activated) or comment',
  182. ], this.options, this.__infoField())
  183. );
  184. if(err !== undefined) return err;
  185. }else{
  186. this.state.quoting = false;
  187. this.state.wasQuoting = true;
  188. this.state.field.prepend(quote);
  189. pos += quote.length - 1;
  190. }
  191. }else{
  192. if(this.state.field.length !== 0){
  193. // In relax_quotes mode, treat opening quote preceded by chrs as regular
  194. if(relax_quotes === false){
  195. const info = this.__infoField();
  196. const bom = Object.keys(boms).map(b => boms[b].equals(this.state.field.toString()) ? b : false).filter(Boolean)[0];
  197. const err = this.__error(
  198. new CsvError('INVALID_OPENING_QUOTE', [
  199. 'Invalid Opening Quote:',
  200. `a quote is found on field ${JSON.stringify(info.column)} at line ${info.lines}, value is ${JSON.stringify(this.state.field.toString(encoding))}`,
  201. bom ? `(${bom} bom)` : undefined
  202. ], this.options, info, {
  203. field: this.state.field,
  204. })
  205. );
  206. if(err !== undefined) return err;
  207. }
  208. }else{
  209. this.state.quoting = true;
  210. pos += quote.length - 1;
  211. continue;
  212. }
  213. }
  214. }
  215. if(this.state.quoting === false){
  216. const recordDelimiterLength = this.__isRecordDelimiter(chr, buf, pos);
  217. if(recordDelimiterLength !== 0){
  218. // Do not emit comments which take a full line
  219. const skipCommentLine = this.state.commenting && (this.state.wasQuoting === false && this.state.record.length === 0 && this.state.field.length === 0);
  220. if(skipCommentLine){
  221. this.info.comment_lines++;
  222. // Skip full comment line
  223. }else{
  224. // Activate records emition if above from_line
  225. if(this.state.enabled === false && this.info.lines + (this.state.wasRowDelimiter === true ? 1: 0) >= from_line){
  226. this.state.enabled = true;
  227. this.__resetField();
  228. this.__resetRecord();
  229. pos += recordDelimiterLength - 1;
  230. continue;
  231. }
  232. // Skip if line is empty and skip_empty_lines activated
  233. if(skip_empty_lines === true && this.state.wasQuoting === false && this.state.record.length === 0 && this.state.field.length === 0){
  234. this.info.empty_lines++;
  235. pos += recordDelimiterLength - 1;
  236. continue;
  237. }
  238. this.info.bytes = this.state.bufBytesStart + pos;
  239. const errField = this.__onField();
  240. if(errField !== undefined) return errField;
  241. this.info.bytes = this.state.bufBytesStart + pos + recordDelimiterLength;
  242. const errRecord = this.__onRecord(push);
  243. if(errRecord !== undefined) return errRecord;
  244. if(to !== -1 && this.info.records >= to){
  245. this.state.stop = true;
  246. close();
  247. return;
  248. }
  249. }
  250. this.state.commenting = false;
  251. pos += recordDelimiterLength - 1;
  252. continue;
  253. }
  254. if(this.state.commenting){
  255. continue;
  256. }
  257. const commentCount = comment === null ? 0 : this.__compareBytes(comment, buf, pos, chr);
  258. if(commentCount !== 0 && (comment_no_infix === false || this.state.field.length === 0)){
  259. this.state.commenting = true;
  260. continue;
  261. }
  262. const delimiterLength = this.__isDelimiter(buf, pos, chr);
  263. if(delimiterLength !== 0){
  264. this.info.bytes = this.state.bufBytesStart + pos;
  265. const errField = this.__onField();
  266. if(errField !== undefined) return errField;
  267. pos += delimiterLength - 1;
  268. continue;
  269. }
  270. }
  271. }
  272. if(this.state.commenting === false){
  273. if(max_record_size !== 0 && this.state.record_length + this.state.field.length > max_record_size){
  274. return this.__error(
  275. new CsvError('CSV_MAX_RECORD_SIZE', [
  276. 'Max Record Size:',
  277. 'record exceed the maximum number of tolerated bytes',
  278. `of ${max_record_size}`,
  279. `at line ${this.info.lines}`,
  280. ], this.options, this.__infoField())
  281. );
  282. }
  283. }
  284. const lappend = ltrim === false || this.state.quoting === true || this.state.field.length !== 0 || !this.__isCharTrimable(buf, pos);
  285. // rtrim in non quoting is handle in __onField
  286. const rappend = rtrim === false || this.state.wasQuoting === false;
  287. if(lappend === true && rappend === true){
  288. this.state.field.append(chr);
  289. }else if(rtrim === true && !this.__isCharTrimable(buf, pos)){
  290. return this.__error(
  291. new CsvError('CSV_NON_TRIMABLE_CHAR_AFTER_CLOSING_QUOTE', [
  292. 'Invalid Closing Quote:',
  293. 'found non trimable byte after quote',
  294. `at line ${this.info.lines}`,
  295. ], this.options, this.__infoField())
  296. );
  297. }else{
  298. if(lappend === false){
  299. pos += this.__isCharTrimable(buf, pos) - 1;
  300. }
  301. continue;
  302. }
  303. }
  304. if(end === true){
  305. // Ensure we are not ending in a quoting state
  306. if(this.state.quoting === true){
  307. const err = this.__error(
  308. new CsvError('CSV_QUOTE_NOT_CLOSED', [
  309. 'Quote Not Closed:',
  310. `the parsing is finished with an opening quote at line ${this.info.lines}`,
  311. ], this.options, this.__infoField())
  312. );
  313. if(err !== undefined) return err;
  314. }else{
  315. // Skip last line if it has no characters
  316. if(this.state.wasQuoting === true || this.state.record.length !== 0 || this.state.field.length !== 0){
  317. this.info.bytes = this.state.bufBytesStart + pos;
  318. const errField = this.__onField();
  319. if(errField !== undefined) return errField;
  320. const errRecord = this.__onRecord(push);
  321. if(errRecord !== undefined) return errRecord;
  322. }else if(this.state.wasRowDelimiter === true){
  323. this.info.empty_lines++;
  324. }else if(this.state.commenting === true){
  325. this.info.comment_lines++;
  326. }
  327. }
  328. }else{
  329. this.state.bufBytesStart += pos;
  330. this.state.previousBuf = buf.slice(pos);
  331. }
  332. if(this.state.wasRowDelimiter === true){
  333. this.info.lines++;
  334. this.state.wasRowDelimiter = false;
  335. }
  336. },
  337. __onRecord: function(push){
  338. const {columns, group_columns_by_name, encoding, info, from, relax_column_count, relax_column_count_less, relax_column_count_more, raw, skip_records_with_empty_values} = this.options;
  339. const {enabled, record} = this.state;
  340. if(enabled === false){
  341. return this.__resetRecord();
  342. }
  343. // Convert the first line into column names
  344. const recordLength = record.length;
  345. if(columns === true){
  346. if(skip_records_with_empty_values === true && isRecordEmpty(record)){
  347. this.__resetRecord();
  348. return;
  349. }
  350. return this.__firstLineToColumns(record);
  351. }
  352. if(columns === false && this.info.records === 0){
  353. this.state.expectedRecordLength = recordLength;
  354. }
  355. if(recordLength !== this.state.expectedRecordLength){
  356. const err = columns === false ?
  357. new CsvError('CSV_RECORD_INCONSISTENT_FIELDS_LENGTH', [
  358. 'Invalid Record Length:',
  359. `expect ${this.state.expectedRecordLength},`,
  360. `got ${recordLength} on line ${this.info.lines}`,
  361. ], this.options, this.__infoField(), {
  362. record: record,
  363. })
  364. :
  365. new CsvError('CSV_RECORD_INCONSISTENT_COLUMNS', [
  366. 'Invalid Record Length:',
  367. `columns length is ${columns.length},`, // rename columns
  368. `got ${recordLength} on line ${this.info.lines}`,
  369. ], this.options, this.__infoField(), {
  370. record: record,
  371. });
  372. if(relax_column_count === true ||
  373. (relax_column_count_less === true && recordLength < this.state.expectedRecordLength) ||
  374. (relax_column_count_more === true && recordLength > this.state.expectedRecordLength)){
  375. this.info.invalid_field_length++;
  376. this.state.error = err;
  377. // Error is undefined with skip_records_with_error
  378. }else{
  379. const finalErr = this.__error(err);
  380. if(finalErr) return finalErr;
  381. }
  382. }
  383. if(skip_records_with_empty_values === true && isRecordEmpty(record)){
  384. this.__resetRecord();
  385. return;
  386. }
  387. if(this.state.recordHasError === true){
  388. this.__resetRecord();
  389. this.state.recordHasError = false;
  390. return;
  391. }
  392. this.info.records++;
  393. if(from === 1 || this.info.records >= from){
  394. const {objname} = this.options;
  395. // With columns, records are object
  396. if(columns !== false){
  397. const obj = {};
  398. // Transform record array to an object
  399. for(let i = 0, l = record.length; i < l; i++){
  400. if(columns[i] === undefined || columns[i].disabled) continue;
  401. // Turn duplicate columns into an array
  402. if (group_columns_by_name === true && obj[columns[i].name] !== undefined) {
  403. if (Array.isArray(obj[columns[i].name])) {
  404. obj[columns[i].name] = obj[columns[i].name].concat(record[i]);
  405. } else {
  406. obj[columns[i].name] = [obj[columns[i].name], record[i]];
  407. }
  408. } else {
  409. obj[columns[i].name] = record[i];
  410. }
  411. }
  412. // Without objname (default)
  413. if(raw === true || info === true){
  414. const extRecord = Object.assign(
  415. {record: obj},
  416. (raw === true ? {raw: this.state.rawBuffer.toString(encoding)}: {}),
  417. (info === true ? {info: this.__infoRecord()}: {})
  418. );
  419. const err = this.__push(
  420. objname === undefined ? extRecord : [obj[objname], extRecord]
  421. , push);
  422. if(err){
  423. return err;
  424. }
  425. }else{
  426. const err = this.__push(
  427. objname === undefined ? obj : [obj[objname], obj]
  428. , push);
  429. if(err){
  430. return err;
  431. }
  432. }
  433. // Without columns, records are array
  434. }else{
  435. if(raw === true || info === true){
  436. const extRecord = Object.assign(
  437. {record: record},
  438. raw === true ? {raw: this.state.rawBuffer.toString(encoding)}: {},
  439. info === true ? {info: this.__infoRecord()}: {}
  440. );
  441. const err = this.__push(
  442. objname === undefined ? extRecord : [record[objname], extRecord]
  443. , push);
  444. if(err){
  445. return err;
  446. }
  447. }else{
  448. const err = this.__push(
  449. objname === undefined ? record : [record[objname], record]
  450. , push);
  451. if(err){
  452. return err;
  453. }
  454. }
  455. }
  456. }
  457. this.__resetRecord();
  458. },
  459. __firstLineToColumns: function(record){
  460. const {firstLineToHeaders} = this.state;
  461. try{
  462. const headers = firstLineToHeaders === undefined ? record : firstLineToHeaders.call(null, record);
  463. if(!Array.isArray(headers)){
  464. return this.__error(
  465. new CsvError('CSV_INVALID_COLUMN_MAPPING', [
  466. 'Invalid Column Mapping:',
  467. 'expect an array from column function,',
  468. `got ${JSON.stringify(headers)}`
  469. ], this.options, this.__infoField(), {
  470. headers: headers,
  471. })
  472. );
  473. }
  474. const normalizedHeaders = normalize_columns_array(headers);
  475. this.state.expectedRecordLength = normalizedHeaders.length;
  476. this.options.columns = normalizedHeaders;
  477. this.__resetRecord();
  478. return;
  479. }catch(err){
  480. return err;
  481. }
  482. },
  483. __resetRecord: function(){
  484. if(this.options.raw === true){
  485. this.state.rawBuffer.reset();
  486. }
  487. this.state.error = undefined;
  488. this.state.record = [];
  489. this.state.record_length = 0;
  490. },
  491. __onField: function(){
  492. const {cast, encoding, rtrim, max_record_size} = this.options;
  493. const {enabled, wasQuoting} = this.state;
  494. // Short circuit for the from_line options
  495. if(enabled === false){
  496. return this.__resetField();
  497. }
  498. let field = this.state.field.toString(encoding);
  499. if(rtrim === true && wasQuoting === false){
  500. field = field.trimRight();
  501. }
  502. if(cast === true){
  503. const [err, f] = this.__cast(field);
  504. if(err !== undefined) return err;
  505. field = f;
  506. }
  507. this.state.record.push(field);
  508. // Increment record length if record size must not exceed a limit
  509. if(max_record_size !== 0 && typeof field === 'string'){
  510. this.state.record_length += field.length;
  511. }
  512. this.__resetField();
  513. },
  514. __resetField: function(){
  515. this.state.field.reset();
  516. this.state.wasQuoting = false;
  517. },
  518. __push: function(record, push){
  519. const {on_record} = this.options;
  520. if(on_record !== undefined){
  521. const info = this.__infoRecord();
  522. try{
  523. record = on_record.call(null, record, info);
  524. }catch(err){
  525. return err;
  526. }
  527. if(record === undefined || record === null){ return; }
  528. }
  529. push(record);
  530. },
  531. // Return a tuple with the error and the casted value
  532. __cast: function(field){
  533. const {columns, relax_column_count} = this.options;
  534. const isColumns = Array.isArray(columns);
  535. // Dont loose time calling cast
  536. // because the final record is an object
  537. // and this field can't be associated to a key present in columns
  538. if(isColumns === true && relax_column_count && this.options.columns.length <= this.state.record.length){
  539. return [undefined, undefined];
  540. }
  541. if(this.state.castField !== null){
  542. try{
  543. const info = this.__infoField();
  544. return [undefined, this.state.castField.call(null, field, info)];
  545. }catch(err){
  546. return [err];
  547. }
  548. }
  549. if(this.__isFloat(field)){
  550. return [undefined, parseFloat(field)];
  551. }else if(this.options.cast_date !== false){
  552. const info = this.__infoField();
  553. return [undefined, this.options.cast_date.call(null, field, info)];
  554. }
  555. return [undefined, field];
  556. },
  557. // Helper to test if a character is a space or a line delimiter
  558. __isCharTrimable: function(buf, pos){
  559. const isTrim = (buf, pos) => {
  560. const {timchars} = this.state;
  561. loop1: for(let i = 0; i < timchars.length; i++){
  562. const timchar = timchars[i];
  563. for(let j = 0; j < timchar.length; j++){
  564. if(timchar[j] !== buf[pos+j]) continue loop1;
  565. }
  566. return timchar.length;
  567. }
  568. return 0;
  569. };
  570. return isTrim(buf, pos);
  571. },
  572. // Keep it in case we implement the `cast_int` option
  573. // __isInt(value){
  574. // // return Number.isInteger(parseInt(value))
  575. // // return !isNaN( parseInt( obj ) );
  576. // return /^(\-|\+)?[1-9][0-9]*$/.test(value)
  577. // }
  578. __isFloat: function(value){
  579. return (value - parseFloat(value) + 1) >= 0; // Borrowed from jquery
  580. },
  581. __compareBytes: function(sourceBuf, targetBuf, targetPos, firstByte){
  582. if(sourceBuf[0] !== firstByte) return 0;
  583. const sourceLength = sourceBuf.length;
  584. for(let i = 1; i < sourceLength; i++){
  585. if(sourceBuf[i] !== targetBuf[targetPos+i]) return 0;
  586. }
  587. return sourceLength;
  588. },
  589. __isDelimiter: function(buf, pos, chr){
  590. const {delimiter, ignore_last_delimiters} = this.options;
  591. if(ignore_last_delimiters === true && this.state.record.length === this.options.columns.length - 1){
  592. return 0;
  593. }else if(ignore_last_delimiters !== false && typeof ignore_last_delimiters === 'number' && this.state.record.length === ignore_last_delimiters - 1){
  594. return 0;
  595. }
  596. loop1: for(let i = 0; i < delimiter.length; i++){
  597. const del = delimiter[i];
  598. if(del[0] === chr){
  599. for(let j = 1; j < del.length; j++){
  600. if(del[j] !== buf[pos+j]) continue loop1;
  601. }
  602. return del.length;
  603. }
  604. }
  605. return 0;
  606. },
  607. __isRecordDelimiter: function(chr, buf, pos){
  608. const {record_delimiter} = this.options;
  609. const recordDelimiterLength = record_delimiter.length;
  610. loop1: for(let i = 0; i < recordDelimiterLength; i++){
  611. const rd = record_delimiter[i];
  612. const rdLength = rd.length;
  613. if(rd[0] !== chr){
  614. continue;
  615. }
  616. for(let j = 1; j < rdLength; j++){
  617. if(rd[j] !== buf[pos+j]){
  618. continue loop1;
  619. }
  620. }
  621. return rd.length;
  622. }
  623. return 0;
  624. },
  625. __isEscape: function(buf, pos, chr){
  626. const {escape} = this.options;
  627. if(escape === null) return false;
  628. const l = escape.length;
  629. if(escape[0] === chr){
  630. for(let i = 0; i < l; i++){
  631. if(escape[i] !== buf[pos+i]){
  632. return false;
  633. }
  634. }
  635. return true;
  636. }
  637. return false;
  638. },
  639. __isQuote: function(buf, pos){
  640. const {quote} = this.options;
  641. if(quote === null) return false;
  642. const l = quote.length;
  643. for(let i = 0; i < l; i++){
  644. if(quote[i] !== buf[pos+i]){
  645. return false;
  646. }
  647. }
  648. return true;
  649. },
  650. __autoDiscoverRecordDelimiter: function(buf, pos){
  651. const { encoding } = this.options;
  652. // Note, we don't need to cache this information in state,
  653. // It is only called on the first line until we find out a suitable
  654. // record delimiter.
  655. const rds = [
  656. // Important, the windows line ending must be before mac os 9
  657. Buffer.from('\r\n', encoding),
  658. Buffer.from('\n', encoding),
  659. Buffer.from('\r', encoding),
  660. ];
  661. loop: for(let i = 0; i < rds.length; i++){
  662. const l = rds[i].length;
  663. for(let j = 0; j < l; j++){
  664. if(rds[i][j] !== buf[pos + j]){
  665. continue loop;
  666. }
  667. }
  668. this.options.record_delimiter.push(rds[i]);
  669. this.state.recordDelimiterMaxLength = rds[i].length;
  670. return rds[i].length;
  671. }
  672. return 0;
  673. },
  674. __error: function(msg){
  675. const {encoding, raw, skip_records_with_error} = this.options;
  676. const err = typeof msg === 'string' ? new Error(msg) : msg;
  677. if(skip_records_with_error){
  678. this.state.recordHasError = true;
  679. if(this.options.on_skip !== undefined){
  680. this.options.on_skip(err, raw ? this.state.rawBuffer.toString(encoding) : undefined);
  681. }
  682. // this.emit('skip', err, raw ? this.state.rawBuffer.toString(encoding) : undefined);
  683. return undefined;
  684. }else{
  685. return err;
  686. }
  687. },
  688. __infoDataSet: function(){
  689. return {
  690. ...this.info,
  691. columns: this.options.columns
  692. };
  693. },
  694. __infoRecord: function(){
  695. const {columns, raw, encoding} = this.options;
  696. return {
  697. ...this.__infoDataSet(),
  698. error: this.state.error,
  699. header: columns === true,
  700. index: this.state.record.length,
  701. raw: raw ? this.state.rawBuffer.toString(encoding) : undefined
  702. };
  703. },
  704. __infoField: function(){
  705. const {columns} = this.options;
  706. const isColumns = Array.isArray(columns);
  707. return {
  708. ...this.__infoRecord(),
  709. column: isColumns === true ?
  710. (columns.length > this.state.record.length ?
  711. columns[this.state.record.length].name :
  712. null
  713. ) :
  714. this.state.record.length,
  715. quoting: this.state.wasQuoting,
  716. };
  717. }
  718. };
  719. };
  720. export {transform, CsvError};