@mitre/hdf-utilities 3.6.0-rc.2 → 3.6.0-rc.4

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 CHANGED
@@ -15,6 +15,33 @@ Generic utility functions for working with common data formats (JSON, XML, CSV)
15
15
  - Parse HDF-specific structures (use `@mitre/hdf-parsers` for HDF traversal)
16
16
  - Convert between security tool formats (use `@mitre/hdf-converters`)
17
17
 
18
+ ### Why the Go and TypeScript surfaces differ
19
+
20
+ The two languages do not export the same modules, and that is deliberate. This
21
+ package holds cross-language **policy** — decisions both implementations must
22
+ agree on — while each language uses its own serializer.
23
+
24
+ The plumbing in `hash` and `json` has no Go counterpart because Go's standard library already provides it: `crypto/sha256`, `encoding/json`. Wrapping `parseJSON`, `stringifyJSON` or `generateHash` in Go would add a layer with no decision in it. The TypeScript side needs those wrappers because JavaScript ships no equivalent, and the wrapper is where safe defaults get centralised. The policy in those modules does exist in both: Go's `CanonicalJSON`, `ChecksumJSON` and `SHA256Hex` are the twins of TypeScript's `canonicalJson` and `canonicalChecksum`. The canonical byte form a checksum is taken over (sorted keys, `<`, `>` and `&` escaped, null-valued keys dropped) is a decision both implementations must agree on, so it is pinned against shared vectors rather than left to each serializer.
25
+
26
+ `csv` exists in both, but the two surfaces are deliberately different sizes: the
27
+ TypeScript module parses and builds CSV, while the Go one carries only the
28
+ formula-injection policy and leaves writing to `encoding/csv`.
29
+
30
+ XML shows the pattern most clearly. Go's `xml.go` exports only
31
+ `ContainsXMLEntityDeclarations` and `ExtractXMLRootElement` — an XXE-safety
32
+ policy and a domain helper — and leaves parsing to `encoding/xml`. The
33
+ TypeScript `xml/` module wraps fast-xml-parser and carries the same policy in its
34
+ defaults.
35
+
36
+ So the test for whether something belongs in both languages is not "does the
37
+ other one have it" but "is it a decision, or is it plumbing". Formula-injection
38
+ sanitizing (`SanitizeCSVValue` / `sanitizeCsvValue`) is a decision: which leading
39
+ characters a spreadsheet will execute, and what neutralises them. It exists in
40
+ both, pinned to one shared table in `testdata/csv-sanitize-cases.json`, because a
41
+ trigger added on one side alone would leave the other exporting CSVs a
42
+ spreadsheet runs. CSV *writing* is plumbing, and stays with each language's
43
+ serializer.
44
+
18
45
  ### hdf-utilities vs. hdf-validators
19
46
 
20
47
  | hdf-utilities | hdf-validators |
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/csv/index.ts"],"mappings":";;;;;UAiBiB;EACf;EACA;EACA,mBAAmB,gBAAgB;EACnC;EACA;EACA;;;;;UAMe;EACf;EACA;EACA;EACA;;;;;;;;;iBAwEc,SAAS,IAAI,yBAC3B,aACA,UAAU,QAAQ,mBACjB;;;;;;;;iBAoDa,cACd,aACA,UAAU,QAAQ;;;;;;;;;;;iBAiCJ,SAAS,IAAI,yBAC3B,MAAM,KACN,UAAU,QAAQ;EAAqB;;;;;;;;;;;;iBAuDzB,cACd,kBACA,UAAU,QAAQ;EAAqB;;;;;;;iBA2BzB,WAAW;;;;;;;;;;iBA0BX,iBAAiB;;;;;;;iBAejB,iBAAiB;;;;;;;iBAUjB,kBAAkB,UAAU,yBAC1C,KAAK,IACJ"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/csv/index.ts"],"mappings":";;;;;UAkBiB;EACf;EACA;EACA,mBAAmB,gBAAgB;EACnC;EACA;EACA;;;;;UAMe;EACf;EACA;EACA;EACA;;;;;;;;;iBAwEc,SAAS,IAAI,yBAC3B,aACA,UAAU,QAAQ,mBACjB;;;;;;;;iBAoDa,cACd,aACA,UAAU,QAAQ;;;;;;;;;;;iBAiCJ,SAAS,IAAI,yBAC3B,MAAM,KACN,UAAU,QAAQ;EAAqB;;;;;;;;;;;;iBA6GzB,cACd,kBACA,UAAU,QAAQ;EAAqB;;;;;;;iBA4BzB,WAAW;;;;;;;;;;iBA0BX,iBAAiB;;;;;;;iBAejB,iBAAiB;;;;;;;iBAUjB,kBAAkB,UAAU,yBAC1C,KAAK,IACJ"}
package/dist/csv/index.js CHANGED
@@ -129,11 +129,58 @@ function buildCsv(data, options) {
129
129
  const rows = processedData.map((row) => Object.values(row).map(String));
130
130
  result = dsv.formatRows(rows);
131
131
  } else result = dsv.format(processedData);
132
+ result = quoteLeadingWhitespace(result, dsv, opts.delimiter ?? ",");
132
133
  result = terminateFinalRecord(result);
133
134
  if (opts.newline && opts.newline !== "\n") result = result.replace(/\n/g, opts.newline);
134
135
  return result;
135
136
  }
