@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 +27 -0
- package/dist/csv/index.d.ts.map +1 -1
- package/dist/csv/index.js +50 -1
- package/dist/csv/index.js.map +1 -1
- package/dist/index-i4P63h9e.d.ts +83 -0
- package/dist/index-i4P63h9e.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +33 -6
- package/dist/index.js.map +1 -1
- package/dist/json/index.d.ts +2 -34
- package/dist/json/index.js +2 -51
- package/dist/json-DdbPTX2g.js +193 -0
- package/dist/json-DdbPTX2g.js.map +1 -0
- package/dist/string/index.d.ts +7 -1
- package/dist/string/index.d.ts.map +1 -1
- package/dist/string/index.js +13 -1
- package/dist/string/index.js.map +1 -1
- package/dist/xml/index.d.ts +18 -3
- package/dist/xml/index.d.ts.map +1 -1
- package/dist/xml/index.js +25 -2
- package/dist/xml/index.js.map +1 -1
- package/package.json +2 -4
- package/dist/json/index.d.ts.map +0 -1
- package/dist/json/index.js.map +0 -1
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 |
|
package/dist/csv/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/csv/index.ts"],"mappings":";;;;;
|
|
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
|
-
|
|
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
|
}
|
package/dist/csv/index.js.map
CHANGED
|
@@ -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":"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"}
|
|
@@ -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 {
|
|
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
|
-
|
|
1812
|
-
|
|
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
|