web-csv-toolbox 0.13.1-next-afac98bd3a41b6e902268ac4ca6a99a8da883c81 → 0.14.0-next-120af8840dd0795dac94d59e32713b96a34e2a41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -9
- package/dist/CSVRecordAssembler.js +14 -0
- package/dist/CSVRecordAssembler.js.map +1 -1
- package/dist/_virtual/web_csv_toolbox_wasm_bg.wasm.js +1 -1
- package/dist/common/types.d.ts +1 -1
- package/dist/getOptionsFromBlob.d.ts +10 -0
- package/dist/getOptionsFromBlob.js +18 -0
- package/dist/getOptionsFromBlob.js.map +1 -0
- package/dist/getOptionsFromRequest.d.ts +11 -0
- package/dist/getOptionsFromRequest.js +41 -0
- package/dist/getOptionsFromRequest.js.map +1 -0
- package/dist/parse.d.ts +4 -2
- package/dist/parse.js +11 -29
- package/dist/parse.js.map +1 -1
- package/dist/parseBlob.d.ts +102 -0
- package/dist/parseBlob.js +31 -0
- package/dist/parseBlob.js.map +1 -0
- package/dist/parseBlobToStream.d.ts +11 -0
- package/dist/parseBlobToStream.js +10 -0
- package/dist/parseBlobToStream.js.map +1 -0
- package/dist/parseFile.d.ts +93 -0
- package/dist/parseFile.js +20 -0
- package/dist/parseFile.js.map +1 -0
- package/dist/parseRequest.d.ts +120 -0
- package/dist/parseRequest.js +37 -0
- package/dist/parseRequest.js.map +1 -0
- package/dist/parseRequestToStream.d.ts +11 -0
- package/dist/parseRequestToStream.js +16 -0
- package/dist/parseRequestToStream.js.map +1 -0
- package/dist/parseResponse.js +1 -6
- package/dist/parseResponse.js.map +1 -1
- package/dist/web-csv-toolbox.d.ts +3 -0
- package/dist/web-csv-toolbox.js +3 -0
- package/dist/web-csv-toolbox.js.map +1 -1
- package/dist/web_csv_toolbox_wasm_bg.wasm +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,13 +56,13 @@ A CSV Toolbox utilizing Web Standard APIs.
|
|
|
56
56
|
- ⏳ Use [`AbortSignal.timeout`](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static) to automatically cancel operations that exceed a specified time limit.
|
|
57
57
|
- 🛡️ **Memory Safety Protection**: Built-in limits prevent memory exhaustion attacks.
|
|
58
58
|
- 🔒 Configurable maximum buffer size (default: 10M characters) to prevent DoS attacks via unbounded input.
|
|
59
|
-
|
|
59
|
+
- 🚨 Throws `RangeError` when buffer exceeds the limit.
|
|
60
60
|
- 📊 Configurable maximum field count (default: 100,000 fields/record) to prevent excessive column attacks.
|
|
61
|
-
|
|
61
|
+
- ⚠️ Throws `RangeError` when field count exceeds the limit.
|
|
62
62
|
- 💾 Configurable maximum binary size (default: 100MB bytes) for ArrayBuffer/Uint8Array inputs.
|
|
63
|
-
|
|
63
|
+
- 🛑 Throws `RangeError` when binary size exceeds the limit.
|
|
64
64
|
- 🎨 **Flexible Source Support**
|
|
65
|
-
- 🧩 Parse CSVs directly from `string`s, `ReadableStream`s, or `
|
|
65
|
+
- 🧩 Parse CSVs directly from `string`s, `ReadableStream`s, `Response` objects, `Blob`/`File` objects, or `Request` objects.
|
|
66
66
|
- ⚙️ **Advanced Parsing Options**: Customize your experience with various delimiters and quotation marks.
|
|
67
67
|
- 🔄 Defaults to `,` and `"` respectively.
|
|
68
68
|
- 💾 **Specialized Binary CSV Parsing**: Leverage Stream-based processing for versatility and strength.
|
|
@@ -173,6 +173,47 @@ for await (const record of parse(response)) {
|
|
|
173
173
|
// { name: 'Bob', age: '69' }
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
### Parsing CSV files from `Blob` or `File` objects
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
import { parse } from 'web-csv-toolbox';
|
|
180
|
+
|
|
181
|
+
// From file input
|
|
182
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
183
|
+
fileInput.addEventListener('change', async (e) => {
|
|
184
|
+
const file = e.target.files[0];
|
|
185
|
+
|
|
186
|
+
for await (const record of parse(file)) {
|
|
187
|
+
console.log(record);
|
|
188
|
+
}
|
|
189
|
+
// Prints:
|
|
190
|
+
// { name: 'Alice', age: '42' }
|
|
191
|
+
// { name: 'Bob', age: '69' }
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Parsing CSV files from `Request` objects (Server-side)
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
import { parse } from 'web-csv-toolbox';
|
|
199
|
+
|
|
200
|
+
// Cloudflare Workers / Service Workers
|
|
201
|
+
export default {
|
|
202
|
+
async fetch(request) {
|
|
203
|
+
if (request.method === 'POST') {
|
|
204
|
+
for await (const record of parse(request)) {
|
|
205
|
+
console.log(record);
|
|
206
|
+
}
|
|
207
|
+
// Prints:
|
|
208
|
+
// { name: 'Alice', age: '42' }
|
|
209
|
+
// { name: 'Bob', age: '69' }
|
|
210
|
+
|
|
211
|
+
return new Response('OK', { status: 200 });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
176
217
|
### Parsing CSV files with different delimiters and quotation characters
|
|
177
218
|
|
|
178
219
|
```js
|
|
@@ -316,6 +357,19 @@ try {
|
|
|
316
357
|
|
|
317
358
|
- Verify that JavaScript is executable on the Deno. [](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/deno.yaml)
|
|
318
359
|
|
|
360
|
+
### Platform-Specific Usage Guide 📚
|
|
361
|
+
|
|
362
|
+
For detailed examples and best practices for your specific runtime environment, see:
|
|
363
|
+
|
|
364
|
+
**[Platform-Specific Usage Guide](./docs/how-to-guides/platform-usage/)**
|
|
365
|
+
|
|
366
|
+
This guide covers:
|
|
367
|
+
- 🌐 **Browser**: File input, drag-and-drop, Clipboard API, FormData, Fetch API
|
|
368
|
+
- 🟢 **Node.js**: Buffer, fs.ReadStream, HTTP requests, stdin/stdout
|
|
369
|
+
- 🦕 **Deno**: Deno.readFile, Deno.open, fetch API
|
|
370
|
+
- ⚡ **Edge**: Cloudflare Workers, Deno Deploy, Vercel Edge Functions
|
|
371
|
+
- 🐰 **Bun**: File API, HTTP server
|
|
372
|
+
|
|
319
373
|
## APIs 🧑💻
|
|
320
374
|
|
|
321
375
|
### High-level APIs 🚀
|
|
@@ -328,11 +382,14 @@ providing an intuitive and straightforward experience for users.
|
|
|
328
382
|
- **`function parse.toArray(input[, options]): Promise<CSVRecord[]>`**: [📑](https://kamiazya.github.io/web-csv-toolbox/functions/parse.toArray.html)
|
|
329
383
|
- Parses CSV input into an array of records, ideal for smaller data sets.
|
|
330
384
|
|
|
331
|
-
The `input` paramater can be
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
385
|
+
The `input` paramater can be:
|
|
386
|
+
- a `string`
|
|
387
|
+
- a [ReadableStream](https://developer.mozilla.org/docs/Web/API/ReadableStream) of `string`s or [Uint8Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)s
|
|
388
|
+
- a [Uint8Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) object
|
|
389
|
+
- an [ArrayBuffer](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) object
|
|
390
|
+
- a [Response](https://developer.mozilla.org/docs/Web/API/Response) object
|
|
391
|
+
- a [Blob](https://developer.mozilla.org/docs/Web/API/Blob) or [File](https://developer.mozilla.org/docs/Web/API/File) object
|
|
392
|
+
- a [Request](https://developer.mozilla.org/docs/Web/API/Request) object (server-side)
|
|
336
393
|
|
|
337
394
|
### Middle-level APIs 🧱
|
|
338
395
|
|
|
@@ -345,6 +402,12 @@ catering to users who need more detailed and fine-tuned functionality.
|
|
|
345
402
|
- Parse CSV Binary of ArrayBuffer or Uint8Array.
|
|
346
403
|
- **`function parseResponse(response[, options])`**: [📑](https://kamiazya.github.io/web-csv-toolbox/functions/parseResponse-1.html)
|
|
347
404
|
- Customized parsing directly from `Response` objects.
|
|
405
|
+
- **`function parseRequest(request[, options])`**: [📑](https://kamiazya.github.io/web-csv-toolbox/functions/parseRequest-1.html)
|
|
406
|
+
- Server-side parsing from `Request` objects (Cloudflare Workers, Service Workers, etc.).
|
|
407
|
+
- **`function parseBlob(blob[, options])`**: [📑](https://kamiazya.github.io/web-csv-toolbox/functions/parseBlob-1.html)
|
|
408
|
+
- Parse CSV data from `Blob` or `File` objects.
|
|
409
|
+
- **`function parseFile(file[, options])`**: [📑](https://kamiazya.github.io/web-csv-toolbox/functions/parseFile-1.html)
|
|
410
|
+
- Alias for `parseBlob`, semantically clearer for `File` objects.
|
|
348
411
|
- **`function parseStream(stream[, options])`**: [📑](https://kamiazya.github.io/web-csv-toolbox/functions/parseStream-1.html)
|
|
349
412
|
- Stream-based parsing for larger or continuous data.
|
|
350
413
|
- **`function parseStringStream(stream[, options])`**: [📑](https://kamiazya.github.io/web-csv-toolbox/functions/parseStringStream-1.html)
|
|
@@ -92,6 +92,20 @@ class CSVRecordAssembler {
|
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* Flushes any remaining buffered data as a final record.
|
|
95
|
+
*
|
|
96
|
+
* @remarks
|
|
97
|
+
* Prototype Pollution Safety:
|
|
98
|
+
* This method uses Object.fromEntries() to create record objects from CSV data.
|
|
99
|
+
* Object.fromEntries() is safe from prototype pollution because it creates
|
|
100
|
+
* own properties (not prototype properties) even when keys like "__proto__",
|
|
101
|
+
* "constructor", or "prototype" are used.
|
|
102
|
+
*
|
|
103
|
+
* For example, Object.fromEntries([["__proto__", "value"]]) creates an object
|
|
104
|
+
* with an own property "__proto__" set to "value", which does NOT pollute
|
|
105
|
+
* Object.prototype and does NOT affect other objects.
|
|
106
|
+
*
|
|
107
|
+
* This safety is verified by regression tests in:
|
|
108
|
+
* CSVRecordAssembler.prototype-safety.test.ts
|
|
95
109
|
*/
|
|
96
110
|
*#flush() {
|
|
97
111
|
if (this.#header !== void 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CSVRecordAssembler.js","sources":["../src/CSVRecordAssembler.ts"],"sourcesContent":["import { FieldDelimiter, RecordDelimiter } from \"./common/constants.ts\";\nimport { ParseError } from \"./common/errors.ts\";\nimport type {\n CSVRecord,\n CSVRecordAssemblerOptions,\n Token,\n} from \"./common/types.ts\";\n\n/**\n * Default maximum field count per record (100,000 fields).\n */\nconst DEFAULT_MAX_FIELD_COUNT = 100_000;\n\n/**\n * Options for the CSVRecordAssembler.assemble method.\n */\nexport interface CSVRecordAssemblerAssembleOptions {\n /**\n * If true, indicates that more tokens are expected.\n * If false or omitted, flushes remaining data.\n */\n stream?: boolean;\n}\n\n/**\n * CSV Record Assembler.\n *\n * CSVRecordAssembler assembles tokens into CSV records.\n */\nexport class CSVRecordAssembler<Header extends ReadonlyArray<string>> {\n #fieldIndex = 0;\n #row: string[] = [];\n #header: Header | undefined;\n #dirty = false;\n #signal?: AbortSignal;\n #maxFieldCount: number;\n #skipEmptyLines: boolean;\n\n constructor(options: CSVRecordAssemblerOptions<Header> = {}) {\n const mfc = options.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT;\n // Validate maxFieldCount\n if (\n !(Number.isFinite(mfc) || mfc === Number.POSITIVE_INFINITY) ||\n (Number.isFinite(mfc) && (mfc < 1 || !Number.isInteger(mfc)))\n ) {\n throw new RangeError(\n \"maxFieldCount must be a positive integer or Number.POSITIVE_INFINITY\",\n );\n }\n this.#maxFieldCount = mfc;\n this.#skipEmptyLines = options.skipEmptyLines ?? false;\n if (options.header !== undefined && Array.isArray(options.header)) {\n this.#setHeader(options.header);\n }\n if (options.signal) {\n this.#signal = options.signal;\n }\n }\n\n /**\n * Assembles tokens into CSV records.\n * @param input - A single token or an iterable of tokens. Omit to flush remaining data.\n * @param options - Assembler options.\n * @returns An iterable iterator of CSV records.\n */\n public *assemble(\n input?: Token | Iterable<Token>,\n options?: CSVRecordAssemblerAssembleOptions,\n ): IterableIterator<CSVRecord<Header>> {\n const stream = options?.stream ?? false;\n\n if (input !== undefined) {\n // Check if input is iterable (has Symbol.iterator)\n if (this.#isIterable(input)) {\n for (const token of input) {\n yield* this.#processToken(token);\n }\n } else {\n // Single token\n yield* this.#processToken(input);\n }\n }\n\n if (!stream) {\n yield* this.#flush();\n }\n }\n\n /**\n * Checks if a value is iterable.\n */\n #isIterable(value: any): value is Iterable<Token> {\n return value != null && typeof value[Symbol.iterator] === \"function\";\n }\n\n /**\n * Processes a single token and yields a record if one is completed.\n */\n *#processToken(token: Token): IterableIterator<CSVRecord<Header>> {\n this.#signal?.throwIfAborted();\n\n switch (token.type) {\n case FieldDelimiter:\n this.#fieldIndex++;\n this.#checkFieldCount();\n this.#dirty = true;\n break;\n case RecordDelimiter:\n if (this.#header === undefined) {\n this.#setHeader(this.#row as unknown as Header);\n } else {\n if (this.#dirty) {\n yield Object.fromEntries(\n this.#header\n .map((header, index) => [header, index] as const)\n .filter(([header]) => header)\n .map(([header, index]) => [header, this.#row.at(index)]),\n ) as unknown as CSVRecord<Header>;\n } else {\n if (!this.#skipEmptyLines) {\n yield Object.fromEntries(\n this.#header\n .filter((header) => header)\n .map((header) => [header, \"\"]),\n ) as CSVRecord<Header>;\n }\n }\n }\n // Reset the row fields buffer.\n this.#fieldIndex = 0;\n this.#row = new Array(this.#header?.length).fill(\"\");\n this.#dirty = false;\n break;\n default:\n this.#dirty = true;\n this.#row[this.#fieldIndex] = token.value;\n break;\n }\n }\n\n /**\n * Flushes any remaining buffered data as a final record.\n */\n *#flush(): IterableIterator<CSVRecord<Header>> {\n if (this.#header !== undefined) {\n if (this.#dirty) {\n yield Object.fromEntries(\n this.#header\n .map((header, index) => [header, index] as const)\n .filter(([header]) => header)\n .map(([header, index]) => [header, this.#row.at(index)]),\n ) as unknown as CSVRecord<Header>;\n }\n }\n }\n\n #checkFieldCount(): void {\n if (this.#fieldIndex + 1 > this.#maxFieldCount) {\n throw new RangeError(\n `Field count (${this.#fieldIndex + 1}) exceeded maximum allowed count of ${this.#maxFieldCount}`,\n );\n }\n }\n\n #setHeader(header: Header) {\n if (header.length > this.#maxFieldCount) {\n throw new RangeError(\n `Header field count (${header.length}) exceeded maximum allowed count of ${this.#maxFieldCount}`,\n );\n }\n this.#header = header;\n if (this.#header.length === 0) {\n throw new ParseError(\"The header must not be empty.\");\n }\n if (new Set(this.#header).size !== this.#header.length) {\n throw new ParseError(\"The header must not contain duplicate fields.\");\n }\n }\n}\n"],"names":[],"mappings":";;;AAWA,MAAM,uBAAA,GAA0B,GAAA;AAkBzB,MAAM,kBAAA,CAAyD;AAAA,EACpE,WAAA,GAAc,CAAA;AAAA,EACd,OAAiB,EAAC;AAAA,EAClB,OAAA;AAAA,EACA,MAAA,GAAS,KAAA;AAAA,EACT,OAAA;AAAA,EACA,cAAA;AAAA,EACA,eAAA;AAAA,EAEA,WAAA,CAAY,OAAA,GAA6C,EAAC,EAAG;AAC3D,IAAA,MAAM,GAAA,GAAM,QAAQ,aAAA,IAAiB,uBAAA;AAErC,IAAA,IACE,EAAE,MAAA,CAAO,QAAA,CAAS,GAAG,CAAA,IAAK,GAAA,KAAQ,OAAO,iBAAA,CAAA,IACxC,MAAA,CAAO,QAAA,CAAS,GAAG,MAAM,GAAA,GAAM,CAAA,IAAK,CAAC,MAAA,CAAO,SAAA,CAAU,GAAG,CAAA,CAAA,EAC1D;AACA,MAAA,MAAM,IAAI,UAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,IAAA,CAAK,cAAA,GAAiB,GAAA;AACtB,IAAA,IAAA,CAAK,eAAA,GAAkB,QAAQ,cAAA,IAAkB,KAAA;AACjD,IAAA,IAAI,QAAQ,MAAA,KAAW,MAAA,IAAa,MAAM,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjE,MAAA,IAAA,CAAK,UAAA,CAAW,QAAQ,MAAM,CAAA;AAAA,IAChC;AACA,IAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,MAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,MAAA;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,CAAQ,QAAA,CACN,KAAA,EACA,OAAA,EACqC;AACrC,IAAA,MAAM,MAAA,GAAS,SAAS,MAAA,IAAU,KAAA;AAElC,IAAA,IAAI,UAAU,MAAA,EAAW;AAEvB,MAAA,IAAI,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA,EAAG;AAC3B,QAAA,KAAA,MAAW,SAAS,KAAA,EAAO;AACzB,UAAA,OAAO,IAAA,CAAK,cAAc,KAAK,CAAA;AAAA,QACjC;AAAA,MACF,CAAA,MAAO;AAEL,QAAA,OAAO,IAAA,CAAK,cAAc,KAAK,CAAA;AAAA,MACjC;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,OAAO,KAAK,MAAA,EAAO;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,KAAA,EAAsC;AAChD,IAAA,OAAO,SAAS,IAAA,IAAQ,OAAO,KAAA,CAAM,MAAA,CAAO,QAAQ,CAAA,KAAM,UAAA;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA,EAKA,CAAC,cAAc,KAAA,EAAmD;AAChE,IAAA,IAAA,CAAK,SAAS,cAAA,EAAe;AAE7B,IAAA,QAAQ,MAAM,IAAA;AAAM,MAClB,KAAK,cAAA;AACH,QAAA,IAAA,CAAK,WAAA,EAAA;AACL,QAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,QAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,QAAA;AAAA,MACF,KAAK,eAAA;AACH,QAAA,IAAI,IAAA,CAAK,YAAY,MAAA,EAAW;AAC9B,UAAA,IAAA,CAAK,UAAA,CAAW,KAAK,IAAyB,CAAA;AAAA,QAChD,CAAA,MAAO;AACL,UAAA,IAAI,KAAK,MAAA,EAAQ;AACf,YAAA,MAAM,MAAA,CAAO,WAAA;AAAA,cACX,IAAA,CAAK,OAAA,CACF,GAAA,CAAI,CAAC,MAAA,EAAQ,KAAA,KAAU,CAAC,MAAA,EAAQ,KAAK,CAAU,CAAA,CAC/C,MAAA,CAAO,CAAC,CAAC,MAAM,CAAA,KAAM,MAAM,CAAA,CAC3B,GAAA,CAAI,CAAC,CAAC,QAAQ,KAAK,CAAA,KAAM,CAAC,MAAA,EAAQ,IAAA,CAAK,IAAA,CAAK,EAAA,CAAG,KAAK,CAAC,CAAC;AAAA,aAC3D;AAAA,UACF,CAAA,MAAO;AACL,YAAA,IAAI,CAAC,KAAK,eAAA,EAAiB;AACzB,cAAA,MAAM,MAAA,CAAO,WAAA;AAAA,gBACX,IAAA,CAAK,OAAA,CACF,MAAA,CAAO,CAAC,MAAA,KAAW,MAAM,CAAA,CACzB,GAAA,CAAI,CAAC,MAAA,KAAW,CAAC,MAAA,EAAQ,EAAE,CAAC;AAAA,eACjC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,QAAA,IAAA,CAAK,WAAA,GAAc,CAAA;AACnB,QAAA,IAAA,CAAK,IAAA,GAAO,IAAI,KAAA,CAAM,IAAA,CAAK,SAAS,MAAM,CAAA,CAAE,KAAK,EAAE,CAAA;AACnD,QAAA,IAAA,CAAK,MAAA,GAAS,KAAA;AACd,QAAA;AAAA,MACF;AACE,QAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,QAAA,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,WAAW,CAAA,GAAI,KAAA,CAAM,KAAA;AACpC,QAAA;AAAA;AACJ,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,CAAC,MAAA,GAA8C;AAC7C,IAAA,IAAI,IAAA,CAAK,YAAY,MAAA,EAAW;AAC9B,MAAA,IAAI,KAAK,MAAA,EAAQ;AACf,QAAA,MAAM,MAAA,CAAO,WAAA;AAAA,UACX,IAAA,CAAK,OAAA,CACF,GAAA,CAAI,CAAC,MAAA,EAAQ,KAAA,KAAU,CAAC,MAAA,EAAQ,KAAK,CAAU,CAAA,CAC/C,MAAA,CAAO,CAAC,CAAC,MAAM,CAAA,KAAM,MAAM,CAAA,CAC3B,GAAA,CAAI,CAAC,CAAC,QAAQ,KAAK,CAAA,KAAM,CAAC,MAAA,EAAQ,IAAA,CAAK,IAAA,CAAK,EAAA,CAAG,KAAK,CAAC,CAAC;AAAA,SAC3D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,gBAAA,GAAyB;AACvB,IAAA,IAAI,IAAA,CAAK,WAAA,GAAc,CAAA,GAAI,IAAA,CAAK,cAAA,EAAgB;AAC9C,MAAA,MAAM,IAAI,UAAA;AAAA,QACR,gBAAgB,IAAA,CAAK,WAAA,GAAc,CAAC,CAAA,oCAAA,EAAuC,KAAK,cAAc,CAAA;AAAA,OAChG;AAAA,IACF;AAAA,EACF;AAAA,EAEA,WAAW,MAAA,EAAgB;AACzB,IAAA,IAAI,MAAA,CAAO,MAAA,GAAS,IAAA,CAAK,cAAA,EAAgB;AACvC,MAAA,MAAM,IAAI,UAAA;AAAA,QACR,CAAA,oBAAA,EAAuB,MAAA,CAAO,MAAM,CAAA,oCAAA,EAAuC,KAAK,cAAc,CAAA;AAAA,OAChG;AAAA,IACF;AACA,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AACf,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAC7B,MAAA,MAAM,IAAI,WAAW,+BAA+B,CAAA;AAAA,IACtD;AACA,IAAA,IAAI,IAAI,IAAI,IAAA,CAAK,OAAO,EAAE,IAAA,KAAS,IAAA,CAAK,QAAQ,MAAA,EAAQ;AACtD,MAAA,MAAM,IAAI,WAAW,+CAA+C,CAAA;AAAA,IACtE;AAAA,EACF;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"CSVRecordAssembler.js","sources":["../src/CSVRecordAssembler.ts"],"sourcesContent":["import { FieldDelimiter, RecordDelimiter } from \"./common/constants.ts\";\nimport { ParseError } from \"./common/errors.ts\";\nimport type {\n CSVRecord,\n CSVRecordAssemblerOptions,\n Token,\n} from \"./common/types.ts\";\n\n/**\n * Default maximum field count per record (100,000 fields).\n */\nconst DEFAULT_MAX_FIELD_COUNT = 100_000;\n\n/**\n * Options for the CSVRecordAssembler.assemble method.\n */\nexport interface CSVRecordAssemblerAssembleOptions {\n /**\n * If true, indicates that more tokens are expected.\n * If false or omitted, flushes remaining data.\n */\n stream?: boolean;\n}\n\n/**\n * CSV Record Assembler.\n *\n * CSVRecordAssembler assembles tokens into CSV records.\n */\nexport class CSVRecordAssembler<Header extends ReadonlyArray<string>> {\n #fieldIndex = 0;\n #row: string[] = [];\n #header: Header | undefined;\n #dirty = false;\n #signal?: AbortSignal;\n #maxFieldCount: number;\n #skipEmptyLines: boolean;\n\n constructor(options: CSVRecordAssemblerOptions<Header> = {}) {\n const mfc = options.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT;\n // Validate maxFieldCount\n if (\n !(Number.isFinite(mfc) || mfc === Number.POSITIVE_INFINITY) ||\n (Number.isFinite(mfc) && (mfc < 1 || !Number.isInteger(mfc)))\n ) {\n throw new RangeError(\n \"maxFieldCount must be a positive integer or Number.POSITIVE_INFINITY\",\n );\n }\n this.#maxFieldCount = mfc;\n this.#skipEmptyLines = options.skipEmptyLines ?? false;\n if (options.header !== undefined && Array.isArray(options.header)) {\n this.#setHeader(options.header);\n }\n if (options.signal) {\n this.#signal = options.signal;\n }\n }\n\n /**\n * Assembles tokens into CSV records.\n * @param input - A single token or an iterable of tokens. Omit to flush remaining data.\n * @param options - Assembler options.\n * @returns An iterable iterator of CSV records.\n */\n public *assemble(\n input?: Token | Iterable<Token>,\n options?: CSVRecordAssemblerAssembleOptions,\n ): IterableIterator<CSVRecord<Header>> {\n const stream = options?.stream ?? false;\n\n if (input !== undefined) {\n // Check if input is iterable (has Symbol.iterator)\n if (this.#isIterable(input)) {\n for (const token of input) {\n yield* this.#processToken(token);\n }\n } else {\n // Single token\n yield* this.#processToken(input);\n }\n }\n\n if (!stream) {\n yield* this.#flush();\n }\n }\n\n /**\n * Checks if a value is iterable.\n */\n #isIterable(value: any): value is Iterable<Token> {\n return value != null && typeof value[Symbol.iterator] === \"function\";\n }\n\n /**\n * Processes a single token and yields a record if one is completed.\n */\n *#processToken(token: Token): IterableIterator<CSVRecord<Header>> {\n this.#signal?.throwIfAborted();\n\n switch (token.type) {\n case FieldDelimiter:\n this.#fieldIndex++;\n this.#checkFieldCount();\n this.#dirty = true;\n break;\n case RecordDelimiter:\n if (this.#header === undefined) {\n this.#setHeader(this.#row as unknown as Header);\n } else {\n if (this.#dirty) {\n // SAFETY: Object.fromEntries() is safe from prototype pollution.\n // See CSVRecordAssembler.prototype-safety.test.ts for details.\n yield Object.fromEntries(\n this.#header\n .map((header, index) => [header, index] as const)\n .filter(([header]) => header)\n .map(([header, index]) => [header, this.#row.at(index)]),\n ) as unknown as CSVRecord<Header>;\n } else {\n if (!this.#skipEmptyLines) {\n // SAFETY: Object.fromEntries() is safe from prototype pollution.\n // See CSVRecordAssembler.prototype-safety.test.ts for details.\n yield Object.fromEntries(\n this.#header\n .filter((header) => header)\n .map((header) => [header, \"\"]),\n ) as CSVRecord<Header>;\n }\n }\n }\n // Reset the row fields buffer.\n this.#fieldIndex = 0;\n this.#row = new Array(this.#header?.length).fill(\"\");\n this.#dirty = false;\n break;\n default:\n this.#dirty = true;\n this.#row[this.#fieldIndex] = token.value;\n break;\n }\n }\n\n /**\n * Flushes any remaining buffered data as a final record.\n *\n * @remarks\n * Prototype Pollution Safety:\n * This method uses Object.fromEntries() to create record objects from CSV data.\n * Object.fromEntries() is safe from prototype pollution because it creates\n * own properties (not prototype properties) even when keys like \"__proto__\",\n * \"constructor\", or \"prototype\" are used.\n *\n * For example, Object.fromEntries([[\"__proto__\", \"value\"]]) creates an object\n * with an own property \"__proto__\" set to \"value\", which does NOT pollute\n * Object.prototype and does NOT affect other objects.\n *\n * This safety is verified by regression tests in:\n * CSVRecordAssembler.prototype-safety.test.ts\n */\n *#flush(): IterableIterator<CSVRecord<Header>> {\n if (this.#header !== undefined) {\n if (this.#dirty) {\n // SAFETY: Object.fromEntries() creates own properties, preventing prototype pollution\n // even when CSV headers contain dangerous property names like __proto__, constructor, etc.\n // See CSVRecordAssembler.prototype-safety.test.ts for verification tests.\n yield Object.fromEntries(\n this.#header\n .map((header, index) => [header, index] as const)\n .filter(([header]) => header)\n .map(([header, index]) => [header, this.#row.at(index)]),\n ) as unknown as CSVRecord<Header>;\n }\n }\n }\n\n #checkFieldCount(): void {\n if (this.#fieldIndex + 1 > this.#maxFieldCount) {\n throw new RangeError(\n `Field count (${this.#fieldIndex + 1}) exceeded maximum allowed count of ${this.#maxFieldCount}`,\n );\n }\n }\n\n #setHeader(header: Header) {\n if (header.length > this.#maxFieldCount) {\n throw new RangeError(\n `Header field count (${header.length}) exceeded maximum allowed count of ${this.#maxFieldCount}`,\n );\n }\n this.#header = header;\n if (this.#header.length === 0) {\n throw new ParseError(\"The header must not be empty.\");\n }\n if (new Set(this.#header).size !== this.#header.length) {\n throw new ParseError(\"The header must not contain duplicate fields.\");\n }\n }\n}\n"],"names":[],"mappings":"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"}
|