136
137
  /**
138
+ * Quote any field Go's encoding/csv would quote and d3-dsv would not.
139
+ *
140
+ * d3-dsv quotes only for the delimiter, a quote, or a newline. Go additionally
141
+ * quotes when the field's FIRST rune is Unicode whitespace — so " padded "
142
+ * round-trips through a parser that trims — and quotes the literal field `\.`,
143
+ * which some dialects treat as end-of-data. Trailing whitespace alone triggers
144
+ * neither language. Without this a TS CSV differs from its Go twin on any such
145
+ * value: the same class of split terminateFinalRecord exists to close.
146
+ *
147
+ * ONE predicate decides, and it is the same one Go uses. An earlier version
148
+ * tested [ \t] to skip the re-parse cheaply and \s to decide, so a field led by
149
+ * U+00A0 or U+2003 exited before the decision ever ran.
150
+ *
151
+ * Re-parses rather than intercepting the writer because d3-dsv exposes no
152
+ * per-field hook on format(); the parse is over output this function just
153
+ * produced, so it cannot fail on unbalanced quotes.
154
+ */
155
+ function quoteLeadingWhitespace(csv, dsv, delimiter) {
156
+ return formatRowsGoCompatible(dsv.parseRows(csv), dsv, delimiter);
157
+ }
158
+ /** The one writer both public builders go through, so a field cannot come out
159
+ * quoted from one and bare from the other. */
160
+ function formatRowsGoCompatible(rows, dsv, delimiter) {
161
+ return rows.map((row) => row.map((field) => requote(field, dsv)).join(delimiter)).join("\n");
162
+ }
163
+ /**
164
+ * Go's encoding/csv fieldNeedsQuotes, minus the conditions d3-dsv already
165
+ * covers: a leading Unicode space, or the literal `\.`.
166
+ *
167
+ * The class is Go's unicode.IsSpace, spelled out rather than borrowed from
168
+ * JavaScript's \s, which is not the same set: \s omits U+0085 (NEL) and adds
169
+ * U+FEFF, which Unicode 4.0.1 removed from White_Space. Borrowing \s left a
170
+ * field led by U+0085 unquoted here but quoted in Go, and the reverse for
171
+ * U+FEFF.
172
+ */
173
+ const GO_LEADING_SPACE = /^[\t\n\v\f\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/;
174
+ function goWouldQuote(field) {
175
+ return field === "\\." || GO_LEADING_SPACE.test(field);
176
+ }
177
+ /** One field, formatted by d3-dsv, then quoted if Go would quote it and d3-dsv did not. */
178
+ function requote(field, dsv) {
179
+ const formatted = dsv.formatValue(field);
180
+ if (formatted.startsWith("\"") || !goWouldQuote(field)) return formatted;
181
+ return `"${field.replace(/"/g, "\"\"")}"`;
182
+ }
183
+ /**
137
184
  * Terminate the final record with a newline. d3-dsv leaves it off, but Go's
138
185
  * encoding/csv (which the Go converters use) always writes it, so without this
139
186
  * every TS CSV would differ from its Go twin by exactly one trailing byte.
@@ -162,7 +209,9 @@ function buildCsvArray(data, options) {
162
209
  if (data.length === 0) return "";
163
210
  let processedData = data;
164
211
  if (sanitize) processedData = data.map((row) => sanitizeCsvArray(row));
165
- let result = terminateFinalRecord(dsvFormat(opts.delimiter ?? ",").formatRows(processedData));
212
+ const delimiter = opts.delimiter ?? ",";
213
+ const dsv = dsvFormat(delimiter);
214
+ let result = terminateFinalRecord(formatRowsGoCompatible(processedData, dsv, delimiter));
166
215
  if (opts.newline && opts.newline !== "\n") result = result.replace(/\n/g, opts.newline);
167
216
  return result;
168
217
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/csv/index.ts"],"sourcesContent":["/**\n * CSV parsing and generation utilities using d3-dsv\n *\n * SECURITY WARNING: CSV files opened in Excel/LibreOffice may execute formulas.\n * Use sanitization options when exporting data that may contain user-controlled content.\n */\n\nimport { dsvFormat } from 'd3-dsv';\n\nexport {\n extractColumn as extractCsvColumn,\n findRows as findCsvRows,\n} from '../object/index.js';\n\n/**\n * Options for parsing CSV\n */\nexport interface CsvParseOptions {\n header?: boolean;\n skipEmptyLines?: boolean;\n transformHeader?: (header: string, index: number) => string;\n dynamicTyping?: boolean;\n delimiter?: string;\n maxSize?: number;\n}\n\n/**\n * Options for building CSV\n */\nexport interface CsvBuildOptions {\n header?: boolean;\n newline?: string;\n skipEmptyLines?: boolean;\n delimiter?: string;\n}\n\n/**\n * Default options for parsing CSV with headers\n */\nconst DEFAULT_PARSE_OPTIONS: CsvParseOptions = {\n header: true,\n skipEmptyLines: true,\n transformHeader: undefined,\n dynamicTyping: false,\n};\n\n/**\n * Default options for parsing CSV without headers\n */\nconst DEFAULT_PARSE_ARRAY_OPTIONS: CsvParseOptions = {\n header: false,\n skipEmptyLines: true,\n dynamicTyping: false,\n};\n\n/**\n * Default options for building CSV\n */\nconst DEFAULT_BUILD_OPTIONS: CsvBuildOptions = {\n header: true,\n newline: '\\n',\n skipEmptyLines: true,\n};\n\n/**\n * Validate CSV quoting — throws if unclosed quotes are found\n */\nfunction validateQuoting(csv: string): void {\n let inQuotes = false;\n let row = 0;\n for (let i = 0; i < csv.length; i++) {\n if (csv[i] === '\"') {\n if (inQuotes && i + 1 < csv.length && csv[i + 1] === '\"') {\n i++; // skip escaped quote \"\"\n } else {\n inQuotes = !inQuotes;\n }\n } else if (csv[i] === '\\n' && !inQuotes) {\n row++;\n }\n }\n if (inQuotes) {\n throw new Error(`CSV parsing failed: Row ${row}: Unclosed quote`);\n }\n}\n\n/**\n * Convert string value to number or boolean when appropriate\n */\nfunction dynamicTypeConvert(value: string): unknown {\n if (value === '') return value;\n if (value === 'true') return true;\n if (value === 'false') return false;\n const num = Number(value);\n if (!isNaN(num) && value.trim() !== '') return num;\n return value;\n}\n\n/**\n * Parse CSV string into array of objects with headers as keys\n * @param csv - CSV string to parse\n * @param options - Optional parse configuration\n * @returns Array of objects representing rows\n * @throws Error if parsing fails\n */\nexport function parseCsv<T = Record<string, unknown>>(\n csv: string,\n options?: Partial<CsvParseOptions>\n): T[] {\n if (options?.maxSize !== undefined && csv.length > options.maxSize) {\n throw new Error(\n `Input exceeds maximum allowed size: ${csv.length} bytes exceeds limit of ${options.maxSize} bytes`\n );\n }\n\n const opts = { ...DEFAULT_PARSE_OPTIONS, ...options };\n const trimmed = csv.trim();\n\n validateQuoting(trimmed);\n\n const dsv = dsvFormat(opts.delimiter ?? ',');\n const parsed = dsv.parse(trimmed);\n const { columns } = parsed;\n\n // Pre-compute transformed headers\n const headers = opts.transformHeader\n ? columns.map((col, i) => opts.transformHeader!(col, i))\n : columns;\n\n let rows: Record<string, unknown>[] = parsed.map((row) => {\n const obj: Record<string, unknown> = {};\n columns.forEach((col, i) => {\n let value: unknown = row[col] ?? '';\n if (opts.dynamicTyping && typeof value === 'string') {\n value = dynamicTypeConvert(value);\n }\n const key = headers[i];\n if (key !== undefined) {\n obj[key] = value;\n }\n });\n return obj;\n });\n\n if (opts.skipEmptyLines) {\n rows = rows.filter((row) =>\n Object.values(row).some((v) => v !== '' && v !== null && v !== undefined)\n );\n }\n\n return rows as T[];\n}\n\n/**\n * Parse CSV string into array of arrays without treating first row as headers\n * @param csv - CSV string to parse\n * @param options - Optional parse configuration\n * @returns Array of arrays representing rows\n * @throws Error if parsing fails\n */\nexport function parseCsvArray(\n csv: string,\n options?: Partial<CsvParseOptions>\n): string[][] {\n if (options?.maxSize !== undefined && csv.length > options.maxSize) {\n throw new Error(\n `Input exceeds maximum allowed size: ${csv.length} bytes exceeds limit of ${options.maxSize} bytes`\n );\n }\n\n const opts = { ...DEFAULT_PARSE_ARRAY_OPTIONS, ...options };\n const trimmed = csv.trim();\n\n validateQuoting(trimmed);\n\n const dsv = dsvFormat(opts.delimiter ?? ',');\n let rows = dsv.parseRows(trimmed);\n\n if (opts.skipEmptyLines) {\n rows = rows.filter((row) => !(row.length === 1 && row[0] === ''));\n }\n\n return rows;\n}\n\n/**\n * Build CSV string from array of objects\n *\n * SECURITY: Set sanitize=true when data may contain user-controlled content\n * to prevent CSV formula injection attacks in Excel/LibreOffice.\n *\n * @param data - Array of objects to convert to CSV\n * @param options - Optional build configuration and sanitize flag\n * @returns CSV string\n */\nexport function buildCsv<T = Record<string, unknown>>(\n data: T[],\n options?: Partial<CsvBuildOptions> & { sanitize?: boolean }\n): string {\n const { sanitize, ...buildOpts } = options || {};\n const opts = { ...DEFAULT_BUILD_OPTIONS, ...buildOpts };\n\n if (data.length === 0) return '';\n\n let processedData = data;\n if (sanitize) {\n processedData = data.map((row) =>\n sanitizeCsvObject(row as Record<string, unknown>)\n ) as T[];\n }\n\n const dsv = dsvFormat(opts.delimiter ?? ',');\n let result: string;\n\n if (opts.header === false) {\n const rows = processedData.map((row) =>\n Object.values(row as Record<string, unknown>).map(String)\n );\n result = dsv.formatRows(rows);\n } else {\n result = dsv.format(processedData as Record<string, unknown>[]);\n }\n\n result = terminateFinalRecord(result);\n\n if (opts.newline && opts.newline !== '\\n') {\n result = result.replace(/\\n/g, opts.newline);\n }\n\n return result;\n}\n\n/**\n * Terminate the final record with a newline. d3-dsv leaves it off, but Go's\n * encoding/csv (which the Go converters use) always writes it, so without this\n * every TS CSV would differ from its Go twin by exactly one trailing byte.\n * Applied before the newline substitution so a \\r\\n dialect gets it too.\n */\nfunction terminateFinalRecord(csv: string): string {\n return csv.endsWith('\\n') ? csv : `${csv}\\n`;\n}\n\n/**\n * Build CSV string from array of arrays\n *\n * SECURITY: Set sanitize=true when data may contain user-controlled content\n * to prevent CSV formula injection attacks in Excel/LibreOffice.\n *\n * @param data - Array of arrays to convert to CSV\n * @param options - Optional build configuration and sanitize flag\n * @returns CSV string\n */\nexport function buildCsvArray(\n data: string[][],\n options?: Partial<CsvBuildOptions> & { sanitize?: boolean }\n): string {\n const { sanitize, ...buildOpts } = options || {};\n const opts = { ...DEFAULT_BUILD_OPTIONS, header: false, ...buildOpts };\n\n if (data.length === 0) return '';\n\n let processedData = data;\n if (sanitize) {\n processedData = data.map((row) => sanitizeCsvArray(row));\n }\n\n const dsv = dsvFormat(opts.delimiter ?? ',');\n let result = terminateFinalRecord(dsv.formatRows(processedData));\n\n if (opts.newline && opts.newline !== '\\n') {\n result = result.replace(/\\n/g, opts.newline);\n }\n\n return result;\n}\n\n/**\n * Validate CSV format\n * @param csv - CSV string to validate\n * @returns True if valid CSV, false otherwise\n */\nexport function isValidCsv(csv: string): boolean {\n if (!csv || csv.trim().length === 0) return false;\n\n const trimmed = csv.trim();\n\n // CSV must have at least one delimiter (comma) to be considered CSV structure\n if (!trimmed.includes(',')) return false;\n\n try {\n validateQuoting(trimmed);\n } catch {\n return false;\n }\n\n return true;\n}\n\n/**\n * Sanitize a single CSV value to prevent formula injection\n *\n * Prefixes values starting with =, +, -, @, |, or % with a single quote\n * to prevent Excel/LibreOffice from interpreting them as formulas.\n *\n * @param value - Value to sanitize\n * @returns Sanitized value\n */\nexport function sanitizeCsvValue(value: unknown): string {\n const str = String(value);\n // Check if string starts with formula trigger characters\n if (/^[=+\\-@|%]/.test(str)) {\n return `'${str}`;\n }\n return str;\n}\n\n/**\n * Sanitize all values in an array to prevent formula injection\n *\n * @param values - Array of values to sanitize\n * @returns Array with sanitized values\n */\nexport function sanitizeCsvArray(values: unknown[]): string[] {\n return values.map(sanitizeCsvValue);\n}\n\n/**\n * Sanitize all values in an object to prevent formula injection\n *\n * @param obj - Object with values to sanitize\n * @returns New object with sanitized values\n */\nexport function sanitizeCsvObject<T extends Record<string, unknown>>(\n obj: T\n): Record<string, string> {\n const sanitized: Record<string, string> = {};\n for (const [key, value] of Object.entries(obj)) {\n sanitized[key] = sanitizeCsvValue(value);\n }\n return sanitized;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/csv/index.ts"],"sourcesContent":["/**\n * CSV parsing and generation utilities using d3-dsv\n *\n * SECURITY WARNING: CSV files opened in Excel/LibreOffice may execute formulas.\n * Use sanitization options when exporting data that may contain user-controlled content.\n */\n\nimport { dsvFormat } from 'd3-dsv';\nimport type { DSV } from 'd3-dsv';\n\nexport {\n extractColumn as extractCsvColumn,\n findRows as findCsvRows,\n} from '../object/index.js';\n\n/**\n * Options for parsing CSV\n */\nexport interface CsvParseOptions {\n header?: boolean;\n skipEmptyLines?: boolean;\n transformHeader?: (header: string, index: number) => string;\n dynamicTyping?: boolean;\n delimiter?: string;\n maxSize?: number;\n}\n\n/**\n * Options for building CSV\n */\nexport interface CsvBuildOptions {\n header?: boolean;\n newline?: string;\n skipEmptyLines?: boolean;\n delimiter?: string;\n}\n\n/**\n * Default options for parsing CSV with headers\n */\nconst DEFAULT_PARSE_OPTIONS: CsvParseOptions = {\n header: true,\n skipEmptyLines: true,\n transformHeader: undefined,\n dynamicTyping: false,\n};\n\n/**\n * Default options for parsing CSV without headers\n */\nconst DEFAULT_PARSE_ARRAY_OPTIONS: CsvParseOptions = {\n header: false,\n skipEmptyLines: true,\n dynamicTyping: false,\n};\n\n/**\n * Default options for building CSV\n */\nconst DEFAULT_BUILD_OPTIONS: CsvBuildOptions = {\n header: true,\n newline: '\\n',\n skipEmptyLines: true,\n};\n\n/**\n * Validate CSV quoting — throws if unclosed quotes are found\n */\nfunction validateQuoting(csv: string): void {\n let inQuotes = false;\n let row = 0;\n for (let i = 0; i < csv.length; i++) {\n if (csv[i] === '\"') {\n if (inQuotes && i + 1 < csv.length && csv[i + 1] === '\"') {\n i++; // skip escaped quote \"\"\n } else {\n inQuotes = !inQuotes;\n }\n } else if (csv[i] === '\\n' && !inQuotes) {\n row++;\n }\n }\n if (inQuotes) {\n throw new Error(`CSV parsing failed: Row ${row}: Unclosed quote`);\n }\n}\n\n/**\n * Convert string value to number or boolean when appropriate\n */\nfunction dynamicTypeConvert(value: string): unknown {\n if (value === '') return value;\n if (value === 'true') return true;\n if (value === 'false') return false;\n const num = Number(value);\n if (!isNaN(num) && value.trim() !== '') return num;\n return value;\n}\n\n/**\n * Parse CSV string into array of objects with headers as keys\n * @param csv - CSV string to parse\n * @param options - Optional parse configuration\n * @returns Array of objects representing rows\n * @throws Error if parsing fails\n */\nexport function parseCsv<T = Record<string, unknown>>(\n csv: string,\n options?: Partial<CsvParseOptions>\n): T[] {\n if (options?.maxSize !== undefined && csv.length > options.maxSize) {\n throw new Error(\n `Input exceeds maximum allowed size: ${csv.length} bytes exceeds limit of ${options.maxSize} bytes`\n );\n }\n\n const opts = { ...DEFAULT_PARSE_OPTIONS, ...options };\n const trimmed = csv.trim();\n\n validateQuoting(trimmed);\n\n const dsv = dsvFormat(opts.delimiter ?? ',');\n const parsed = dsv.parse(trimmed);\n const { columns } = parsed;\n\n // Pre-compute transformed headers\n const headers = opts.transformHeader\n ? columns.map((col, i) => opts.transformHeader!(col, i))\n : columns;\n\n let rows: Record<string, unknown>[] = parsed.map((row) => {\n const obj: Record<string, unknown> = {};\n columns.forEach((col, i) => {\n let value: unknown = row[col] ?? '';\n if (opts.dynamicTyping && typeof value === 'string') {\n value = dynamicTypeConvert(value);\n }\n const key = headers[i];\n if (key !== undefined) {\n obj[key] = value;\n }\n });\n return obj;\n });\n\n if (opts.skipEmptyLines) {\n rows = rows.filter((row) =>\n Object.values(row).some((v) => v !== '' && v !== null && v !== undefined)\n );\n }\n\n return rows as T[];\n}\n\n/**\n * Parse CSV string into array of arrays without treating first row as headers\n * @param csv - CSV string to parse\n * @param options - Optional parse configuration\n * @returns Array of arrays representing rows\n * @throws Error if parsing fails\n */\nexport function parseCsvArray(\n csv: string,\n options?: Partial<CsvParseOptions>\n): string[][] {\n if (options?.maxSize !== undefined && csv.length > options.maxSize) {\n throw new Error(\n `Input exceeds maximum allowed size: ${csv.length} bytes exceeds limit of ${options.maxSize} bytes`\n );\n }\n\n const opts = { ...DEFAULT_PARSE_ARRAY_OPTIONS, ...options };\n const trimmed = csv.trim();\n\n validateQuoting(trimmed);\n\n const dsv = dsvFormat(opts.delimiter ?? ',');\n let rows = dsv.parseRows(trimmed);\n\n if (opts.skipEmptyLines) {\n rows = rows.filter((row) => !(row.length === 1 && row[0] === ''));\n }\n\n return rows;\n}\n\n/**\n * Build CSV string from array of objects\n *\n * SECURITY: Set sanitize=true when data may contain user-controlled content\n * to prevent CSV formula injection attacks in Excel/LibreOffice.\n *\n * @param data - Array of objects to convert to CSV\n * @param options - Optional build configuration and sanitize flag\n * @returns CSV string\n */\nexport function buildCsv<T = Record<string, unknown>>(\n data: T[],\n options?: Partial<CsvBuildOptions> & { sanitize?: boolean }\n): string {\n const { sanitize, ...buildOpts } = options || {};\n const opts = { ...DEFAULT_BUILD_OPTIONS, ...buildOpts };\n\n if (data.length === 0) return '';\n\n let processedData = data;\n if (sanitize) {\n processedData = data.map((row) =>\n sanitizeCsvObject(row as Record<string, unknown>)\n ) as T[];\n }\n\n const dsv = dsvFormat(opts.delimiter ?? ',');\n let result: string;\n\n if (opts.header === false) {\n const rows = processedData.map((row) =>\n Object.values(row as Record<string, unknown>).map(String)\n );\n result = dsv.formatRows(rows);\n } else {\n result = dsv.format(processedData as Record<string, unknown>[]);\n }\n\n result = quoteLeadingWhitespace(result, dsv, opts.delimiter ?? ',');\n result = terminateFinalRecord(result);\n\n if (opts.newline && opts.newline !== '\\n') {\n result = result.replace(/\\n/g, opts.newline);\n }\n\n return result;\n}\n\n/**\n * Quote any field Go's encoding/csv would quote and d3-dsv would not.\n *\n * d3-dsv quotes only for the delimiter, a quote, or a newline. Go additionally\n * quotes when the field's FIRST rune is Unicode whitespace — so \" padded \"\n * round-trips through a parser that trims — and quotes the literal field `\\.`,\n * which some dialects treat as end-of-data. Trailing whitespace alone triggers\n * neither language. Without this a TS CSV differs from its Go twin on any such\n * value: the same class of split terminateFinalRecord exists to close.\n *\n * ONE predicate decides, and it is the same one Go uses. An earlier version\n * tested [ \\t] to skip the re-parse cheaply and \\s to decide, so a field led by\n * U+00A0 or U+2003 exited before the decision ever ran.\n *\n * Re-parses rather than intercepting the writer because d3-dsv exposes no\n * per-field hook on format(); the parse is over output this function just\n * produced, so it cannot fail on unbalanced quotes.\n */\nfunction quoteLeadingWhitespace(csv: string, dsv: DSV, delimiter: string): string {\n return formatRowsGoCompatible(dsv.parseRows(csv), dsv, delimiter);\n}\n\n/** The one writer both public builders go through, so a field cannot come out\n * quoted from one and bare from the other. */\nfunction formatRowsGoCompatible(rows: string[][], dsv: DSV, delimiter: string): string {\n return rows.map((row) => row.map((field) => requote(field, dsv)).join(delimiter)).join('\\n');\n}\n\n/**\n * Go's encoding/csv fieldNeedsQuotes, minus the conditions d3-dsv already\n * covers: a leading Unicode space, or the literal `\\.`.\n *\n * The class is Go's unicode.IsSpace, spelled out rather than borrowed from\n * JavaScript's \\s, which is not the same set: \\s omits U+0085 (NEL) and adds\n * U+FEFF, which Unicode 4.0.1 removed from White_Space. Borrowing \\s left a\n * field led by U+0085 unquoted here but quoted in Go, and the reverse for\n * U+FEFF.\n */\nconst GO_LEADING_SPACE = /^[\\t\\n\\v\\f\\r \\u0085\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]/;\n\nfunction goWouldQuote(field: string): boolean {\n return field === '\\\\.' || GO_LEADING_SPACE.test(field);\n}\n\n/** One field, formatted by d3-dsv, then quoted if Go would quote it and d3-dsv did not. */\nfunction requote(field: string, dsv: DSV): string {\n const formatted = dsv.formatValue(field);\n if (formatted.startsWith('\"') || !goWouldQuote(field)) {\n return formatted;\n }\n return `\"${field.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Terminate the final record with a newline. d3-dsv leaves it off, but Go's\n * encoding/csv (which the Go converters use) always writes it, so without this\n * every TS CSV would differ from its Go twin by exactly one trailing byte.\n * Applied before the newline substitution so a \\r\\n dialect gets it too.\n */\nfunction terminateFinalRecord(csv: string): string {\n return csv.endsWith('\\n') ? csv : `${csv}\\n`;\n}\n\n/**\n * Build CSV string from array of arrays\n *\n * SECURITY: Set sanitize=true when data may contain user-controlled content\n * to prevent CSV formula injection attacks in Excel/LibreOffice.\n *\n * @param data - Array of arrays to convert to CSV\n * @param options - Optional build configuration and sanitize flag\n * @returns CSV string\n */\nexport function buildCsvArray(\n data: string[][],\n options?: Partial<CsvBuildOptions> & { sanitize?: boolean }\n): string {\n const { sanitize, ...buildOpts } = options || {};\n const opts = { ...DEFAULT_BUILD_OPTIONS, header: false, ...buildOpts };\n\n if (data.length === 0) return '';\n\n let processedData = data;\n if (sanitize) {\n processedData = data.map((row) => sanitizeCsvArray(row));\n }\n\n const delimiter = opts.delimiter ?? ',';\n const dsv = dsvFormat(delimiter);\n let result = terminateFinalRecord(formatRowsGoCompatible(processedData, dsv, delimiter));\n\n if (opts.newline && opts.newline !== '\\n') {\n result = result.replace(/\\n/g, opts.newline);\n }\n\n return result;\n}\n\n/**\n * Validate CSV format\n * @param csv - CSV string to validate\n * @returns True if valid CSV, false otherwise\n */\nexport function isValidCsv(csv: string): boolean {\n if (!csv || csv.trim().length === 0) return false;\n\n const trimmed = csv.trim();\n\n // CSV must have at least one delimiter (comma) to be considered CSV structure\n if (!trimmed.includes(',')) return false;\n\n try {\n validateQuoting(trimmed);\n } catch {\n return false;\n }\n\n return true;\n}\n\n/**\n * Sanitize a single CSV value to prevent formula injection\n *\n * Prefixes values starting with =, +, -, @, |, or % with a single quote\n * to prevent Excel/LibreOffice from interpreting them as formulas.\n *\n * @param value - Value to sanitize\n * @returns Sanitized value\n */\nexport function sanitizeCsvValue(value: unknown): string {\n const str = String(value);\n // Check if string starts with formula trigger characters\n if (/^[=+\\-@|%]/.test(str)) {\n return `'${str}`;\n }\n return str;\n}\n\n/**\n * Sanitize all values in an array to prevent formula injection\n *\n * @param values - Array of values to sanitize\n * @returns Array with sanitized values\n */\nexport function sanitizeCsvArray(values: unknown[]): string[] {\n return values.map(sanitizeCsvValue);\n}\n\n/**\n * Sanitize all values in an object to prevent formula injection\n *\n * @param obj - Object with values to sanitize\n * @returns New object with sanitized values\n */\nexport function sanitizeCsvObject<T extends Record<string, unknown>>(\n obj: T\n): Record<string, string> {\n const sanitized: Record<string, string> = {};\n for (const [key, value] of Object.entries(obj)) {\n sanitized[key] = sanitizeCsvValue(value);\n }\n return sanitized;\n}\n"],"mappings":";;;;;;;;;;;;AAwCA,MAAM,wBAAyC;CAC7C,QAAQ;CACR,gBAAgB;CAChB,iBAAiB,KAAA;CACjB,eAAe;AACjB;;;;AAKA,MAAM,8BAA+C;CACnD,QAAQ;CACR,gBAAgB;CAChB,eAAe;AACjB;;;;AAKA,MAAM,wBAAyC;CAC7C,QAAQ;CACR,SAAS;CACT,gBAAgB;AAClB;;;;AAKA,SAAS,gBAAgB,KAAmB;CAC1C,IAAI,WAAW;CACf,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAC9B,IAAI,IAAI,OAAO,MACb,IAAI,YAAY,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI,OAAO,MACnD;MAEA,WAAW,CAAC;MAET,IAAI,IAAI,OAAO,QAAQ,CAAC,UAC7B;CAGJ,IAAI,UACF,MAAM,IAAI,MAAM,2BAA2B,IAAI,iBAAiB;AAEpE;;;;AAKA,SAAS,mBAAmB,OAAwB;CAClD,IAAI,UAAU,IAAI,OAAO;CACzB,IAAI,UAAU,QAAQ,OAAO;CAC7B,IAAI,UAAU,SAAS,OAAO;CAC9B,MAAM,MAAM,OAAO,KAAK;CACxB,IAAI,CAAC,MAAM,GAAG,KAAK,MAAM,KAAK,MAAM,IAAI,OAAO;CAC/C,OAAO;AACT;;;;;;;;AASA,SAAgB,SACd,KACA,SACK;CACL,IAAI,SAAS,YAAY,KAAA,KAAa,IAAI,SAAS,QAAQ,SACzD,MAAM,IAAI,MACR,uCAAuC,IAAI,OAAO,0BAA0B,QAAQ,QAAQ,OAC9F;CAGF,MAAM,OAAO;EAAE,GAAG;EAAuB,GAAG;CAAQ;CACpD,MAAM,UAAU,IAAI,KAAK;CAEzB,gBAAgB,OAAO;CAGvB,MAAM,SADM,UAAU,KAAK,aAAa,GACvB,CAAC,CAAC,MAAM,OAAO;CAChC,MAAM,EAAE,YAAY;CAGpB,MAAM,UAAU,KAAK,kBACjB,QAAQ,KAAK,KAAK,MAAM,KAAK,gBAAiB,KAAK,CAAC,CAAC,IACrD;CAEJ,IAAI,OAAkC,OAAO,KAAK,QAAQ;EACxD,MAAM,MAA+B,CAAC;EACtC,QAAQ,SAAS,KAAK,MAAM;GAC1B,IAAI,QAAiB,IAAI,QAAQ;GACjC,IAAI,KAAK,iBAAiB,OAAO,UAAU,UACzC,QAAQ,mBAAmB,KAAK;GAElC,MAAM,MAAM,QAAQ;GACpB,IAAI,QAAQ,KAAA,GACV,IAAI,OAAO;EAEf,CAAC;EACD,OAAO;CACT,CAAC;CAED,IAAI,KAAK,gBACP,OAAO,KAAK,QAAQ,QAClB,OAAO,OAAO,GAAG,CAAC,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,QAAQ,MAAM,KAAA,CAAS,CAC1E;CAGF,OAAO;AACT;;;;;;;;AASA,SAAgB,cACd,KACA,SACY;CACZ,IAAI,SAAS,YAAY,KAAA,KAAa,IAAI,SAAS,QAAQ,SACzD,MAAM,IAAI,MACR,uCAAuC,IAAI,OAAO,0BAA0B,QAAQ,QAAQ,OAC9F;CAGF,MAAM,OAAO;EAAE,GAAG;EAA6B,GAAG;CAAQ;CAC1D,MAAM,UAAU,IAAI,KAAK;CAEzB,gBAAgB,OAAO;CAGvB,IAAI,OADQ,UAAU,KAAK,aAAa,GAC3B,CAAC,CAAC,UAAU,OAAO;CAEhC,IAAI,KAAK,gBACP,OAAO,KAAK,QAAQ,QAAQ,EAAE,IAAI,WAAW,KAAK,IAAI,OAAO,GAAG;CAGlE,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,SACd,MACA,SACQ;CACR,MAAM,EAAE,UAAU,GAAG,cAAc,WAAW,CAAC;CAC/C,MAAM,OAAO;EAAE,GAAG;EAAuB,GAAG;CAAU;CAEtD,IAAI,KAAK,WAAW,GAAG,OAAO;CAE9B,IAAI,gBAAgB;CACpB,IAAI,UACF,gBAAgB,KAAK,KAAK,QACxB,kBAAkB,GAA8B,CAClD;CAGF,MAAM,MAAM,UAAU,KAAK,aAAa,GAAG;CAC3C,IAAI;CAEJ,IAAI,KAAK,WAAW,OAAO;EACzB,MAAM,OAAO,cAAc,KAAK,QAC9B,OAAO,OAAO,GAA8B,CAAC,CAAC,IAAI,MAAM,CAC1D;EACA,SAAS,IAAI,WAAW,IAAI;CAC9B,OACE,SAAS,IAAI,OAAO,aAA0C;CAGhE,SAAS,uBAAuB,QAAQ,KAAK,KAAK,aAAa,GAAG;CAClE,SAAS,qBAAqB,MAAM;CAEpC,IAAI,KAAK,WAAW,KAAK,YAAY,MACnC,SAAS,OAAO,QAAQ,OAAO,KAAK,OAAO;CAG7C,OAAO;AACT;;;;;;;;;;;;;;;;;;;AAoBA,SAAS,uBAAuB,KAAa,KAAU,WAA2B;CAChF,OAAO,uBAAuB,IAAI,UAAU,GAAG,GAAG,KAAK,SAAS;AAClE;;;AAIA,SAAS,uBAAuB,MAAkB,KAAU,WAA2B;CACrF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,UAAU,QAAQ,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI;AAC7F;;;;;;;;;;;AAYA,MAAM,mBAAmB;AAEzB,SAAS,aAAa,OAAwB;CAC5C,OAAO,UAAU,SAAS,iBAAiB,KAAK,KAAK;AACvD;;AAGA,SAAS,QAAQ,OAAe,KAAkB;CAChD,MAAM,YAAY,IAAI,YAAY,KAAK;CACvC,IAAI,UAAU,WAAW,IAAG,KAAK,CAAC,aAAa,KAAK,GAClD,OAAO;CAET,OAAO,IAAI,MAAM,QAAQ,MAAM,MAAI,EAAE;AACvC;;;;;;;AAQA,SAAS,qBAAqB,KAAqB;CACjD,OAAO,IAAI,SAAS,IAAI,IAAI,MAAM,GAAG,IAAI;AAC3C;;;;;;;;;;;AAYA,SAAgB,cACd,MACA,SACQ;CACR,MAAM,EAAE,UAAU,GAAG,cAAc,WAAW,CAAC;CAC/C,MAAM,OAAO;EAAE,GAAG;EAAuB,QAAQ;EAAO,GAAG;CAAU;CAErE,IAAI,KAAK,WAAW,GAAG,OAAO;CAE9B,IAAI,gBAAgB;CACpB,IAAI,UACF,gBAAgB,KAAK,KAAK,QAAQ,iBAAiB,GAAG,CAAC;CAGzD,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,MAAM,UAAU,SAAS;CAC/B,IAAI,SAAS,qBAAqB,uBAAuB,eAAe,KAAK,SAAS,CAAC;CAEvF,IAAI,KAAK,WAAW,KAAK,YAAY,MACnC,SAAS,OAAO,QAAQ,OAAO,KAAK,OAAO;CAG7C,OAAO;AACT;;;;;;AAOA,SAAgB,WAAW,KAAsB;CAC/C,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC,WAAW,GAAG,OAAO;CAE5C,MAAM,UAAU,IAAI,KAAK;CAGzB,IAAI,CAAC,QAAQ,SAAS,GAAG,GAAG,OAAO;CAEnC,IAAI;EACF,gBAAgB,OAAO;CACzB,QAAQ;EACN,OAAO;CACT;CAEA,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAAiB,OAAwB;CACvD,MAAM,MAAM,OAAO,KAAK;CAExB,IAAI,aAAa,KAAK,GAAG,GACvB,OAAO,IAAI;CAEb,OAAO;AACT;;;;;;;AAQA,SAAgB,iBAAiB,QAA6B;CAC5D,OAAO,OAAO,IAAI,gBAAgB;AACpC;;;;;;;AAQA,SAAgB,kBACd,KACwB;CACxB,MAAM,YAAoC,CAAC;CAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAC3C,UAAU,OAAO,iBAAiB,KAAK;CAEzC,OAAO;AACT"}
@@ -0,0 +1,83 @@
1
+ //#region src/json/canonical.d.ts
2
+ /**
3
+ * Canonical JSON — the encoding HDF uses for content-addressed checksums, such
4
+ * as the amendment chain's `previousChecksum`.
5
+ *
6
+ * The form is Go's `encoding/json` object encoding of the value's JSON shape:
7
+ *
8
+ * - object keys sorted by UTF-8 byte order;
9
+ * - `<`, `>`, `&`, U+2028 and U+2029 escaped;
10
+ * - keys whose value is `null` removed, so a document that spells an absent
11
+ * optional field as an explicit null hashes the same as one that omits it;
12
+ * - unpaired surrogates replaced with U+FFFD, in keys and string values alike,
13
+ * because Go's `json.Unmarshal` substitutes them at parse time while JS's
14
+ * `JSON.parse` preserves them — without this the two disagree on any string
15
+ * carrying a truncated UTF-16 sequence;
16
+ * - negative zero emitted as `-0`, which `JSON.stringify` renders as `0`;
17
+ * - NaN and Infinity rejected, as Go's encoder rejects them.
18
+ *
19
+ * Every one of those points is a place the two languages would otherwise
20
+ * diverge, and a divergence means a chain written by one is reported as
21
+ * tampered by the other. The Go counterpart is `hdfutil.CanonicalJSON`, and the
22
+ * two are pinned against shared vectors in
23
+ * `hdf-utilities/testdata/canonical-json-vectors.json`.
24
+ */
25
+ /** Returns the canonical JSON encoding of a value. */
26
+ declare function canonicalJson(value: unknown): string;
27
+ /** Returns the hex-encoded SHA-256 of a value's canonical JSON encoding. */
28
+ declare function canonicalChecksum(value: unknown): Promise<string>;
29
+ //#endregion
30
+ //#region src/json/index.d.ts
31
+ /**
32
+ * Options for JSON stringification
33
+ */
34
+ interface StringifyOptions {
35
+ /** Enable pretty printing with indentation */
36
+ pretty?: boolean;
37
+ /** Number of spaces for indentation (default: 2) */
38
+ indent?: number;
39
+ }
40
+ /**
41
+ * Safely parse a JSON string
42
+ * @param input - JSON string to parse
43
+ * @returns Parsed JSON value
44
+ * @throws Error if input is not valid JSON
45
+ */
46
+ declare function parseJSON<T = unknown>(input: string): T;
47
+ /**
48
+ * Safely stringify a value to JSON
49
+ * @param value - Value to stringify
50
+ * @param options - Stringification options
51
+ * @returns JSON string
52
+ * @throws Error if value contains circular references or cannot be stringified
53
+ */
54
+ declare function stringifyJSON(value: unknown, options?: StringifyOptions): string;
55
+ /**
56
+ * Check if a string is valid JSON
57
+ * @param input - String to validate
58
+ * @returns true if input is valid JSON, false otherwise
59
+ */
60
+ declare function isValidJSON(input: unknown): boolean;
61
+ /**
62
+ * Render a FINITE JSON number as text the way Go's
63
+ * strconv.FormatFloat(v, 'f', -1, 64) does: shortest round-trip digits,
64
+ * positional always, never exponent notation.
65
+ *
66
+ * JavaScript's own Number-to-string switches to exponent at and above 1e21 and
67
+ * below 1e-6, and drops the sign of negative zero, so a converter that emits
68
+ * String(n) diverges from its Go peer at both magnitude extremes and at -0.
69
+ * Expansion works on the digit string rather than toFixed, which caps at 100
70
+ * fraction digits and so cannot render the smallest denormals at all.
71
+ *
72
+ * Non-finite values are passed through as JavaScript renders them and do NOT
73
+ * match Go: this returns "Infinity"/"-Infinity" where FormatFloat returns
74
+ * "+Inf"/"-Inf" ("NaN" agrees). JSON has no literal for any of them, but
75
+ * JSON.parse manufactures Infinity from an overflow literal such as 1e400, so
76
+ * the case is reachable. The Go peer never renders that value at all -- its
77
+ * ParseFloat fails and it emits the original token text instead -- which is a
78
+ * documented cross-language divergence, not something this helper can close.
79
+ */
80
+ declare function formatJsonNumber(value: number): string;
81
+ //#endregion
82
+ export { stringifyJSON as a, parseJSON as i, formatJsonNumber as n, canonicalChecksum as o, isValidJSON as r, canonicalJson as s, StringifyOptions as t };
83
+ //# sourceMappingURL=index-i4P63h9e.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-i4P63h9e.d.ts","names":[],"sources":["../src/json/canonical.ts","../src/json/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;iBAkIgB,cAAc;;iBAKR,kBAAkB,iBAAiB;;;;;;UCjIxC;;EAEf;;EAEA;;;;;;;;iBASc,UAAU,aAAa,gBAAgB;;;;;;;;iBAuBvC,cAAc,gBAAgB,UAAS;;;;;;iBAkBvC,YAAY;;;;;;;;;;;;;;;;;;;;iBAoCZ,iBAAiB"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { extractColumn, findRows, findValuesByKey } from "./object/index.js";
2
2
  import { CsvBuildOptions, CsvParseOptions, buildCsv, buildCsvArray, isValidCsv, parseCsv, parseCsvArray, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue } from "./csv/index.js";
3
3
  import { HashAlgorithm, HashEncoding, HashOptions, generateHash, hashObject, sha256, sha512, verifyHash } from "./hash/index.js";
4
- import { StringifyOptions, isValidJSON, parseJSON, stringifyJSON } from "./json/index.js";
5
- import { buildXml, extractTextFromXml, isValidXml, parseXml, parseXmlWithArrays } from "./xml/index.js";
6
- import { formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction } from "./string/index.js";
4
+ import { a as stringifyJSON, i as parseJSON, n as formatJsonNumber, o as canonicalChecksum, r as isValidJSON, s as canonicalJson, t as StringifyOptions } from "./index-i4P63h9e.js";
5
+ import { buildXml, extractTextFromXml, isValidXml, isXmlChar, parseXml, parseXmlWithArrays, xmlSafeText } from "./xml/index.js";
6
+ import { encodeBase64Utf8, formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction } from "./string/index.js";
7
7
  //#region src/severity/index.d.ts
8
8
  /**
9
9
  * Severity-to-impact mapping utilities.
@@ -385,5 +385,5 @@ declare const DEFAULT_MAX_INPUT_SIZE: number;
385
385
  */
386
386
  declare function validateInputSize(input: string | Uint8Array, maxSize?: number): void;
387
387
  //#endregion
388
- export { type CpePart, type CsvBuildOptions, type CsvParseOptions, type CvssScore, type CvssValidationResult, DEFAULT_MAX_INPUT_SIZE, type EffectiveStatusInput, type HashAlgorithm, type HashEncoding, type HashOptions, type ParsedCpe, type ParsedCvssVector, type ParsedPurl, STATUS_SEVERITY_ORDER, type StatusOverrideInput, type StatusSeverity, type StringifyOptions, buildCsv, buildCsvArray, buildXml, computeCvss40Score, computeCvssScore, computeEffectiveStatus, cvss40MacroVector, cvssScoreToSeverity, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, formatTimestamp, formatTimestampSeconds, generateHash, governingOverrideIndex, governingStatusOverride, governingStatusOverrideIndex, hashObject, impactToSeverity, isUnratedSeverity, isValidCsv, isValidJSON, isValidXml, normalizeHdfTimestamps, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, severityToImpactWithAliases, sha256, sha512, statusRank, stringifyJSON, stripHtml, trimUtcFraction, validateCvssVector, validateInputSize, verifyHash, worstStatus };
388
+ export { type CpePart, type CsvBuildOptions, type CsvParseOptions, type CvssScore, type CvssValidationResult, DEFAULT_MAX_INPUT_SIZE, type EffectiveStatusInput, type HashAlgorithm, type HashEncoding, type HashOptions, type ParsedCpe, type ParsedCvssVector, type ParsedPurl, STATUS_SEVERITY_ORDER, type StatusOverrideInput, type StatusSeverity, type StringifyOptions, buildCsv, buildCsvArray, buildXml, canonicalChecksum, canonicalJson, computeCvss40Score, computeCvssScore, computeEffectiveStatus, cvss40MacroVector, cvssScoreToSeverity, encodeBase64Utf8, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, formatJsonNumber, formatTimestamp, formatTimestampSeconds, generateHash, governingOverrideIndex, governingStatusOverride, governingStatusOverrideIndex, hashObject, impactToSeverity, isUnratedSeverity, isValidCsv, isValidJSON, isValidXml, isXmlChar, normalizeHdfTimestamps, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, severityToImpactWithAliases, sha256, sha512, statusRank, stringifyJSON, stripHtml, trimUtcFraction, validateCvssVector, validateInputSize, verifyHash, worstStatus, xmlSafeText };
389
389
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
+ import { a as canonicalChecksum, i as stringifyJSON, n as isValidJSON, o as canonicalJson, r as parseJSON, t as formatJsonNumber } from "./json-DdbPTX2g.js";
1
2
  import { extractColumn, findRows, findValuesByKey } from "./object/index.js";
2
- import { isValidJSON, parseJSON, stringifyJSON } from "./json/index.js";
3
3
  import { generateHash, hashObject, sha256, sha512, verifyHash } from "./hash/index.js";
4
- import { buildXml, extractTextFromXml, isValidXml, parseXml, parseXmlWithArrays } from "./xml/index.js";
4
+ import { buildXml, extractTextFromXml, isValidXml, isXmlChar, parseXml, parseXmlWithArrays, xmlSafeText } from "./xml/index.js";
5
5
  import { buildCsv, buildCsvArray, isValidCsv, parseCsv, parseCsvArray, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue } from "./csv/index.js";
6
- import { formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction } from "./string/index.js";
6
+ import { encodeBase64Utf8, formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction } from "./string/index.js";
7
7
  //#region src/severity/index.ts
8
8
  /**
9
9
  * Severity-to-impact mapping utilities.
@@ -1808,10 +1808,37 @@ const DEFAULT_MAX_INPUT_SIZE = 50 * 1024 * 1024;
1808
1808
  */
1809
1809
  function validateInputSize(input, maxSize = 0) {
1810
1810
  const limit = maxSize > 0 ? maxSize : DEFAULT_MAX_INPUT_SIZE;
1811
- const len = typeof input === "string" ? new TextEncoder().encode(input).length : input.length;
1812
- if (len > limit) throw new Error(`input exceeds maximum allowed size of ${limit} bytes (${len} bytes provided)`);
1811
+ if (typeof input !== "string") {
1812
+ if (input.length > limit) throw tooLarge(limit, input.length);
1813
+ return;
1814
+ }
1815
+ if (input.length * 3 <= limit) return;
1816
+ const len = utf8ByteLength(input);
1817
+ if (len > limit) throw tooLarge(limit, len);
1818
+ }
1819
+ function tooLarge(limit, len) {
1820
+ return /* @__PURE__ */ new Error(`input exceeds maximum allowed size of ${limit} bytes (${len} bytes provided)`);
1821
+ }
1822
+ /**
1823
+ * UTF-8 byte length, counted rather than encoded: a TextEncoder pass would
1824
+ * allocate a full copy of the very input this guard exists to reject.
1825
+ * An unpaired surrogate counts as the three-byte replacement character, which
1826
+ * is what TextEncoder emits for one.
1827
+ */
1828
+ function utf8ByteLength(s) {
1829
+ let bytes = 0;
1830
+ for (let i = 0; i < s.length; i++) {
1831
+ const c = s.charCodeAt(i);
1832
+ if (c < 128) bytes += 1;
1833
+ else if (c < 2048) bytes += 2;
1834
+ else if (c >= 55296 && c <= 56319 && (s.charCodeAt(i + 1) & 64512) === 56320) {
1835
+ bytes += 4;
1836
+ i++;
1837
+ } else bytes += 3;
1838
+ }
1839
+ return bytes;
1813
1840
  }
1814
1841
  //#endregion
1815
- export { DEFAULT_MAX_INPUT_SIZE, STATUS_SEVERITY_ORDER, buildCsv, buildCsvArray, buildXml, computeCvss40Score, computeCvssScore, computeEffectiveStatus, cvss40MacroVector, cvssScoreToSeverity, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, formatTimestamp, formatTimestampSeconds, generateHash, governingOverrideIndex, governingStatusOverride, governingStatusOverrideIndex, hashObject, impactToSeverity, isUnratedSeverity, isValidCsv, isValidJSON, isValidXml, normalizeHdfTimestamps, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, severityToImpactWithAliases, sha256, sha512, statusRank, stringifyJSON, stripHtml, trimUtcFraction, validateCvssVector, validateInputSize, verifyHash, worstStatus };
1842
+ export { DEFAULT_MAX_INPUT_SIZE, STATUS_SEVERITY_ORDER, buildCsv, buildCsvArray, buildXml, canonicalChecksum, canonicalJson, computeCvss40Score, computeCvssScore, computeEffectiveStatus, cvss40MacroVector, cvssScoreToSeverity, encodeBase64Utf8, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, formatJsonNumber, formatTimestamp, formatTimestampSeconds, generateHash, governingOverrideIndex, governingStatusOverride, governingStatusOverrideIndex, hashObject, impactToSeverity, isUnratedSeverity, isValidCsv, isValidJSON, isValidXml, isXmlChar, normalizeHdfTimestamps, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, severityToImpactWithAliases, sha256, sha512, statusRank, stringifyJSON, stripHtml, trimUtcFraction, validateCvssVector, validateInputSize, verifyHash, worstStatus, xmlSafeText };
1816
1843
 
1817
1844
  //# sourceMappingURL=index.js.map