@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
|
@@ -0,0 +1,193 @@
|
|
|
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
|
+
/**
|
|
154
|
+
* Render a FINITE JSON number as text the way Go's
|
|
155
|
+
* strconv.FormatFloat(v, 'f', -1, 64) does: shortest round-trip digits,
|
|
156
|
+
* positional always, never exponent notation.
|
|
157
|
+
*
|
|
158
|
+
* JavaScript's own Number-to-string switches to exponent at and above 1e21 and
|
|
159
|
+
* below 1e-6, and drops the sign of negative zero, so a converter that emits
|
|
160
|
+
* String(n) diverges from its Go peer at both magnitude extremes and at -0.
|
|
161
|
+
* Expansion works on the digit string rather than toFixed, which caps at 100
|
|
162
|
+
* fraction digits and so cannot render the smallest denormals at all.
|
|
163
|
+
*
|
|
164
|
+
* Non-finite values are passed through as JavaScript renders them and do NOT
|
|
165
|
+
* match Go: this returns "Infinity"/"-Infinity" where FormatFloat returns
|
|
166
|
+
* "+Inf"/"-Inf" ("NaN" agrees). JSON has no literal for any of them, but
|
|
167
|
+
* JSON.parse manufactures Infinity from an overflow literal such as 1e400, so
|
|
168
|
+
* the case is reachable. The Go peer never renders that value at all -- its
|
|
169
|
+
* ParseFloat fails and it emits the original token text instead -- which is a
|
|
170
|
+
* documented cross-language divergence, not something this helper can close.
|
|
171
|
+
*/
|
|
172
|
+
function formatJsonNumber(value) {
|
|
173
|
+
if (Object.is(value, -0)) return "-0";
|
|
174
|
+
if (!Number.isFinite(value)) return String(value);
|
|
175
|
+
const rendered = String(value);
|
|
176
|
+
const exponent = rendered.indexOf("e");
|
|
177
|
+
if (exponent < 0) return rendered;
|
|
178
|
+
const negative = rendered.startsWith("-");
|
|
179
|
+
const significand = rendered.slice(negative ? 1 : 0, exponent);
|
|
180
|
+
const power = Number(rendered.slice(exponent + 1));
|
|
181
|
+
const point = significand.indexOf(".");
|
|
182
|
+
const digits = point < 0 ? significand : significand.slice(0, point) + significand.slice(point + 1);
|
|
183
|
+
const position = (point < 0 ? significand.length : point) + power;
|
|
184
|
+
let out;
|
|
185
|
+
if (position <= 0) out = `0.${"0".repeat(-position)}${digits}`;
|
|
186
|
+
else if (position >= digits.length) out = digits + "0".repeat(position - digits.length);
|
|
187
|
+
else out = `${digits.slice(0, position)}.${digits.slice(position)}`;
|
|
188
|
+
return negative ? `-${out}` : out;
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
export { canonicalChecksum as a, stringifyJSON as i, isValidJSON as n, canonicalJson as o, parseJSON as r, formatJsonNumber as t };
|
|
192
|
+
|
|
193
|
+
//# sourceMappingURL=json-DdbPTX2g.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-DdbPTX2g.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\n/**\n * Render a FINITE JSON number as text the way Go's\n * strconv.FormatFloat(v, 'f', -1, 64) does: shortest round-trip digits,\n * positional always, never exponent notation.\n *\n * JavaScript's own Number-to-string switches to exponent at and above 1e21 and\n * below 1e-6, and drops the sign of negative zero, so a converter that emits\n * String(n) diverges from its Go peer at both magnitude extremes and at -0.\n * Expansion works on the digit string rather than toFixed, which caps at 100\n * fraction digits and so cannot render the smallest denormals at all.\n *\n * Non-finite values are passed through as JavaScript renders them and do NOT\n * match Go: this returns \"Infinity\"/\"-Infinity\" where FormatFloat returns\n * \"+Inf\"/\"-Inf\" (\"NaN\" agrees). JSON has no literal for any of them, but\n * JSON.parse manufactures Infinity from an overflow literal such as 1e400, so\n * the case is reachable. The Go peer never renders that value at all -- its\n * ParseFloat fails and it emits the original token text instead -- which is a\n * documented cross-language divergence, not something this helper can close.\n */\nexport function formatJsonNumber(value: number): string {\n if (Object.is(value, -0)) return '-0';\n if (!Number.isFinite(value)) return String(value);\n\n const rendered = String(value);\n const exponent = rendered.indexOf('e');\n if (exponent < 0) return rendered;\n\n const negative = rendered.startsWith('-');\n const significand = rendered.slice(negative ? 1 : 0, exponent);\n const power = Number(rendered.slice(exponent + 1));\n const point = significand.indexOf('.');\n const digits = point < 0 ? significand : significand.slice(0, point) + significand.slice(point + 1);\n // Where the decimal point lands once the exponent is applied, counted from the\n // left of the digit string; <= 0 means the value is entirely below the point.\n const position = (point < 0 ? significand.length : point) + power;\n\n let out: string;\n if (position <= 0) {\n out = `0.${'0'.repeat(-position)}${digits}`;\n } else if (position >= digits.length) {\n out = digits + '0'.repeat(position - digits.length);\n } else {\n out = `${digits.slice(0, position)}.${digits.slice(position)}`;\n }\n return negative ? `-${out}` : out;\n}\n"],"mappings":"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"}
|
package/dist/string/index.d.ts
CHANGED
|
@@ -94,6 +94,12 @@ declare function formatTimestamp(d: Date): string;
|
|
|
94
94
|
* formatTimestampSeconds(new Date('2024-11-15T10:30:00.123Z')); // '2024-11-15T10:30:00Z'
|
|
95
95
|
*/
|
|
96
96
|
declare function formatTimestampSeconds(d: Date): string;
|
|
97
|
+
/**
|
|
98
|
+
* Base64-encode the UTF-8 bytes of a string. Runtime-agnostic (TextEncoder +
|
|
99
|
+
* btoa) so library code stays usable from browser bundles, where Node's
|
|
100
|
+
* `Buffer` does not exist. Equivalent to `Buffer.from(text, 'utf-8').toString('base64')`.
|
|
101
|
+
*/
|
|
102
|
+
declare function encodeBase64Utf8(text: string): string;
|
|
97
103
|
/**
|
|
98
104
|
* Rewrite zone-less timestamps in a raw HDF document to HDF's canonical
|
|
99
105
|
* trimmed-UTC RFC3339 form, before the document is parsed.
|
|
@@ -110,5 +116,5 @@ declare function formatTimestampSeconds(d: Date): string;
|
|
|
110
116
|
*/
|
|
111
117
|
declare function normalizeHdfTimestamps(json: string): string;
|
|
112
118
|
//#endregion
|
|
113
|
-
export { formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction };
|
|
119
|
+
export { encodeBase64Utf8, formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction };
|
|
114
120
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/string/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BgB,UAAU;;;;;;;;;;;;;;;;;;iBAqBV,gBAAgB,cAAc;;;;;;;;;;;;;;;;;;;;;iBAmD9B,eAAe,YAAY;;;;;;;;;;iBAkC3B,gBAAgB;;;;;;;;;iBAqBhB,gBAAgB,GAAG;;;;;;;;;;iBAanB,uBAAuB,GAAG;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/string/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BgB,UAAU;;;;;;;;;;;;;;;;;;iBAqBV,gBAAgB,cAAc;;;;;;;;;;;;;;;;;;;;;iBAmD9B,eAAe,YAAY;;;;;;;;;;iBAkC3B,gBAAgB;;;;;;;;;iBAqBhB,gBAAgB,GAAG;;;;;;;;;;iBAanB,uBAAuB,GAAG;;;;;;iBAa1B,iBAAiB;;;;;;;;;;;;;;;iBA0BjB,uBAAuB"}
|
package/dist/string/index.js
CHANGED
|
@@ -138,6 +138,18 @@ function formatTimestamp(d) {
|
|
|
138
138
|
function formatTimestampSeconds(d) {
|
|
139
139
|
return d.toISOString().replace(/\.\d+Z$/, "Z");
|
|
140
140
|
}
|
|
141
|
+
const BASE64_CHUNK_BYTES = 32768;
|
|
142
|
+
/**
|
|
143
|
+
* Base64-encode the UTF-8 bytes of a string. Runtime-agnostic (TextEncoder +
|
|
144
|
+
* btoa) so library code stays usable from browser bundles, where Node's
|
|
145
|
+
* `Buffer` does not exist. Equivalent to `Buffer.from(text, 'utf-8').toString('base64')`.
|
|
146
|
+
*/
|
|
147
|
+
function encodeBase64Utf8(text) {
|
|
148
|
+
const bytes = new TextEncoder().encode(text);
|
|
149
|
+
let binary = "";
|
|
150
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));
|
|
151
|
+
return btoa(binary);
|
|
152
|
+
}
|
|
141
153
|
/** A JSON string whose entire value is an ISO-8601 date-time with no zone designator. */
|
|
142
154
|
const BARE_ISO_IN_JSON = /"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?)"/g;
|
|
143
155
|
/**
|
|
@@ -162,6 +174,6 @@ function normalizeHdfTimestamps(json) {
|
|
|
162
174
|
});
|
|
163
175
|
}
|
|
164
176
|
//#endregion
|
|
165
|
-
export { formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction };
|
|
177
|
+
export { encodeBase64Utf8, formatTimestamp, formatTimestampSeconds, normalizeHdfTimestamps, parseTimestamp, replaceHtmlTags, stripHtml, trimUtcFraction };
|
|
166
178
|
|
|
167
179
|
//# sourceMappingURL=index.js.map
|
package/dist/string/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/string/index.ts"],"sourcesContent":["/**\n * String manipulation utilities for HDF converters.\n *\n * These handle common operations on security tool output strings:\n * HTML stripping for STIG descriptions and SonarQube messages,\n * and multi-format timestamp parsing for tool output dates.\n */\n\n/**\n * Remove HTML tags from a string and normalize whitespace.\n *\n * Strips all HTML/XML tags, collapses multiple whitespace characters\n * into single spaces, and trims leading/trailing whitespace.\n *\n * For XML documents, use {@link extractTextFromXml} from the xml module instead.\n * This function is for inline HTML in non-XML strings (STIG descriptions,\n * SonarQube messages, etc.).\n *\n * @param html - String potentially containing HTML tags\n * @returns Plain text with tags removed and whitespace normalized\n *\n * @example\n * ```typescript\n * stripHtml('<p>hello</p> <b>world</b>'); // 'hello world'\n * stripHtml('no tags here'); // 'no tags here'\n * ```\n */\nexport function stripHtml(html: string): string {\n return replaceHtmlTags(html, ' ').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Replace every `<...>` tag span with `replacement`, in a single linear scan.\n *\n * Matches the historical `replace(/<[^>]*>/g, replacement)` span semantics: a\n * span runs from `<` to the first following `>`; a trailing `<` with no\n * closing `>` stays literal. The manual scan avoids that regex's quadratic\n * backtracking on pathological inputs (e.g. long runs of `<`).\n *\n * `replacement` is inserted literally (no `$`-pattern expansion, unlike\n * `String.replace`) and is expected to be a plain separator such as `' '` or\n * `''`. For such replacements — anything containing no `<` or `>` — the output\n * can never contain a `<...>` pair, so tags cannot be reassembled from\n * fragments.\n *\n * @param html - String potentially containing HTML tags\n * @param replacement - What each tag span becomes (`' '` or `''`)\n */\nexport function replaceHtmlTags(html: string, replacement: string): string {\n let out = '';\n let i = 0;\n while (i < html.length) {\n const lt = html.indexOf('<', i);\n if (lt === -1) {\n out += html.slice(i);\n break;\n }\n const gt = html.indexOf('>', lt + 1);\n if (gt === -1) {\n out += html.slice(i);\n break;\n }\n out += html.slice(i, lt) + replacement;\n i = gt + 1;\n }\n return out;\n}\n\n// A bare ISO-8601 datetime with a time component but no timezone designator.\n// JavaScript's Date constructor interprets such a value as host-LOCAL time,\n// whereas Go's time.Parse (and HDF's canonical model) treats it as UTC. We\n// append 'Z' to force UTC so output is host-timezone-independent.\nconst ISO_DATETIME_NO_ZONE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2})?(\\.\\d+)?$/;\n\n// C ctime layout (\"Tue Mar 22 14:54:47 2022\", or \"Mon Jan 2 ...\" for a\n// single-digit day). Go's hdfutil.ParseTimestamp parses this layout as UTC;\n// V8's Date treats it as host-local, so we append a 'GMT' designator to match.\nconst CTIME_NO_ZONE = /^[A-Za-z]{3} [A-Za-z]{3} +\\d{1,2} \\d{2}:\\d{2}:\\d{2} \\d{4}$/;\n\n/**\n * Parse a timestamp string in various common formats into a Date.\n *\n * A zone-less ISO datetime (e.g. `2024-01-15T10:30:00`) is interpreted as\n * UTC — matching Go's `hdfutil.ParseTimestamp` — rather than host-local time,\n * so converter output does not depend on the machine's timezone.\n *\n * Returns `null` if the input is empty or cannot be parsed.\n *\n * @param s - Timestamp string to parse\n * @returns Parsed Date or null if unparseable\n *\n * @example\n * ```typescript\n * parseTimestamp('2024-01-15T10:30:00Z'); // Date object\n * parseTimestamp('2024-01-15T10:30:00'); // Date object (interpreted as UTC)\n * parseTimestamp('not a date'); // null\n * parseTimestamp(''); // null\n * ```\n */\nexport function parseTimestamp(s: string): Date | null {\n if (!s || s.trim().length === 0) {\n return null;\n }\n\n const trimmed = s.trim();\n let normalized = trimmed;\n if (ISO_DATETIME_NO_ZONE.test(trimmed)) {\n normalized = `${trimmed.replace(' ', 'T')}Z`;\n } else if (CTIME_NO_ZONE.test(trimmed)) {\n normalized = `${trimmed} GMT`;\n }\n\n // parseTimestamp IS the sanctioned helper the guard routes callers to: it\n // normalizes zone-less input to UTC above before constructing the Date, so\n // this is not the host-local footgun the guard prevents.\n // eslint-disable-next-line no-restricted-syntax -- sanctioned parse-helper implementation\n const parsed = new Date(normalized);\n if (isNaN(parsed.getTime())) {\n return null;\n }\n\n return parsed;\n}\n\n/**\n * Trim trailing fractional-second zeros from an RFC3339 UTC timestamp string,\n * matching Go's RFC3339Nano marshaling (which drops trailing zeros and the\n * decimal point when the fraction is all zeros).\n *\n * @example\n * trimUtcFraction('2024-11-15T10:30:00.000Z'); // '2024-11-15T10:30:00Z'\n * trimUtcFraction('2024-01-01T00:00:00.120Z'); // '2024-01-01T00:00:00.12Z'\n */\nexport function trimUtcFraction(s: string): string {\n // The digits are located with an unambiguous (linear) pattern; the\n // trailing-zero trim happens on the bounded capture with plain string ops.\n const m = /\\.(\\d*)Z$/.exec(s);\n if (!m) return s;\n const digits = m[1] as string;\n let end = digits.length;\n while (end > 0 && digits[end - 1] === '0') end--;\n if (end === digits.length) return s; // no trailing zeros (or no digits at all)\n const kept = digits.slice(0, end);\n return s.slice(0, m.index) + (kept ? `.${kept}Z` : 'Z');\n}\n\n/**\n * Format a Date as HDF's canonical timestamp: RFC3339 in UTC with trailing\n * fractional-second zeros trimmed. Byte-identical to what the Go converters\n * emit for the same instant (a UTC `time.Time` marshaled as RFC3339Nano).\n *\n * @example\n * formatTimestamp(new Date('2024-11-15T10:30:00Z')); // '2024-11-15T10:30:00Z'\n */\nexport function formatTimestamp(d: Date): string {\n return trimUtcFraction(d.toISOString());\n}\n\n/**\n * Format a Date as RFC3339 in UTC truncated to whole seconds (no fractional\n * part). For exporters whose target format conventionally uses whole-second\n * timestamps (CSAF, OpenVEX, CycloneDX). Use {@link formatTimestamp} for\n * canonical HDF output instead — it preserves sub-second precision.\n *\n * @example\n * formatTimestampSeconds(new Date('2024-11-15T10:30:00.123Z')); // '2024-11-15T10:30:00Z'\n */\nexport function formatTimestampSeconds(d: Date): string {\n return d.toISOString().replace(/\\.\\d+Z$/, 'Z');\n}\n\n/** A JSON string whose entire value is an ISO-8601 date-time with no zone designator. */\nconst BARE_ISO_IN_JSON = /\"(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)\"/g;\n\n/**\n * Rewrite zone-less timestamps in a raw HDF document to HDF's canonical\n * trimmed-UTC RFC3339 form, before the document is parsed.\n *\n * Real HDF carries them — InSpec emits startTime with no offset. Go's schema\n * types decode date-time fields into time.Time, whose decoder demands an offset\n * and hard-fails, so Go rejected documents TypeScript read happily; and where\n * TypeScript did read them, it passed the zone-less value straight through to\n * its output, which is not canonical HDF. Normalizing on ingest in both\n * languages means they accept the same real-world files and emit the same bytes,\n * reading a zone-less value as UTC exactly as parseTimestamp already does.\n *\n * Mirrored by NormalizeHDFTimestamps in hdf-utilities/go/timestamp.go.\n */\nexport function normalizeHdfTimestamps(json: string): string {\n return json.replace(BARE_ISO_IN_JSON, (match, raw: string) => {\n const parsed = parseTimestamp(raw);\n if (!parsed) return match;\n return `\"${trimUtcFraction(parsed.toISOString())}\"`;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,gBAAgB,MAAM,GAAG,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AAC9D;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,gBAAgB,MAAc,aAA6B;CACzE,IAAI,MAAM;CACV,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,QAAQ;EACtB,MAAM,KAAK,KAAK,QAAQ,KAAK,CAAC;EAC9B,IAAI,OAAO,IAAI;GACb,OAAO,KAAK,MAAM,CAAC;GACnB;EACF;EACA,MAAM,KAAK,KAAK,QAAQ,KAAK,KAAK,CAAC;EACnC,IAAI,OAAO,IAAI;GACb,OAAO,KAAK,MAAM,CAAC;GACnB;EACF;EACA,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI;EAC3B,IAAI,KAAK;CACX;CACA,OAAO;AACT;AAMA,MAAM,uBAAuB;AAK7B,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;AAsBtB,SAAgB,eAAe,GAAwB;CACrD,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,WAAW,GAC5B,OAAO;CAGT,MAAM,UAAU,EAAE,KAAK;CACvB,IAAI,aAAa;CACjB,IAAI,qBAAqB,KAAK,OAAO,GACnC,aAAa,GAAG,QAAQ,QAAQ,KAAK,GAAG,EAAE;MACrC,IAAI,cAAc,KAAK,OAAO,GACnC,aAAa,GAAG,QAAQ;CAO1B,MAAM,SAAS,IAAI,KAAK,UAAU;CAClC,IAAI,MAAM,OAAO,QAAQ,CAAC,GACxB,OAAO;CAGT,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,gBAAgB,GAAmB;CAGjD,MAAM,IAAI,YAAY,KAAK,CAAC;CAC5B,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,SAAS,EAAE;CACjB,IAAI,MAAM,OAAO;CACjB,OAAO,MAAM,KAAK,OAAO,MAAM,OAAO,KAAK;CAC3C,IAAI,QAAQ,OAAO,QAAQ,OAAO;CAClC,MAAM,OAAO,OAAO,MAAM,GAAG,GAAG;CAChC,OAAO,EAAE,MAAM,GAAG,EAAE,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK;AACrD;;;;;;;;;AAUA,SAAgB,gBAAgB,GAAiB;CAC/C,OAAO,gBAAgB,EAAE,YAAY,CAAC;AACxC;;;;;;;;;;AAWA,SAAgB,uBAAuB,GAAiB;CACtD,OAAO,EAAE,YAAY,CAAC,CAAC,QAAQ,WAAW,GAAG;AAC/C;;AAGA,MAAM,mBAAmB;;;;;;;;;;;;;;;AAgBzB,SAAgB,uBAAuB,MAAsB;CAC3D,OAAO,KAAK,QAAQ,mBAAmB,OAAO,QAAgB;EAC5D,MAAM,SAAS,eAAe,GAAG;EACjC,IAAI,CAAC,QAAQ,OAAO;EACpB,OAAO,IAAI,gBAAgB,OAAO,YAAY,CAAC,EAAE;CACnD,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/string/index.ts"],"sourcesContent":["/**\n * String manipulation utilities for HDF converters.\n *\n * These handle common operations on security tool output strings:\n * HTML stripping for STIG descriptions and SonarQube messages,\n * and multi-format timestamp parsing for tool output dates.\n */\n\n/**\n * Remove HTML tags from a string and normalize whitespace.\n *\n * Strips all HTML/XML tags, collapses multiple whitespace characters\n * into single spaces, and trims leading/trailing whitespace.\n *\n * For XML documents, use {@link extractTextFromXml} from the xml module instead.\n * This function is for inline HTML in non-XML strings (STIG descriptions,\n * SonarQube messages, etc.).\n *\n * @param html - String potentially containing HTML tags\n * @returns Plain text with tags removed and whitespace normalized\n *\n * @example\n * ```typescript\n * stripHtml('<p>hello</p> <b>world</b>'); // 'hello world'\n * stripHtml('no tags here'); // 'no tags here'\n * ```\n */\nexport function stripHtml(html: string): string {\n return replaceHtmlTags(html, ' ').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Replace every `<...>` tag span with `replacement`, in a single linear scan.\n *\n * Matches the historical `replace(/<[^>]*>/g, replacement)` span semantics: a\n * span runs from `<` to the first following `>`; a trailing `<` with no\n * closing `>` stays literal. The manual scan avoids that regex's quadratic\n * backtracking on pathological inputs (e.g. long runs of `<`).\n *\n * `replacement` is inserted literally (no `$`-pattern expansion, unlike\n * `String.replace`) and is expected to be a plain separator such as `' '` or\n * `''`. For such replacements — anything containing no `<` or `>` — the output\n * can never contain a `<...>` pair, so tags cannot be reassembled from\n * fragments.\n *\n * @param html - String potentially containing HTML tags\n * @param replacement - What each tag span becomes (`' '` or `''`)\n */\nexport function replaceHtmlTags(html: string, replacement: string): string {\n let out = '';\n let i = 0;\n while (i < html.length) {\n const lt = html.indexOf('<', i);\n if (lt === -1) {\n out += html.slice(i);\n break;\n }\n const gt = html.indexOf('>', lt + 1);\n if (gt === -1) {\n out += html.slice(i);\n break;\n }\n out += html.slice(i, lt) + replacement;\n i = gt + 1;\n }\n return out;\n}\n\n// A bare ISO-8601 datetime with a time component but no timezone designator.\n// JavaScript's Date constructor interprets such a value as host-LOCAL time,\n// whereas Go's time.Parse (and HDF's canonical model) treats it as UTC. We\n// append 'Z' to force UTC so output is host-timezone-independent.\nconst ISO_DATETIME_NO_ZONE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2})?(\\.\\d+)?$/;\n\n// C ctime layout (\"Tue Mar 22 14:54:47 2022\", or \"Mon Jan 2 ...\" for a\n// single-digit day). Go's hdfutil.ParseTimestamp parses this layout as UTC;\n// V8's Date treats it as host-local, so we append a 'GMT' designator to match.\nconst CTIME_NO_ZONE = /^[A-Za-z]{3} [A-Za-z]{3} +\\d{1,2} \\d{2}:\\d{2}:\\d{2} \\d{4}$/;\n\n/**\n * Parse a timestamp string in various common formats into a Date.\n *\n * A zone-less ISO datetime (e.g. `2024-01-15T10:30:00`) is interpreted as\n * UTC — matching Go's `hdfutil.ParseTimestamp` — rather than host-local time,\n * so converter output does not depend on the machine's timezone.\n *\n * Returns `null` if the input is empty or cannot be parsed.\n *\n * @param s - Timestamp string to parse\n * @returns Parsed Date or null if unparseable\n *\n * @example\n * ```typescript\n * parseTimestamp('2024-01-15T10:30:00Z'); // Date object\n * parseTimestamp('2024-01-15T10:30:00'); // Date object (interpreted as UTC)\n * parseTimestamp('not a date'); // null\n * parseTimestamp(''); // null\n * ```\n */\nexport function parseTimestamp(s: string): Date | null {\n if (!s || s.trim().length === 0) {\n return null;\n }\n\n const trimmed = s.trim();\n let normalized = trimmed;\n if (ISO_DATETIME_NO_ZONE.test(trimmed)) {\n normalized = `${trimmed.replace(' ', 'T')}Z`;\n } else if (CTIME_NO_ZONE.test(trimmed)) {\n normalized = `${trimmed} GMT`;\n }\n\n // parseTimestamp IS the sanctioned helper the guard routes callers to: it\n // normalizes zone-less input to UTC above before constructing the Date, so\n // this is not the host-local footgun the guard prevents.\n // eslint-disable-next-line no-restricted-syntax -- sanctioned parse-helper implementation\n const parsed = new Date(normalized);\n if (isNaN(parsed.getTime())) {\n return null;\n }\n\n return parsed;\n}\n\n/**\n * Trim trailing fractional-second zeros from an RFC3339 UTC timestamp string,\n * matching Go's RFC3339Nano marshaling (which drops trailing zeros and the\n * decimal point when the fraction is all zeros).\n *\n * @example\n * trimUtcFraction('2024-11-15T10:30:00.000Z'); // '2024-11-15T10:30:00Z'\n * trimUtcFraction('2024-01-01T00:00:00.120Z'); // '2024-01-01T00:00:00.12Z'\n */\nexport function trimUtcFraction(s: string): string {\n // The digits are located with an unambiguous (linear) pattern; the\n // trailing-zero trim happens on the bounded capture with plain string ops.\n const m = /\\.(\\d*)Z$/.exec(s);\n if (!m) return s;\n const digits = m[1] as string;\n let end = digits.length;\n while (end > 0 && digits[end - 1] === '0') end--;\n if (end === digits.length) return s; // no trailing zeros (or no digits at all)\n const kept = digits.slice(0, end);\n return s.slice(0, m.index) + (kept ? `.${kept}Z` : 'Z');\n}\n\n/**\n * Format a Date as HDF's canonical timestamp: RFC3339 in UTC with trailing\n * fractional-second zeros trimmed. Byte-identical to what the Go converters\n * emit for the same instant (a UTC `time.Time` marshaled as RFC3339Nano).\n *\n * @example\n * formatTimestamp(new Date('2024-11-15T10:30:00Z')); // '2024-11-15T10:30:00Z'\n */\nexport function formatTimestamp(d: Date): string {\n return trimUtcFraction(d.toISOString());\n}\n\n/**\n * Format a Date as RFC3339 in UTC truncated to whole seconds (no fractional\n * part). For exporters whose target format conventionally uses whole-second\n * timestamps (CSAF, OpenVEX, CycloneDX). Use {@link formatTimestamp} for\n * canonical HDF output instead — it preserves sub-second precision.\n *\n * @example\n * formatTimestampSeconds(new Date('2024-11-15T10:30:00.123Z')); // '2024-11-15T10:30:00Z'\n */\nexport function formatTimestampSeconds(d: Date): string {\n return d.toISOString().replace(/\\.\\d+Z$/, 'Z');\n}\n\n// btoa takes a \"binary string\" (one char per byte); building it in bounded\n// chunks keeps String.fromCharCode's argument list small on multi-MB input.\nconst BASE64_CHUNK_BYTES = 0x8000;\n\n/**\n * Base64-encode the UTF-8 bytes of a string. Runtime-agnostic (TextEncoder +\n * btoa) so library code stays usable from browser bundles, where Node's\n * `Buffer` does not exist. Equivalent to `Buffer.from(text, 'utf-8').toString('base64')`.\n */\nexport function encodeBase64Utf8(text: string): string {\n const bytes = new TextEncoder().encode(text);\n let binary = '';\n for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {\n binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));\n }\n return btoa(binary);\n}\n\n/** A JSON string whose entire value is an ISO-8601 date-time with no zone designator. */\nconst BARE_ISO_IN_JSON = /\"(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)\"/g;\n\n/**\n * Rewrite zone-less timestamps in a raw HDF document to HDF's canonical\n * trimmed-UTC RFC3339 form, before the document is parsed.\n *\n * Real HDF carries them — InSpec emits startTime with no offset. Go's schema\n * types decode date-time fields into time.Time, whose decoder demands an offset\n * and hard-fails, so Go rejected documents TypeScript read happily; and where\n * TypeScript did read them, it passed the zone-less value straight through to\n * its output, which is not canonical HDF. Normalizing on ingest in both\n * languages means they accept the same real-world files and emit the same bytes,\n * reading a zone-less value as UTC exactly as parseTimestamp already does.\n *\n * Mirrored by NormalizeHDFTimestamps in hdf-utilities/go/timestamp.go.\n */\nexport function normalizeHdfTimestamps(json: string): string {\n return json.replace(BARE_ISO_IN_JSON, (match, raw: string) => {\n const parsed = parseTimestamp(raw);\n if (!parsed) return match;\n return `\"${trimUtcFraction(parsed.toISOString())}\"`;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,gBAAgB,MAAM,GAAG,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AAC9D;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,gBAAgB,MAAc,aAA6B;CACzE,IAAI,MAAM;CACV,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,QAAQ;EACtB,MAAM,KAAK,KAAK,QAAQ,KAAK,CAAC;EAC9B,IAAI,OAAO,IAAI;GACb,OAAO,KAAK,MAAM,CAAC;GACnB;EACF;EACA,MAAM,KAAK,KAAK,QAAQ,KAAK,KAAK,CAAC;EACnC,IAAI,OAAO,IAAI;GACb,OAAO,KAAK,MAAM,CAAC;GACnB;EACF;EACA,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI;EAC3B,IAAI,KAAK;CACX;CACA,OAAO;AACT;AAMA,MAAM,uBAAuB;AAK7B,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;AAsBtB,SAAgB,eAAe,GAAwB;CACrD,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,WAAW,GAC5B,OAAO;CAGT,MAAM,UAAU,EAAE,KAAK;CACvB,IAAI,aAAa;CACjB,IAAI,qBAAqB,KAAK,OAAO,GACnC,aAAa,GAAG,QAAQ,QAAQ,KAAK,GAAG,EAAE;MACrC,IAAI,cAAc,KAAK,OAAO,GACnC,aAAa,GAAG,QAAQ;CAO1B,MAAM,SAAS,IAAI,KAAK,UAAU;CAClC,IAAI,MAAM,OAAO,QAAQ,CAAC,GACxB,OAAO;CAGT,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,gBAAgB,GAAmB;CAGjD,MAAM,IAAI,YAAY,KAAK,CAAC;CAC5B,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,SAAS,EAAE;CACjB,IAAI,MAAM,OAAO;CACjB,OAAO,MAAM,KAAK,OAAO,MAAM,OAAO,KAAK;CAC3C,IAAI,QAAQ,OAAO,QAAQ,OAAO;CAClC,MAAM,OAAO,OAAO,MAAM,GAAG,GAAG;CAChC,OAAO,EAAE,MAAM,GAAG,EAAE,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK;AACrD;;;;;;;;;AAUA,SAAgB,gBAAgB,GAAiB;CAC/C,OAAO,gBAAgB,EAAE,YAAY,CAAC;AACxC;;;;;;;;;;AAWA,SAAgB,uBAAuB,GAAiB;CACtD,OAAO,EAAE,YAAY,CAAC,CAAC,QAAQ,WAAW,GAAG;AAC/C;AAIA,MAAM,qBAAqB;;;;;;AAO3B,SAAgB,iBAAiB,MAAsB;CACrD,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;CAC3C,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,oBACrC,UAAU,OAAO,aAAa,GAAG,MAAM,SAAS,GAAG,IAAI,kBAAkB,CAAC;CAE5E,OAAO,KAAK,MAAM;AACpB;;AAGA,MAAM,mBAAmB;;;;;;;;;;;;;;;AAgBzB,SAAgB,uBAAuB,MAAsB;CAC3D,OAAO,KAAK,QAAQ,mBAAmB,OAAO,QAAgB;EAC5D,MAAM,SAAS,eAAe,GAAG;EACjC,IAAI,CAAC,QAAQ,OAAO;EACpB,OAAO,IAAI,gBAAgB,OAAO,YAAY,CAAC,EAAE;CACnD,CAAC;AACH"}
|
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.4",
|
|
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"}
|