@mitre/hdf-utilities 3.6.0-rc.1 → 3.6.0-rc.3
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 +31 -0
- package/dist/csv/index.d.ts.map +1 -1
- package/dist/csv/index.js +42 -0
- package/dist/csv/index.js.map +1 -1
- package/dist/index-b6zg3fBr.d.ts +63 -0
- package/dist/index-b6zg3fBr.d.ts.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/json/index.d.ts +2 -34
- package/dist/json/index.js +2 -51
- package/dist/json-DIcA65Nf.js +156 -0
- package/dist/json-DIcA65Nf.js.map +1 -0
- 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,37 @@ 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
|
+
`hash` and `json` have no Go counterpart because Go's standard library already
|
|
25
|
+
provides them: `crypto/sha256`, `encoding/json`. Wrapping those in Go would add a
|
|
26
|
+
layer with no decision in it. The TypeScript side needs wrappers because
|
|
27
|
+
JavaScript ships no equivalent, and the wrapper is where safe defaults get
|
|
28
|
+
centralised.
|
|
29
|
+
|
|
30
|
+
`csv` exists in both, but the two surfaces are deliberately different sizes: the
|
|
31
|
+
TypeScript module parses and builds CSV, while the Go one carries only the
|
|
32
|
+
formula-injection policy and leaves writing to `encoding/csv`.
|
|
33
|
+
|
|
34
|
+
XML shows the pattern most clearly. Go's `xml.go` exports only
|
|
35
|
+
`ContainsXMLEntityDeclarations` and `ExtractXMLRootElement` — an XXE-safety
|
|
36
|
+
policy and a domain helper — and leaves parsing to `encoding/xml`. The
|
|
37
|
+
TypeScript `xml/` module wraps fast-xml-parser and carries the same policy in its
|
|
38
|
+
defaults.
|
|
39
|
+
|
|
40
|
+
So the test for whether something belongs in both languages is not "does the
|
|
41
|
+
other one have it" but "is it a decision, or is it plumbing". Formula-injection
|
|
42
|
+
sanitizing (`SanitizeCSVValue` / `sanitizeCsvValue`) is a decision: which leading
|
|
43
|
+
characters a spreadsheet will execute, and what neutralises them. It exists in
|
|
44
|
+
both, pinned to one shared table in `testdata/csv-sanitize-cases.json`, because a
|
|
45
|
+
trigger added on one side alone would leave the other exporting CSVs a
|
|
46
|
+
spreadsheet runs. CSV *writing* is plumbing, and stays with each language's
|
|
47
|
+
serializer.
|
|
48
|
+
|
|
18
49
|
### hdf-utilities vs. hdf-validators
|
|
19
50
|
|
|
20
51
|
| 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;;;;;;;;;;;;iBA0GzB,cACd,kBACA,UAAU,QAAQ;EAAqB;;;;;;;iBA2BzB,WAAW;;;;;;;;;;iBA0BX,iBAAiB;;;;;;;iBAejB,iBAAiB;;;;;;;iBAUjB,kBAAkB,UAAU,yBAC1C,KAAK,IACJ"}
|
package/dist/csv/index.js
CHANGED
|
@@ -129,11 +129,53 @@ 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 dsv.parseRows(csv).map((row) => row.map((field) => requote(field, dsv)).join(delimiter)).join("\n");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Go's encoding/csv fieldNeedsQuotes, minus the conditions d3-dsv already
|
|
160
|
+
* covers: a leading Unicode space, or the literal `\.`.
|
|
161
|
+
*
|
|
162
|
+
* The class is Go's unicode.IsSpace, spelled out rather than borrowed from
|
|
163
|
+
* JavaScript's \s, which is not the same set: \s omits U+0085 (NEL) and adds
|
|
164
|
+
* U+FEFF, which Unicode 4.0.1 removed from White_Space. Borrowing \s left a
|
|
165
|
+
* field led by U+0085 unquoted here but quoted in Go, and the reverse for
|
|
166
|
+
* U+FEFF.
|
|
167
|
+
*/
|
|
168
|
+
const GO_LEADING_SPACE = /^[\t\n\v\f\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/;
|
|
169
|
+
function goWouldQuote(field) {
|
|
170
|
+
return field === "\\." || GO_LEADING_SPACE.test(field);
|
|
171
|
+
}
|
|
172
|
+
/** One field, formatted by d3-dsv, then quoted if Go would quote it and d3-dsv did not. */
|
|
173
|
+
function requote(field, dsv) {
|
|
174
|
+
const formatted = dsv.formatValue(field);
|
|
175
|
+
if (formatted.startsWith("\"") || !goWouldQuote(field)) return formatted;
|
|
176
|
+
return `"${field.replace(/"/g, "\"\"")}"`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
137
179
|
* Terminate the final record with a newline. d3-dsv leaves it off, but Go's
|
|
138
180
|
* encoding/csv (which the Go converters use) always writes it, so without this
|
|
139
181
|
* every TS CSV would differ from its Go twin by exactly one trailing byte.
|
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 dsv\n .parseRows(csv)\n .map((row) => row.map((field) => requote(field, dsv)).join(delimiter))\n .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 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":";;;;;;;;;;;;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,IACJ,UAAU,GAAG,CAAC,CACd,KAAK,QAAQ,IAAI,KAAK,UAAU,QAAQ,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CACrE,KAAK,IAAI;AACd;;;;;;;;;;;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;CAIzD,IAAI,SAAS,qBADD,UAAU,KAAK,aAAa,GACJ,CAAC,CAAC,WAAW,aAAa,CAAC;CAE/D,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,63 @@
|
|
|
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
|
+
//#endregion
|
|
62
|
+
export { canonicalChecksum as a, stringifyJSON as i, isValidJSON as n, canonicalJson as o, parseJSON as r, StringifyOptions as t };
|
|
63
|
+
//# sourceMappingURL=index-b6zg3fBr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-b6zg3fBr.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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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";
|
|
4
|
+
import { a as canonicalChecksum, i as stringifyJSON, n as isValidJSON, o as canonicalJson, r as parseJSON, t as StringifyOptions } from "./index-b6zg3fBr.js";
|
|
5
|
+
import { buildXml, extractTextFromXml, isValidXml, isXmlChar, parseXml, parseXmlWithArrays, xmlSafeText } from "./xml/index.js";
|
|
6
6
|
import { formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction } from "./string/index.js";
|
|
7
7
|
//#region src/severity/index.d.ts
|
|
8
8
|
/**
|
|
@@ -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, 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, 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,7 +1,7 @@
|
|
|
1
|
+
import { a as canonicalJson, i as canonicalChecksum, n as parseJSON, r as stringifyJSON, t as isValidJSON } from "./json-DIcA65Nf.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
6
|
import { formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction } from "./string/index.js";
|
|
7
7
|
//#region src/severity/index.ts
|
|
@@ -1812,6 +1812,6 @@ function validateInputSize(input, maxSize = 0) {
|
|
|
1812
1812
|
if (len > limit) throw new Error(`input exceeds maximum allowed size of ${limit} bytes (${len} bytes provided)`);
|
|
1813
1813
|
}
|
|
1814
1814
|
//#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 };
|
|
1815
|
+
export { DEFAULT_MAX_INPUT_SIZE, STATUS_SEVERITY_ORDER, buildCsv, buildCsvArray, buildXml, canonicalChecksum, canonicalJson, 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, 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
1816
|
|
|
1817
1817
|
//# sourceMappingURL=index.js.map
|
package/dist/json/index.d.ts
CHANGED
|
@@ -1,35 +1,3 @@
|
|
|
1
1
|
import { findValuesByKey } from "../object/index.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Options for JSON stringification
|
|
5
|
-
*/
|
|
6
|
-
interface StringifyOptions {
|
|
7
|
-
/** Enable pretty printing with indentation */
|
|
8
|
-
pretty?: boolean;
|
|
9
|
-
/** Number of spaces for indentation (default: 2) */
|
|
10
|
-
indent?: number;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Safely parse a JSON string
|
|
14
|
-
* @param input - JSON string to parse
|
|
15
|
-
* @returns Parsed JSON value
|
|
16
|
-
* @throws Error if input is not valid JSON
|
|
17
|
-
*/
|
|
18
|
-
declare function parseJSON<T = unknown>(input: string): T;
|
|
19
|
-
/**
|
|
20
|
-
* Safely stringify a value to JSON
|
|
21
|
-
* @param value - Value to stringify
|
|
22
|
-
* @param options - Stringification options
|
|
23
|
-
* @returns JSON string
|
|
24
|
-
* @throws Error if value contains circular references or cannot be stringified
|
|
25
|
-
*/
|
|
26
|
-
declare function stringifyJSON(value: unknown, options?: StringifyOptions): string;
|
|
27
|
-
/**
|
|
28
|
-
* Check if a string is valid JSON
|
|
29
|
-
* @param input - String to validate
|
|
30
|
-
* @returns true if input is valid JSON, false otherwise
|
|
31
|
-
*/
|
|
32
|
-
declare function isValidJSON(input: unknown): boolean;
|
|
33
|
-
//#endregion
|
|
34
|
-
export { StringifyOptions, findValuesByKey as findJsonValues, isValidJSON, parseJSON, stringifyJSON };
|
|
35
|
-
//# sourceMappingURL=index.d.ts.map
|
|
2
|
+
import { a as canonicalChecksum, i as stringifyJSON, n as isValidJSON, o as canonicalJson, r as parseJSON, t as StringifyOptions } from "../index-b6zg3fBr.js";
|
|
3
|
+
export { StringifyOptions, canonicalChecksum, canonicalJson, findValuesByKey as findJsonValues, isValidJSON, parseJSON, stringifyJSON };
|
package/dist/json/index.js
CHANGED
|
@@ -1,52 +1,3 @@
|
|
|
1
|
+
import { a as canonicalJson, i as canonicalChecksum, n as parseJSON, r as stringifyJSON, t as isValidJSON } from "../json-DIcA65Nf.js";
|
|
1
2
|
import { findValuesByKey } from "../object/index.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Safely parse a JSON string
|
|
5
|
-
* @param input - JSON string to parse
|
|
6
|
-
* @returns Parsed JSON value
|
|
7
|
-
* @throws Error if input is not valid JSON
|
|
8
|
-
*/
|
|
9
|
-
function parseJSON(input) {
|
|
10
|
-
if (typeof input !== "string") throw new Error("Input must be a string");
|
|
11
|
-
if (input.trim() === "") throw new Error("Input cannot be empty");
|
|
12
|
-
try {
|
|
13
|
-
return JSON.parse(input);
|
|
14
|
-
} catch (error) {
|
|
15
|
-
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Safely stringify a value to JSON
|
|
20
|
-
* @param value - Value to stringify
|
|
21
|
-
* @param options - Stringification options
|
|
22
|
-
* @returns JSON string
|
|
23
|
-
* @throws Error if value contains circular references or cannot be stringified
|
|
24
|
-
*/
|
|
25
|
-
function stringifyJSON(value, options = {}) {
|
|
26
|
-
const { pretty = false, indent = 2 } = options;
|
|
27
|
-
try {
|
|
28
|
-
if (pretty) return JSON.stringify(value, null, indent);
|
|
29
|
-
return JSON.stringify(value);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
throw new Error(`Failed to stringify JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Check if a string is valid JSON
|
|
36
|
-
* @param input - String to validate
|
|
37
|
-
* @returns true if input is valid JSON, false otherwise
|
|
38
|
-
*/
|
|
39
|
-
function isValidJSON(input) {
|
|
40
|
-
if (typeof input !== "string") return false;
|
|
41
|
-
if (input.trim() === "") return false;
|
|
42
|
-
try {
|
|
43
|
-
JSON.parse(input);
|
|
44
|
-
return true;
|
|
45
|
-
} catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
//#endregion
|
|
50
|
-
export { findValuesByKey as findJsonValues, isValidJSON, parseJSON, stringifyJSON };
|
|
51
|
-
|
|
52
|
-
//# sourceMappingURL=index.js.map
|
|
3
|
+
export { canonicalChecksum, canonicalJson, findValuesByKey as findJsonValues, isValidJSON, parseJSON, stringifyJSON };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//#region src/json/canonical.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
|
+
const LINE_SEPARATOR = "\u2028";
|
|
26
|
+
const PARAGRAPH_SEPARATOR = "\u2029";
|
|
27
|
+
/** Characters Go's encoder escapes that `JSON.stringify` leaves literal. */
|
|
28
|
+
const GO_ESCAPES = {
|
|
29
|
+
"<": "\\u003c",
|
|
30
|
+
">": "\\u003e",
|
|
31
|
+
"&": "\\u0026",
|
|
32
|
+
[LINE_SEPARATOR]: "\\u2028",
|
|
33
|
+
[PARAGRAPH_SEPARATOR]: "\\u2029"
|
|
34
|
+
};
|
|
35
|
+
const GO_ESCAPE_PATTERN = /[<>&\u2028\u2029]/g;
|
|
36
|
+
/** An unpaired high or low surrogate — a truncated UTF-16 sequence. */
|
|
37
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
38
|
+
const utf8 = new TextEncoder();
|
|
39
|
+
/**
|
|
40
|
+
* Compares two keys by their UTF-8 bytes, which is how Go orders map keys.
|
|
41
|
+
* `Array.prototype.sort` compares UTF-16 code units instead, and the two
|
|
42
|
+
* disagree for characters outside the BMP.
|
|
43
|
+
*/
|
|
44
|
+
function compareUtf8(a, b) {
|
|
45
|
+
const left = utf8.encode(a);
|
|
46
|
+
const right = utf8.encode(b);
|
|
47
|
+
const shared = Math.min(left.length, right.length);
|
|
48
|
+
for (let i = 0; i < shared; i++) {
|
|
49
|
+
const leftByte = left[i] ?? 0;
|
|
50
|
+
const rightByte = right[i] ?? 0;
|
|
51
|
+
if (leftByte !== rightByte) return leftByte - rightByte;
|
|
52
|
+
}
|
|
53
|
+
return left.length - right.length;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Replaces unpaired surrogates with U+FFFD, matching what Go's `json.Unmarshal`
|
|
57
|
+
* does when it decodes the same bytes. Applied to keys as well as values, and
|
|
58
|
+
* before sorting, so a key that normalizes onto an existing one collides the
|
|
59
|
+
* same way it would in a Go map (last occurrence wins).
|
|
60
|
+
*/
|
|
61
|
+
function replaceLoneSurrogates(value) {
|
|
62
|
+
return value.replace(LONE_SURROGATE, "�");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalizes a value into the shape that gets hashed. This replaces a
|
|
66
|
+
* `JSON.parse(JSON.stringify(...))` round-trip, which would have been simpler
|
|
67
|
+
* but silently destroys negative zero.
|
|
68
|
+
*/
|
|
69
|
+
function normalize(value) {
|
|
70
|
+
if (value === null || value === void 0) return value;
|
|
71
|
+
if (typeof value === "object") {
|
|
72
|
+
const withToJson = value;
|
|
73
|
+
if (typeof withToJson.toJSON === "function") return normalize(withToJson.toJSON());
|
|
74
|
+
if (Array.isArray(value)) return value.map((element) => element === void 0 ? null : normalize(element));
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
77
|
+
if (nested === null || nested === void 0) continue;
|
|
78
|
+
out[replaceLoneSurrogates(key)] = normalize(nested);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === "string") return replaceLoneSurrogates(value);
|
|
83
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError(`value is not JSON-encodable: ${String(value)}`);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
/** Serializes with object keys in UTF-8 byte order at every level. */
|
|
87
|
+
function serialize(value) {
|
|
88
|
+
if (Array.isArray(value)) return `[${value.map(serialize).join(",")}]`;
|
|
89
|
+
if (value !== null && typeof value === "object") return `{${Object.keys(value).sort(compareUtf8).map((key) => `${serialize(key)}:${serialize(value[key])}`).join(",")}}`;
|
|
90
|
+
if (typeof value === "number" && Object.is(value, -0)) return "-0";
|
|
91
|
+
const encoded = JSON.stringify(value);
|
|
92
|
+
if (encoded === void 0) throw new TypeError("value is not JSON-encodable");
|
|
93
|
+
return encoded.replace(GO_ESCAPE_PATTERN, (c) => GO_ESCAPES[c] ?? c);
|
|
94
|
+
}
|
|
95
|
+
/** Returns the canonical JSON encoding of a value. */
|
|
96
|
+
function canonicalJson(value) {
|
|
97
|
+
return serialize(normalize(value ?? null));
|
|
98
|
+
}
|
|
99
|
+
/** Returns the hex-encoded SHA-256 of a value's canonical JSON encoding. */
|
|
100
|
+
async function canonicalChecksum(value) {
|
|
101
|
+
const bytes = utf8.encode(canonicalJson(value));
|
|
102
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
103
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/json/index.ts
|
|
107
|
+
/**
|
|
108
|
+
* Safely parse a JSON string
|
|
109
|
+
* @param input - JSON string to parse
|
|
110
|
+
* @returns Parsed JSON value
|
|
111
|
+
* @throws Error if input is not valid JSON
|
|
112
|
+
*/
|
|
113
|
+
function parseJSON(input) {
|
|
114
|
+
if (typeof input !== "string") throw new Error("Input must be a string");
|
|
115
|
+
if (input.trim() === "") throw new Error("Input cannot be empty");
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(input);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Safely stringify a value to JSON
|
|
124
|
+
* @param value - Value to stringify
|
|
125
|
+
* @param options - Stringification options
|
|
126
|
+
* @returns JSON string
|
|
127
|
+
* @throws Error if value contains circular references or cannot be stringified
|
|
128
|
+
*/
|
|
129
|
+
function stringifyJSON(value, options = {}) {
|
|
130
|
+
const { pretty = false, indent = 2 } = options;
|
|
131
|
+
try {
|
|
132
|
+
if (pretty) return JSON.stringify(value, null, indent);
|
|
133
|
+
return JSON.stringify(value);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new Error(`Failed to stringify JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if a string is valid JSON
|
|
140
|
+
* @param input - String to validate
|
|
141
|
+
* @returns true if input is valid JSON, false otherwise
|
|
142
|
+
*/
|
|
143
|
+
function isValidJSON(input) {
|
|
144
|
+
if (typeof input !== "string") return false;
|
|
145
|
+
if (input.trim() === "") return false;
|
|
146
|
+
try {
|
|
147
|
+
JSON.parse(input);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { canonicalJson as a, canonicalChecksum as i, parseJSON as n, stringifyJSON as r, isValidJSON as t };
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=json-DIcA65Nf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-DIcA65Nf.js","names":[],"sources":["../src/json/canonical.ts","../src/json/index.ts"],"sourcesContent":["/**\n * Canonical JSON — the encoding HDF uses for content-addressed checksums, such\n * as the amendment chain's `previousChecksum`.\n *\n * The form is Go's `encoding/json` object encoding of the value's JSON shape:\n *\n * - object keys sorted by UTF-8 byte order;\n * - `<`, `>`, `&`, U+2028 and U+2029 escaped;\n * - keys whose value is `null` removed, so a document that spells an absent\n * optional field as an explicit null hashes the same as one that omits it;\n * - unpaired surrogates replaced with U+FFFD, in keys and string values alike,\n * because Go's `json.Unmarshal` substitutes them at parse time while JS's\n * `JSON.parse` preserves them — without this the two disagree on any string\n * carrying a truncated UTF-16 sequence;\n * - negative zero emitted as `-0`, which `JSON.stringify` renders as `0`;\n * - NaN and Infinity rejected, as Go's encoder rejects them.\n *\n * Every one of those points is a place the two languages would otherwise\n * diverge, and a divergence means a chain written by one is reported as\n * tampered by the other. The Go counterpart is `hdfutil.CanonicalJSON`, and the\n * two are pinned against shared vectors in\n * `hdf-utilities/testdata/canonical-json-vectors.json`.\n */\n\nconst LINE_SEPARATOR = '\\u2028';\nconst PARAGRAPH_SEPARATOR = '\\u2029';\n\n/** Characters Go's encoder escapes that `JSON.stringify` leaves literal. */\nconst GO_ESCAPES: Record<string, string> = {\n '<': '\\\\u003c',\n '>': '\\\\u003e',\n '&': '\\\\u0026',\n [LINE_SEPARATOR]: '\\\\u2028',\n [PARAGRAPH_SEPARATOR]: '\\\\u2029',\n};\n\nconst GO_ESCAPE_PATTERN = /[<>&\\u2028\\u2029]/g;\n\n/** An unpaired high or low surrogate — a truncated UTF-16 sequence. */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g;\n\nconst utf8 = new TextEncoder();\n\n/**\n * Compares two keys by their UTF-8 bytes, which is how Go orders map keys.\n * `Array.prototype.sort` compares UTF-16 code units instead, and the two\n * disagree for characters outside the BMP.\n */\nfunction compareUtf8(a: string, b: string): number {\n const left = utf8.encode(a);\n const right = utf8.encode(b);\n const shared = Math.min(left.length, right.length);\n for (let i = 0; i < shared; i++) {\n // Indices below the shared length are always present; the fallbacks keep\n // this total for the type checker without asserting.\n const leftByte = left[i] ?? 0;\n const rightByte = right[i] ?? 0;\n if (leftByte !== rightByte) return leftByte - rightByte;\n }\n return left.length - right.length;\n}\n\n/**\n * Replaces unpaired surrogates with U+FFFD, matching what Go's `json.Unmarshal`\n * does when it decodes the same bytes. Applied to keys as well as values, and\n * before sorting, so a key that normalizes onto an existing one collides the\n * same way it would in a Go map (last occurrence wins).\n */\nfunction replaceLoneSurrogates(value: string): string {\n return value.replace(LONE_SURROGATE, '\\uFFFD');\n}\n\n/**\n * Normalizes a value into the shape that gets hashed. This replaces a\n * `JSON.parse(JSON.stringify(...))` round-trip, which would have been simpler\n * but silently destroys negative zero.\n */\nfunction normalize(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n\n if (typeof value === 'object') {\n const withToJson = value as { toJSON?: () => unknown };\n // Dates and anything else that defines toJSON serialize through it, exactly\n // as JSON.stringify would.\n if (typeof withToJson.toJSON === 'function') return normalize(withToJson.toJSON());\n\n if (Array.isArray(value)) {\n // JSON has no undefined: in an array position it becomes null.\n return value.map((element) => (element === undefined ? null : normalize(element)));\n }\n\n const out: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {\n if (nested === null || nested === undefined) continue;\n out[replaceLoneSurrogates(key)] = normalize(nested);\n }\n return out;\n }\n\n if (typeof value === 'string') return replaceLoneSurrogates(value);\n\n if (typeof value === 'number' && !Number.isFinite(value)) {\n throw new TypeError(`value is not JSON-encodable: ${String(value)}`);\n }\n\n return value;\n}\n\n/** Serializes with object keys in UTF-8 byte order at every level. */\nfunction serialize(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(serialize).join(',')}]`;\n }\n if (value !== null && typeof value === 'object') {\n const entries = Object.keys(value as Record<string, unknown>)\n .sort(compareUtf8)\n .map((key) => `${serialize(key)}:${serialize((value as Record<string, unknown>)[key])}`);\n return `{${entries.join(',')}}`;\n }\n // Go distinguishes negative zero; JSON.stringify renders it as \"0\".\n if (typeof value === 'number' && Object.is(value, -0)) return '-0';\n\n const encoded = JSON.stringify(value);\n if (encoded === undefined) {\n throw new TypeError('value is not JSON-encodable');\n }\n return encoded.replace(GO_ESCAPE_PATTERN, (c) => GO_ESCAPES[c] ?? c);\n}\n\n/** Returns the canonical JSON encoding of a value. */\nexport function canonicalJson(value: unknown): string {\n return serialize(normalize(value ?? null));\n}\n\n/** Returns the hex-encoded SHA-256 of a value's canonical JSON encoding. */\nexport async function canonicalChecksum(value: unknown): Promise<string> {\n const bytes = utf8.encode(canonicalJson(value));\n const digest = await crypto.subtle.digest('SHA-256', bytes);\n return Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n","export { canonicalJson, canonicalChecksum } from './canonical.js';\nexport { findValuesByKey as findJsonValues } from '../object/index.js';\n\n/**\n * Options for JSON stringification\n */\nexport interface StringifyOptions {\n /** Enable pretty printing with indentation */\n pretty?: boolean;\n /** Number of spaces for indentation (default: 2) */\n indent?: number;\n}\n\n/**\n * Safely parse a JSON string\n * @param input - JSON string to parse\n * @returns Parsed JSON value\n * @throws Error if input is not valid JSON\n */\nexport function parseJSON<T = unknown>(input: string): T {\n if (typeof input !== 'string') {\n throw new Error('Input must be a string');\n }\n\n if (input.trim() === '') {\n throw new Error('Input cannot be empty');\n }\n\n try {\n return JSON.parse(input) as T;\n } catch (error) {\n throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\n/**\n * Safely stringify a value to JSON\n * @param value - Value to stringify\n * @param options - Stringification options\n * @returns JSON string\n * @throws Error if value contains circular references or cannot be stringified\n */\nexport function stringifyJSON(value: unknown, options: StringifyOptions = {}): string {\n const { pretty = false, indent = 2 } = options;\n\n try {\n if (pretty) {\n return JSON.stringify(value, null, indent);\n }\n return JSON.stringify(value);\n } catch (error) {\n throw new Error(`Failed to stringify JSON: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\n/**\n * Check if a string is valid JSON\n * @param input - String to validate\n * @returns true if input is valid JSON, false otherwise\n */\nexport function isValidJSON(input: unknown): boolean {\n if (typeof input !== 'string') {\n return false;\n }\n\n if (input.trim() === '') {\n return false;\n }\n\n try {\n JSON.parse(input);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
package/dist/xml/index.d.ts
CHANGED
|
@@ -21,7 +21,9 @@ declare function parseXml(xml: string, options?: Partial<X2jOptions> & {
|
|
|
21
21
|
/**
|
|
22
22
|
* Build XML string from JavaScript object
|
|
23
23
|
*
|
|
24
|
-
* @param obj - JavaScript object to convert to XML
|
|
24
|
+
* @param obj - JavaScript object to convert to XML, or a preserveOrder node list.
|
|
25
|
+
* An array is only meaningful with `preserveOrder: true`; without it the builder
|
|
26
|
+
* indexes the array and emits elements named by its numeric keys.
|
|
25
27
|
* @param options - Optional builder configuration (merged with defaults)
|
|
26
28
|
* @returns XML string
|
|
27
29
|
*
|
|
@@ -32,7 +34,7 @@ declare function parseXml(xml: string, options?: Partial<X2jOptions> & {
|
|
|
32
34
|
* // Returns formatted XML string
|
|
33
35
|
* ```
|
|
34
36
|
*/
|
|
35
|
-
declare function buildXml(obj: Record<string, unknown
|
|
37
|
+
declare function buildXml(obj: Record<string, unknown> | unknown[], options?: Partial<XmlBuilderOptions>): string;
|
|
36
38
|
/**
|
|
37
39
|
* Validate if a string is well-formed XML
|
|
38
40
|
*
|
|
@@ -46,6 +48,19 @@ declare function buildXml(obj: Record<string, unknown>, options?: Partial<XmlBui
|
|
|
46
48
|
* isValidXml('not xml'); // false
|
|
47
49
|
* ```
|
|
48
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* Whether a character is legal in an XML 1.0 document, per the Char production.
|
|
53
|
+
* The one home for this rule: an emitter deciding what to replace and a
|
|
54
|
+
* validator deciding what to reject must not answer it differently.
|
|
55
|
+
*/
|
|
56
|
+
declare function isXmlChar(ch: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Replace every character XML 1.0 forbids with U+FFFD, which is what Go's
|
|
59
|
+
* encoding/xml does. XML defines no escape for these — a numeric reference to a
|
|
60
|
+
* C0 control is itself illegal — so the choice is a replacement character or a
|
|
61
|
+
* document no conforming parser will read.
|
|
62
|
+
*/
|
|
63
|
+
declare function xmlSafeText(value: string): string;
|
|
49
64
|
declare function isValidXml(xml: string): boolean;
|
|
50
65
|
/**
|
|
51
66
|
* Parse XML with array handling for repeated elements
|
|
@@ -81,5 +96,5 @@ declare function parseXmlWithArrays(xml: string, arrayTags: string[], options?:
|
|
|
81
96
|
*/
|
|
82
97
|
declare function extractTextFromXml(xml: string): string;
|
|
83
98
|
//#endregion
|
|
84
|
-
export { buildXml, extractTextFromXml, findValuesByKey as findXmlValues, isValidXml, parseXml, parseXmlWithArrays };
|
|
99
|
+
export { buildXml, extractTextFromXml, findValuesByKey as findXmlValues, isValidXml, isXmlChar, parseXml, parseXmlWithArrays, xmlSafeText };
|
|
85
100
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/xml/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/xml/index.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAuDgB,SACd,aACA,UAAU,QAAQ;EAAgB;IACjC
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/xml/index.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAuDgB,SACd,aACA,UAAU,QAAQ;EAAgB;IACjC;;;;;;;;;;;;;;;;;iBAsCa,SACd,KAAK,qCACL,UAAU,QAAQ;;;;;;;;;;;;;;;;;;;iBA6BJ,UAAU;;;;;;;iBAkBV,YAAY;iBAMZ,WAAW;;;;;;;;;;;;;;;;;iBAgCX,mBACd,aACA,qBACA,UAAU,QAAQ;EAAgB;IACjC;;;;;;;;;;;;;;iBAoCa,mBAAmB"}
|
package/dist/xml/index.js
CHANGED
|
@@ -60,7 +60,9 @@ function parseXml(xml, options) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Build XML string from JavaScript object
|
|
62
62
|
*
|
|
63
|
-
* @param obj - JavaScript object to convert to XML
|
|
63
|
+
* @param obj - JavaScript object to convert to XML, or a preserveOrder node list.
|
|
64
|
+
* An array is only meaningful with `preserveOrder: true`; without it the builder
|
|
65
|
+
* indexes the array and emits elements named by its numeric keys.
|
|
64
66
|
* @param options - Optional builder configuration (merged with defaults)
|
|
65
67
|
* @returns XML string
|
|
66
68
|
*
|
|
@@ -90,8 +92,29 @@ function buildXml(obj, options) {
|
|
|
90
92
|
* isValidXml('not xml'); // false
|
|
91
93
|
* ```
|
|
92
94
|
*/
|
|
95
|
+
/**
|
|
96
|
+
* Whether a character is legal in an XML 1.0 document, per the Char production.
|
|
97
|
+
* The one home for this rule: an emitter deciding what to replace and a
|
|
98
|
+
* validator deciding what to reject must not answer it differently.
|
|
99
|
+
*/
|
|
100
|
+
function isXmlChar(ch) {
|
|
101
|
+
const c = ch.codePointAt(0);
|
|
102
|
+
return c === 9 || c === 10 || c === 13 || c >= 32 && c <= 55295 || c >= 57344 && c <= 65533 || c >= 65536;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Replace every character XML 1.0 forbids with U+FFFD, which is what Go's
|
|
106
|
+
* encoding/xml does. XML defines no escape for these — a numeric reference to a
|
|
107
|
+
* C0 control is itself illegal — so the choice is a replacement character or a
|
|
108
|
+
* document no conforming parser will read.
|
|
109
|
+
*/
|
|
110
|
+
function xmlSafeText(value) {
|
|
111
|
+
let out = "";
|
|
112
|
+
for (const ch of value) out += isXmlChar(ch) ? ch : "�";
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
93
115
|
function isValidXml(xml) {
|
|
94
116
|
if (!xml || xml.trim().length === 0) return false;
|
|
117
|
+
for (const ch of xml) if (!isXmlChar(ch)) return false;
|
|
95
118
|
return XMLValidator.validate(xml) === true;
|
|
96
119
|
}
|
|
97
120
|
/**
|
|
@@ -152,6 +175,6 @@ function extractTextRecursive(obj) {
|
|
|
152
175
|
return "";
|
|
153
176
|
}
|
|
154
177
|
//#endregion
|
|
155
|
-
export { buildXml, extractTextFromXml, findValuesByKey as findXmlValues, isValidXml, parseXml, parseXmlWithArrays };
|
|
178
|
+
export { buildXml, extractTextFromXml, findValuesByKey as findXmlValues, isValidXml, isXmlChar, parseXml, parseXmlWithArrays, xmlSafeText };
|
|
156
179
|
|
|
157
180
|
//# sourceMappingURL=index.js.map
|
package/dist/xml/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["record"],"sources":["../../src/xml/index.ts"],"sourcesContent":["/**\n * XML parsing utilities for HDF converters\n * Provides XML parsing with sensible defaults for security tool output\n */\n\nimport { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser';\nimport type { X2jOptions, XmlBuilderOptions } from 'fast-xml-parser';\n\nexport { findValuesByKey as findXmlValues } from '../object/index.js';\n\n/**\n * Default options for XML parsing optimized for security tool converters.\n *\n * **XXE Safety**: fast-xml-parser v5.x does not process DTD or external entities\n * by default. The `processEntities: false` option is set as defense-in-depth to\n * ensure entity expansion is disabled even if future versions change defaults.\n * See: https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/docs/v5/2.XMLparseOptions.md\n */\nconst DEFAULT_PARSE_OPTIONS: Partial<X2jOptions> = {\n attributeNamePrefix: '',\n textNodeName: '#text',\n ignoreAttributes: false,\n ignoreDeclaration: true,\n parseAttributeValue: false,\n parseTagValue: false,\n removeNSPrefix: true,\n processEntities: false,\n};\n\n/**\n * Default options for XML building\n */\nconst DEFAULT_BUILD_OPTIONS: Partial<XmlBuilderOptions> = {\n attributeNamePrefix: '',\n textNodeName: '#text',\n ignoreAttributes: false,\n format: true,\n indentBy: ' ',\n suppressEmptyNode: false,\n};\n\n/**\n * Parse XML string to JavaScript object\n *\n * @param xml - XML string to parse\n * @param options - Optional parser configuration (merged with defaults)\n * @returns Parsed JavaScript object\n *\n * @example\n * ```typescript\n * const xmlString = '<root><item id=\"1\">Test</item></root>';\n * const result = parseXml(xmlString);\n * // Returns: { root: { item: { id: '1', '#text': 'Test' } } }\n * ```\n */\nexport function parseXml(\n xml: string,\n options?: Partial<X2jOptions> & { maxSize?: number }\n): Record<string, unknown> {\n if (options?.maxSize !== undefined && xml.length > options.maxSize) {\n throw new Error(\n `Input exceeds maximum allowed size: ${xml.length} bytes exceeds limit of ${options.maxSize} bytes`\n );\n }\n\n // Validate XML first\n const validation = XMLValidator.validate(xml);\n if (validation !== true) {\n throw new Error(`Invalid XML: ${validation.err.msg}`);\n }\n\n const mergedOptions = {\n ...DEFAULT_PARSE_OPTIONS,\n ...options,\n };\n\n const parser = new XMLParser(mergedOptions);\n return parser.parse(xml) as Record<string, unknown>;\n}\n\n/**\n * Build XML string from JavaScript object\n *\n * @param obj - JavaScript object to convert to XML\n * @param options - Optional builder configuration (merged with defaults)\n * @returns XML string\n *\n * @example\n * ```typescript\n * const obj = { root: { item: { id: '1', '#text': 'Test' } } };\n * const xml = buildXml(obj);\n * // Returns formatted XML string\n * ```\n */\nexport function buildXml(\n obj: Record<string, unknown
|
|
1
|
+
{"version":3,"file":"index.js","names":["record"],"sources":["../../src/xml/index.ts"],"sourcesContent":["/**\n * XML parsing utilities for HDF converters\n * Provides XML parsing with sensible defaults for security tool output\n */\n\nimport { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser';\nimport type { X2jOptions, XmlBuilderOptions } from 'fast-xml-parser';\n\nexport { findValuesByKey as findXmlValues } from '../object/index.js';\n\n/**\n * Default options for XML parsing optimized for security tool converters.\n *\n * **XXE Safety**: fast-xml-parser v5.x does not process DTD or external entities\n * by default. The `processEntities: false` option is set as defense-in-depth to\n * ensure entity expansion is disabled even if future versions change defaults.\n * See: https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/docs/v5/2.XMLparseOptions.md\n */\nconst DEFAULT_PARSE_OPTIONS: Partial<X2jOptions> = {\n attributeNamePrefix: '',\n textNodeName: '#text',\n ignoreAttributes: false,\n ignoreDeclaration: true,\n parseAttributeValue: false,\n parseTagValue: false,\n removeNSPrefix: true,\n processEntities: false,\n};\n\n/**\n * Default options for XML building\n */\nconst DEFAULT_BUILD_OPTIONS: Partial<XmlBuilderOptions> = {\n attributeNamePrefix: '',\n textNodeName: '#text',\n ignoreAttributes: false,\n format: true,\n indentBy: ' ',\n suppressEmptyNode: false,\n};\n\n/**\n * Parse XML string to JavaScript object\n *\n * @param xml - XML string to parse\n * @param options - Optional parser configuration (merged with defaults)\n * @returns Parsed JavaScript object\n *\n * @example\n * ```typescript\n * const xmlString = '<root><item id=\"1\">Test</item></root>';\n * const result = parseXml(xmlString);\n * // Returns: { root: { item: { id: '1', '#text': 'Test' } } }\n * ```\n */\nexport function parseXml(\n xml: string,\n options?: Partial<X2jOptions> & { maxSize?: number }\n): Record<string, unknown> {\n if (options?.maxSize !== undefined && xml.length > options.maxSize) {\n throw new Error(\n `Input exceeds maximum allowed size: ${xml.length} bytes exceeds limit of ${options.maxSize} bytes`\n );\n }\n\n // Validate XML first\n const validation = XMLValidator.validate(xml);\n if (validation !== true) {\n throw new Error(`Invalid XML: ${validation.err.msg}`);\n }\n\n const mergedOptions = {\n ...DEFAULT_PARSE_OPTIONS,\n ...options,\n };\n\n const parser = new XMLParser(mergedOptions);\n return parser.parse(xml) as Record<string, unknown>;\n}\n\n/**\n * Build XML string from JavaScript object\n *\n * @param obj - JavaScript object to convert to XML, or a preserveOrder node list.\n * An array is only meaningful with `preserveOrder: true`; without it the builder\n * indexes the array and emits elements named by its numeric keys.\n * @param options - Optional builder configuration (merged with defaults)\n * @returns XML string\n *\n * @example\n * ```typescript\n * const obj = { root: { item: { id: '1', '#text': 'Test' } } };\n * const xml = buildXml(obj);\n * // Returns formatted XML string\n * ```\n */\nexport function buildXml(\n obj: Record<string, unknown> | unknown[],\n options?: Partial<XmlBuilderOptions>\n): string {\n const mergedOptions = {\n ...DEFAULT_BUILD_OPTIONS,\n ...options,\n };\n\n const builder = new XMLBuilder(mergedOptions);\n return builder.build(obj) as string;\n}\n\n/**\n * Validate if a string is well-formed XML\n *\n * @param xml - String to validate\n * @returns True if valid XML, false otherwise\n *\n * @example\n * ```typescript\n * isValidXml('<root><item>Test</item></root>'); // true\n * isValidXml('<root><item>Test</root>'); // false (mismatched tags)\n * isValidXml('not xml'); // false\n * ```\n */\n/**\n * Whether a character is legal in an XML 1.0 document, per the Char production.\n * The one home for this rule: an emitter deciding what to replace and a\n * validator deciding what to reject must not answer it differently.\n */\nexport function isXmlChar(ch: string): boolean {\n const c = ch.codePointAt(0) as number;\n return (\n c === 0x9 ||\n c === 0xa ||\n c === 0xd ||\n (c >= 0x20 && c <= 0xd7ff) ||\n (c >= 0xe000 && c <= 0xfffd) ||\n c >= 0x10000\n );\n}\n\n/**\n * Replace every character XML 1.0 forbids with U+FFFD, which is what Go's\n * encoding/xml does. XML defines no escape for these — a numeric reference to a\n * C0 control is itself illegal — so the choice is a replacement character or a\n * document no conforming parser will read.\n */\nexport function xmlSafeText(value: string): string {\n let out = '';\n for (const ch of value) out += isXmlChar(ch) ? ch : '\\ufffd';\n return out;\n}\n\nexport function isValidXml(xml: string): boolean {\n if (!xml || xml.trim().length === 0) {\n return false;\n }\n\n // The structural validator does not enforce XML 1.0's Char production, so it\n // called a document well-formed that no conforming parser will read — which is\n // how a converter emitting a raw ESC passed its own well-formedness check.\n for (const ch of xml) {\n if (!isXmlChar(ch)) return false;\n }\n\n const result = XMLValidator.validate(xml);\n return result === true;\n}\n\n/**\n * Parse XML with array handling for repeated elements\n * Forces specified tag names to always be arrays, even if only one element exists\n *\n * @param xml - XML string to parse\n * @param arrayTags - Array of tag names that should always be parsed as arrays\n * @returns Parsed JavaScript object with forced arrays\n *\n * @example\n * ```typescript\n * const xml = '<root><item>Test</item></root>';\n * const result = parseXmlWithArrays(xml, ['item']);\n * // Returns: { root: { item: ['Test'] } }\n * // Without arrayTags, would return: { root: { item: 'Test' } }\n * ```\n */\nexport function parseXmlWithArrays(\n xml: string,\n arrayTags: string[],\n options?: Partial<X2jOptions> & { maxSize?: number }\n): Record<string, unknown> {\n if (options?.maxSize !== undefined && xml.length > options.maxSize) {\n throw new Error(\n `Input exceeds maximum allowed size: ${xml.length} bytes exceeds limit of ${options.maxSize} bytes`\n );\n }\n\n // Validate XML first (consistency with parseXml)\n const validation = XMLValidator.validate(xml);\n if (validation !== true) {\n throw new Error(`Invalid XML: ${validation.err.msg}`);\n }\n\n const mergedOptions: Partial<X2jOptions> = {\n ...DEFAULT_PARSE_OPTIONS,\n ...options,\n isArray: (tagName: string) => arrayTags.includes(tagName),\n };\n\n const parser = new XMLParser(mergedOptions);\n return parser.parse(xml) as Record<string, unknown>;\n}\n\n/**\n * Extract text content from XML, stripping all tags\n *\n * @param xml - XML string\n * @returns Plain text content with tags removed\n *\n * @example\n * ```typescript\n * const xml = '<root><b>Bold</b> and <i>italic</i></root>';\n * const text = extractTextFromXml(xml);\n * // Returns: 'Bold and italic'\n * ```\n */\nexport function extractTextFromXml(xml: string): string {\n if (!isValidXml(xml)) {\n return '';\n }\n\n try {\n const parsed = parseXml(xml, { ignoreAttributes: true });\n return extractTextRecursive(parsed).trim();\n } catch {\n return '';\n }\n}\n\n/**\n * Recursively extract text from parsed XML object\n * @private\n */\nfunction extractTextRecursive(obj: unknown): string {\n if (typeof obj === 'string' || typeof obj === 'number') {\n return String(obj);\n }\n\n if (Array.isArray(obj)) {\n return obj.map(extractTextRecursive).join(' ');\n }\n\n if (typeof obj === 'object' && obj !== null) {\n const record = obj as Record<string, unknown>;\n\n // Recursively extract text from all properties\n return Object.values(record)\n .map(extractTextRecursive)\n .filter(text => text.length > 0)\n .join(' ');\n }\n\n return '';\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAM,wBAA6C;CACjD,qBAAqB;CACrB,cAAc;CACd,kBAAkB;CAClB,mBAAmB;CACnB,qBAAqB;CACrB,eAAe;CACf,gBAAgB;CAChB,iBAAiB;AACnB;;;;AAKA,MAAM,wBAAoD;CACxD,qBAAqB;CACrB,cAAc;CACd,kBAAkB;CAClB,QAAQ;CACR,UAAU;CACV,mBAAmB;AACrB;;;;;;;;;;;;;;;AAgBA,SAAgB,SACd,KACA,SACyB;CACzB,IAAI,SAAS,YAAY,KAAA,KAAa,IAAI,SAAS,QAAQ,SACzD,MAAM,IAAI,MACR,uCAAuC,IAAI,OAAO,0BAA0B,QAAQ,QAAQ,OAC9F;CAIF,MAAM,aAAa,aAAa,SAAS,GAAG;CAC5C,IAAI,eAAe,MACjB,MAAM,IAAI,MAAM,gBAAgB,WAAW,IAAI,KAAK;CAStD,OAAO,IADY,UAAU;EAJ3B,GAAG;EACH,GAAG;CAGoC,CAC7B,CAAC,CAAC,MAAM,GAAG;AACzB;;;;;;;;;;;;;;;;;AAkBA,SAAgB,SACd,KACA,SACQ;CAOR,OAAO,IADa,WAAW;EAJ7B,GAAG;EACH,GAAG;CAGsC,CAC9B,CAAC,CAAC,MAAM,GAAG;AAC1B;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,UAAU,IAAqB;CAC7C,MAAM,IAAI,GAAG,YAAY,CAAC;CAC1B,OACE,MAAM,KACN,MAAM,MACN,MAAM,MACL,KAAK,MAAQ,KAAK,SAClB,KAAK,SAAU,KAAK,SACrB,KAAK;AAET;;;;;;;AAQA,SAAgB,YAAY,OAAuB;CACjD,IAAI,MAAM;CACV,KAAK,MAAM,MAAM,OAAO,OAAO,UAAU,EAAE,IAAI,KAAK;CACpD,OAAO;AACT;AAEA,SAAgB,WAAW,KAAsB;CAC/C,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC,WAAW,GAChC,OAAO;CAMT,KAAK,MAAM,MAAM,KACf,IAAI,CAAC,UAAU,EAAE,GAAG,OAAO;CAI7B,OADe,aAAa,SAAS,GACzB,MAAM;AACpB;;;;;;;;;;;;;;;;;AAkBA,SAAgB,mBACd,KACA,WACA,SACyB;CACzB,IAAI,SAAS,YAAY,KAAA,KAAa,IAAI,SAAS,QAAQ,SACzD,MAAM,IAAI,MACR,uCAAuC,IAAI,OAAO,0BAA0B,QAAQ,QAAQ,OAC9F;CAIF,MAAM,aAAa,aAAa,SAAS,GAAG;CAC5C,IAAI,eAAe,MACjB,MAAM,IAAI,MAAM,gBAAgB,WAAW,IAAI,KAAK;CAUtD,OAAO,IADY,UAAU;EAL3B,GAAG;EACH,GAAG;EACH,UAAU,YAAoB,UAAU,SAAS,OAAO;CAGjB,CAC7B,CAAC,CAAC,MAAM,GAAG;AACzB;;;;;;;;;;;;;;AAeA,SAAgB,mBAAmB,KAAqB;CACtD,IAAI,CAAC,WAAW,GAAG,GACjB,OAAO;CAGT,IAAI;EAEF,OAAO,qBADQ,SAAS,KAAK,EAAE,kBAAkB,KAAK,CACrB,CAAC,CAAC,CAAC,KAAK;CAC3C,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAMA,SAAS,qBAAqB,KAAsB;CAClD,IAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAC5C,OAAO,OAAO,GAAG;CAGnB,IAAI,MAAM,QAAQ,GAAG,GACnB,OAAO,IAAI,IAAI,oBAAoB,CAAC,CAAC,KAAK,GAAG;CAG/C,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAIrC,OAAO,OAAO,OAAOA,GAAM,CAAC,CACzB,IAAI,oBAAoB,CAAC,CACzB,QAAO,SAAQ,KAAK,SAAS,CAAC,CAAC,CAC/B,KAAK,GAAG;CAGb,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mitre/hdf-utilities",
|
|
3
|
-
"version": "3.6.0-rc.
|
|
3
|
+
"version": "3.6.0-rc.3",
|
|
4
4
|
"description": "Utility functions for HDF libraries (JSON parsing, validation helpers)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -76,8 +76,6 @@
|
|
|
76
76
|
"test:watch": "vitest",
|
|
77
77
|
"test:coverage": "vitest run --coverage",
|
|
78
78
|
"type-check": "tsc --noEmit",
|
|
79
|
-
"lint": "
|
|
80
|
-
"lint:go": "cd go && golangci-lint run",
|
|
81
|
-
"lint:fix": "eslint src test --fix"
|
|
79
|
+
"lint:go": "cd go && golangci-lint run"
|
|
82
80
|
}
|
|
83
81
|
}
|
package/dist/json/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/json/index.ts"],"mappings":";;;;;UAKiB;;EAEf;;EAEA;;;;;;;;iBASc,UAAU,aAAa,gBAAgB;;;;;;;;iBAuBvC,cAAc,gBAAgB,UAAS;;;;;;iBAkBvC,YAAY"}
|
package/dist/json/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/json/index.ts"],"sourcesContent":["export { findValuesByKey as findJsonValues } from '../object/index.js';\n\n/**\n * Options for JSON stringification\n */\nexport interface StringifyOptions {\n /** Enable pretty printing with indentation */\n pretty?: boolean;\n /** Number of spaces for indentation (default: 2) */\n indent?: number;\n}\n\n/**\n * Safely parse a JSON string\n * @param input - JSON string to parse\n * @returns Parsed JSON value\n * @throws Error if input is not valid JSON\n */\nexport function parseJSON<T = unknown>(input: string): T {\n if (typeof input !== 'string') {\n throw new Error('Input must be a string');\n }\n\n if (input.trim() === '') {\n throw new Error('Input cannot be empty');\n }\n\n try {\n return JSON.parse(input) as T;\n } catch (error) {\n throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\n/**\n * Safely stringify a value to JSON\n * @param value - Value to stringify\n * @param options - Stringification options\n * @returns JSON string\n * @throws Error if value contains circular references or cannot be stringified\n */\nexport function stringifyJSON(value: unknown, options: StringifyOptions = {}): string {\n const { pretty = false, indent = 2 } = options;\n\n try {\n if (pretty) {\n return JSON.stringify(value, null, indent);\n }\n return JSON.stringify(value);\n } catch (error) {\n throw new Error(`Failed to stringify JSON: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\n/**\n * Check if a string is valid JSON\n * @param input - String to validate\n * @returns true if input is valid JSON, false otherwise\n */\nexport function isValidJSON(input: unknown): boolean {\n if (typeof input !== 'string') {\n return false;\n }\n\n if (input.trim() === '') {\n return false;\n }\n\n try {\n JSON.parse(input);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;AAkBA,SAAgB,UAAuB,OAAkB;CACvD,IAAI,OAAO,UAAU,UACnB,MAAM,IAAI,MAAM,wBAAwB;CAG1C,IAAI,MAAM,KAAK,MAAM,IACnB,MAAM,IAAI,MAAM,uBAAuB;CAGzC,IAAI;EACF,OAAO,KAAK,MAAM,KAAK;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,iBAAiB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;CAC3F;AACF;;;;;;;;AASA,SAAgB,cAAc,OAAgB,UAA4B,CAAC,GAAW;CACpF,MAAM,EAAE,SAAS,OAAO,SAAS,MAAM;CAEvC,IAAI;EACF,IAAI,QACF,OAAO,KAAK,UAAU,OAAO,MAAM,MAAM;EAE3C,OAAO,KAAK,UAAU,KAAK;CAC7B,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,6BAA6B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;CACvG;AACF;;;;;;AAOA,SAAgB,YAAY,OAAyB;CACnD,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,IAAI,MAAM,KAAK,MAAM,IACnB,OAAO;CAGT,IAAI;EACF,KAAK,MAAM,KAAK;EAChB,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF"}
|